@assistant-ui/mcp-docs-server 0.1.6 → 0.1.8
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/.docs/organized/code-examples/with-ai-sdk-v5.md +15 -13
- package/.docs/organized/code-examples/with-cloud.md +19 -25
- package/.docs/organized/code-examples/with-external-store.md +9 -7
- package/.docs/organized/code-examples/with-ffmpeg.md +21 -21
- package/.docs/organized/code-examples/with-langgraph.md +72 -46
- package/.docs/organized/code-examples/with-parent-id-grouping.md +9 -7
- package/.docs/organized/code-examples/with-react-hook-form.md +19 -21
- package/.docs/raw/docs/api-reference/integrations/react-data-stream.mdx +194 -0
- package/.docs/raw/docs/api-reference/overview.mdx +7 -4
- package/.docs/raw/docs/api-reference/primitives/Composer.mdx +31 -0
- package/.docs/raw/docs/api-reference/primitives/Message.mdx +108 -3
- package/.docs/raw/docs/api-reference/primitives/Thread.mdx +59 -0
- package/.docs/raw/docs/api-reference/primitives/ThreadList.mdx +128 -0
- package/.docs/raw/docs/api-reference/primitives/ThreadListItem.mdx +160 -0
- package/.docs/raw/docs/api-reference/runtimes/AssistantRuntime.mdx +0 -11
- package/.docs/raw/docs/api-reference/runtimes/ComposerRuntime.mdx +3 -3
- package/.docs/raw/docs/copilots/assistant-frame.mdx +397 -0
- package/.docs/raw/docs/getting-started.mdx +53 -52
- package/.docs/raw/docs/guides/Attachments.mdx +7 -115
- package/.docs/raw/docs/guides/ToolUI.mdx +3 -3
- package/.docs/raw/docs/guides/Tools.mdx +152 -92
- package/.docs/raw/docs/guides/context-api.mdx +574 -0
- package/.docs/raw/docs/migrations/v0-12.mdx +125 -0
- package/.docs/raw/docs/runtimes/ai-sdk/use-chat.mdx +134 -55
- package/.docs/raw/docs/runtimes/ai-sdk/v4-legacy.mdx +182 -0
- package/.docs/raw/docs/runtimes/custom/local.mdx +16 -3
- package/.docs/raw/docs/runtimes/data-stream.mdx +287 -0
- package/.docs/raw/docs/runtimes/langgraph/index.mdx +0 -1
- package/.docs/raw/docs/runtimes/langserve.mdx +9 -11
- package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +5 -0
- package/.docs/raw/docs/ui/ThreadList.mdx +54 -16
- package/dist/{chunk-L4K23SWI.js → chunk-NVNFQ5ZO.js} +4 -1
- package/dist/index.js +1 -1
- package/dist/prepare-docs/prepare.js +1 -1
- package/dist/stdio.js +1 -1
- package/package.json +7 -7
- package/.docs/organized/code-examples/local-ollama.md +0 -1135
- package/.docs/organized/code-examples/search-agent-for-e-commerce.md +0 -1721
- package/.docs/organized/code-examples/with-ai-sdk.md +0 -1082
- package/.docs/organized/code-examples/with-openai-assistants.md +0 -1175
- package/.docs/raw/docs/concepts/architecture.mdx +0 -19
- package/.docs/raw/docs/concepts/runtime-layer.mdx +0 -163
- package/.docs/raw/docs/concepts/why.mdx +0 -9
- package/.docs/raw/docs/runtimes/ai-sdk/rsc.mdx +0 -226
- package/.docs/raw/docs/runtimes/ai-sdk/use-assistant-hook.mdx +0 -195
- package/.docs/raw/docs/runtimes/ai-sdk/use-chat-hook.mdx +0 -138
- package/.docs/raw/docs/runtimes/ai-sdk/use-chat-v5.mdx +0 -212
|
@@ -1,1721 +0,0 @@
|
|
|
1
|
-
# Example: search-agent-for-e-commerce
|
|
2
|
-
|
|
3
|
-
## app/actions.tsx
|
|
4
|
-
|
|
5
|
-
```tsx
|
|
6
|
-
"use server";
|
|
7
|
-
|
|
8
|
-
import { openai } from "@ai-sdk/openai";
|
|
9
|
-
import { createAI, getMutableAIState, streamUI } from "ai/rsc";
|
|
10
|
-
import { nanoid } from "nanoid";
|
|
11
|
-
import type { ReactNode } from "react";
|
|
12
|
-
import { z } from "zod";
|
|
13
|
-
import { CarouselPlugin } from "../components/ui/productcarousel";
|
|
14
|
-
import fs from "fs";
|
|
15
|
-
import path from "path";
|
|
16
|
-
import { streamText } from "ai";
|
|
17
|
-
|
|
18
|
-
export interface ServerMessage {
|
|
19
|
-
role: "user" | "assistant";
|
|
20
|
-
content: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ClientMessage {
|
|
24
|
-
id: string;
|
|
25
|
-
role: "user" | "assistant";
|
|
26
|
-
display: ReactNode;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function continueConversation(
|
|
30
|
-
input: string,
|
|
31
|
-
indexId: string,
|
|
32
|
-
): Promise<ClientMessage> {
|
|
33
|
-
"use server";
|
|
34
|
-
|
|
35
|
-
const history = getMutableAIState();
|
|
36
|
-
|
|
37
|
-
const result = await streamUI({
|
|
38
|
-
model: openai("gpt-3.5-turbo"),
|
|
39
|
-
temperature: 0,
|
|
40
|
-
system: `\
|
|
41
|
-
You are a friendly assistant that helps the user with shopping on a ecommerce website ('DUMMY SHOP'). You help users with end-to-end shopping experience
|
|
42
|
-
starting from general information about the brands and products, and helping with product discovery, search, and product details, as well as
|
|
43
|
-
product purchase, customer support, fitting questions, technical questions.
|
|
44
|
-
Your responses are solely based on the provided context about the store and its products.
|
|
45
|
-
Right now, the user clicked on the AI assistant widget and your job is to determine their intent.
|
|
46
|
-
The user intent migth not be clear, in this case you ask clarifications questions.
|
|
47
|
-
The user quesiton might not be complete, in this case you ask for follow up questions.
|
|
48
|
-
|
|
49
|
-
Here's a list of user intents to pick from:
|
|
50
|
-
- Product search
|
|
51
|
-
- Guideline for clothes fitting
|
|
52
|
-
- Product specific questions
|
|
53
|
-
- Customer support questions (e.g. track purchase, payment issues, order issues)
|
|
54
|
-
- Escalate to human agent
|
|
55
|
-
- Ask a clarification/follow up question
|
|
56
|
-
- Product comparison
|
|
57
|
-
- Promotions, hot deals
|
|
58
|
-
`,
|
|
59
|
-
messages: [...history.get(), { role: "user", content: input, indexId }],
|
|
60
|
-
text: ({ content, done }) => {
|
|
61
|
-
if (done) {
|
|
62
|
-
history.done((messages: ServerMessage[]) => [
|
|
63
|
-
...messages,
|
|
64
|
-
{ role: "assistant", content },
|
|
65
|
-
]);
|
|
66
|
-
}
|
|
67
|
-
return <div>{content}</div>;
|
|
68
|
-
},
|
|
69
|
-
// toolChoice: 'required', // force the model to call a tool
|
|
70
|
-
// maxToolRoundtrips: 5, // allow up to 5 tool roundtrips
|
|
71
|
-
tools: {
|
|
72
|
-
product_search: {
|
|
73
|
-
description:
|
|
74
|
-
"Search for products on this website using pre-built indices",
|
|
75
|
-
parameters: z.object({
|
|
76
|
-
query: z
|
|
77
|
-
.string()
|
|
78
|
-
.describe(
|
|
79
|
-
"A clear factual product query, potentially including type, name, qualities, characteristics of the product",
|
|
80
|
-
),
|
|
81
|
-
}),
|
|
82
|
-
generate: async ({ query }) => {
|
|
83
|
-
try {
|
|
84
|
-
console.log("query=", query);
|
|
85
|
-
const response = await fetch(
|
|
86
|
-
`https://dummyjson.com/products/search?q=${query}`,
|
|
87
|
-
);
|
|
88
|
-
const data = (await response.json()) as {
|
|
89
|
-
products: {
|
|
90
|
-
thumbnail: string;
|
|
91
|
-
title: string;
|
|
92
|
-
description: string;
|
|
93
|
-
price: string;
|
|
94
|
-
url: string;
|
|
95
|
-
}[];
|
|
96
|
-
};
|
|
97
|
-
console.log("data=", data);
|
|
98
|
-
if (data.products && data.products.length > 0) {
|
|
99
|
-
const products = data.products.map((item) => ({
|
|
100
|
-
thumbnail: item.thumbnail,
|
|
101
|
-
title: item.title,
|
|
102
|
-
description: item.description,
|
|
103
|
-
metadata_3: item.price,
|
|
104
|
-
link: item.url,
|
|
105
|
-
}));
|
|
106
|
-
return <CarouselPlugin products={products} />;
|
|
107
|
-
} else {
|
|
108
|
-
return <p>No products found.</p>;
|
|
109
|
-
}
|
|
110
|
-
} catch {
|
|
111
|
-
return (
|
|
112
|
-
<p>
|
|
113
|
-
Sorry, we are experiencing some error. Please refresh the chat
|
|
114
|
-
and try again.
|
|
115
|
-
</p>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
general_question: {
|
|
121
|
-
description: "User questions not related to products directly",
|
|
122
|
-
parameters: z.object({
|
|
123
|
-
user_question: z
|
|
124
|
-
.string()
|
|
125
|
-
.describe("User questions not related to products directly"),
|
|
126
|
-
}),
|
|
127
|
-
generate: async function* ({ user_question }) {
|
|
128
|
-
const filePath = path.resolve(process.cwd(), "public/shop_info.txt");
|
|
129
|
-
const generalInfo = fs.readFileSync(filePath, "utf-8");
|
|
130
|
-
const result = streamText({
|
|
131
|
-
model: openai("gpt-3.5-turbo"),
|
|
132
|
-
temperature: 0,
|
|
133
|
-
prompt: `Generate response to user question ${user_question} based on the context ${generalInfo}`,
|
|
134
|
-
});
|
|
135
|
-
let textContent = "";
|
|
136
|
-
|
|
137
|
-
for await (const textPart of result.textStream) {
|
|
138
|
-
textContent += textPart;
|
|
139
|
-
yield textContent;
|
|
140
|
-
}
|
|
141
|
-
return textContent;
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
clothes_fitting: {
|
|
145
|
-
description:
|
|
146
|
-
"Send to user link to guidelines for clothes fitting https://images.app.goo.gl/LECaeXJfXa7gzYCC8 ",
|
|
147
|
-
parameters: z.object({}),
|
|
148
|
-
generate: async ({}) => {
|
|
149
|
-
const fittingGuidelinesLink =
|
|
150
|
-
"https://images.app.goo.gl/LECaeXJfXa7gzYCC8 ";
|
|
151
|
-
const formattedLink = `<a href="${fittingGuidelinesLink}" target="_blank">Guidelines for clothes fitting</a>`;
|
|
152
|
-
const linkStyle = {
|
|
153
|
-
color: "blue",
|
|
154
|
-
textDecoration: "underline",
|
|
155
|
-
};
|
|
156
|
-
history.done((messages: ServerMessage[]) => [
|
|
157
|
-
...messages,
|
|
158
|
-
{
|
|
159
|
-
role: "assistant",
|
|
160
|
-
content: formattedLink,
|
|
161
|
-
},
|
|
162
|
-
]);
|
|
163
|
-
|
|
164
|
-
return (
|
|
165
|
-
<a href={fittingGuidelinesLink} target="_blank" style={linkStyle}>
|
|
166
|
-
Guidelines for clothes fitting
|
|
167
|
-
</a>
|
|
168
|
-
);
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
escalate: {
|
|
172
|
-
description:
|
|
173
|
-
"Escalate to human agent if none of the other tools seem relevant or the interaction is repetative, or if the user is getting upset",
|
|
174
|
-
parameters: z.object({
|
|
175
|
-
identifiable_info: z
|
|
176
|
-
.string()
|
|
177
|
-
.describe(
|
|
178
|
-
"Email, full name, or order number to make the request identifiable",
|
|
179
|
-
),
|
|
180
|
-
summary: z.string().describe("Summarize user request concisely"),
|
|
181
|
-
}),
|
|
182
|
-
generate: async function* ({ identifiable_info, summary }) {
|
|
183
|
-
let textContent =
|
|
184
|
-
"I am escalating your question to the human assistant\n\n";
|
|
185
|
-
yield textContent;
|
|
186
|
-
console.log(
|
|
187
|
-
"generating answer while escalating, ",
|
|
188
|
-
identifiable_info,
|
|
189
|
-
);
|
|
190
|
-
const filePath = path.resolve(process.cwd(), "public/shop_info.txt");
|
|
191
|
-
const generalInfo = fs.readFileSync(filePath, "utf-8");
|
|
192
|
-
const result = await streamText({
|
|
193
|
-
model: openai("gpt-3.5-turbo"),
|
|
194
|
-
temperature: 0,
|
|
195
|
-
system: `Generate response to user question ${summary} based on the context ${generalInfo}. Your answer begins with: "While we wait for the human assistant,`,
|
|
196
|
-
messages: [
|
|
197
|
-
{
|
|
198
|
-
role: "user",
|
|
199
|
-
content: summary,
|
|
200
|
-
},
|
|
201
|
-
{
|
|
202
|
-
role: "assistant",
|
|
203
|
-
content: textContent,
|
|
204
|
-
},
|
|
205
|
-
],
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
for await (const textPart of result.textStream) {
|
|
209
|
-
textContent += textPart;
|
|
210
|
-
yield textContent;
|
|
211
|
-
}
|
|
212
|
-
return textContent;
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
},
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
id: nanoid(),
|
|
220
|
-
role: "assistant",
|
|
221
|
-
display: result.value,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export const AI = createAI<ServerMessage[], ClientMessage[]>({
|
|
226
|
-
actions: {
|
|
227
|
-
continueConversation,
|
|
228
|
-
},
|
|
229
|
-
initialAIState: [],
|
|
230
|
-
initialUIState: [],
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
## app/globals.css
|
|
236
|
-
|
|
237
|
-
```css
|
|
238
|
-
@import "tailwindcss";
|
|
239
|
-
@import "tw-animate-css";
|
|
240
|
-
|
|
241
|
-
@custom-variant dark (&:is(.dark *));
|
|
242
|
-
|
|
243
|
-
@theme inline {
|
|
244
|
-
--color-background: var(--background);
|
|
245
|
-
--color-foreground: var(--foreground);
|
|
246
|
-
--font-sans: var(--font-geist-sans);
|
|
247
|
-
--font-mono: var(--font-geist-mono);
|
|
248
|
-
--color-sidebar-ring: var(--sidebar-ring);
|
|
249
|
-
--color-sidebar-border: var(--sidebar-border);
|
|
250
|
-
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
251
|
-
--color-sidebar-accent: var(--sidebar-accent);
|
|
252
|
-
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
253
|
-
--color-sidebar-primary: var(--sidebar-primary);
|
|
254
|
-
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
255
|
-
--color-sidebar: var(--sidebar);
|
|
256
|
-
--color-chart-5: var(--chart-5);
|
|
257
|
-
--color-chart-4: var(--chart-4);
|
|
258
|
-
--color-chart-3: var(--chart-3);
|
|
259
|
-
--color-chart-2: var(--chart-2);
|
|
260
|
-
--color-chart-1: var(--chart-1);
|
|
261
|
-
--color-ring: var(--ring);
|
|
262
|
-
--color-input: var(--input);
|
|
263
|
-
--color-border: var(--border);
|
|
264
|
-
--color-destructive: var(--destructive);
|
|
265
|
-
--color-accent-foreground: var(--accent-foreground);
|
|
266
|
-
--color-accent: var(--accent);
|
|
267
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
268
|
-
--color-muted: var(--muted);
|
|
269
|
-
--color-secondary-foreground: var(--secondary-foreground);
|
|
270
|
-
--color-secondary: var(--secondary);
|
|
271
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
272
|
-
--color-primary: var(--primary);
|
|
273
|
-
--color-popover-foreground: var(--popover-foreground);
|
|
274
|
-
--color-popover: var(--popover);
|
|
275
|
-
--color-card-foreground: var(--card-foreground);
|
|
276
|
-
--color-card: var(--card);
|
|
277
|
-
--radius-sm: calc(var(--radius) - 4px);
|
|
278
|
-
--radius-md: calc(var(--radius) - 2px);
|
|
279
|
-
--radius-lg: var(--radius);
|
|
280
|
-
--radius-xl: calc(var(--radius) + 4px);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
:root {
|
|
284
|
-
--radius: 0.625rem;
|
|
285
|
-
--background: oklch(1 0 0);
|
|
286
|
-
--foreground: oklch(0.141 0.005 285.823);
|
|
287
|
-
--card: oklch(1 0 0);
|
|
288
|
-
--card-foreground: oklch(0.141 0.005 285.823);
|
|
289
|
-
--popover: oklch(1 0 0);
|
|
290
|
-
--popover-foreground: oklch(0.141 0.005 285.823);
|
|
291
|
-
--primary: oklch(0.21 0.006 285.885);
|
|
292
|
-
--primary-foreground: oklch(0.985 0 0);
|
|
293
|
-
--secondary: oklch(0.967 0.001 286.375);
|
|
294
|
-
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
295
|
-
--muted: oklch(0.967 0.001 286.375);
|
|
296
|
-
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
297
|
-
--accent: oklch(0.967 0.001 286.375);
|
|
298
|
-
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
299
|
-
--destructive: oklch(0.577 0.245 27.325);
|
|
300
|
-
--border: oklch(0.92 0.004 286.32);
|
|
301
|
-
--input: oklch(0.92 0.004 286.32);
|
|
302
|
-
--ring: oklch(0.705 0.015 286.067);
|
|
303
|
-
--chart-1: oklch(0.646 0.222 41.116);
|
|
304
|
-
--chart-2: oklch(0.6 0.118 184.704);
|
|
305
|
-
--chart-3: oklch(0.398 0.07 227.392);
|
|
306
|
-
--chart-4: oklch(0.828 0.189 84.429);
|
|
307
|
-
--chart-5: oklch(0.769 0.188 70.08);
|
|
308
|
-
--sidebar: oklch(0.985 0 0);
|
|
309
|
-
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
310
|
-
--sidebar-primary: oklch(0.21 0.006 285.885);
|
|
311
|
-
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
312
|
-
--sidebar-accent: oklch(0.967 0.001 286.375);
|
|
313
|
-
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
314
|
-
--sidebar-border: oklch(0.92 0.004 286.32);
|
|
315
|
-
--sidebar-ring: oklch(0.705 0.015 286.067);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
.dark {
|
|
319
|
-
--background: oklch(0.141 0.005 285.823);
|
|
320
|
-
--foreground: oklch(0.985 0 0);
|
|
321
|
-
--card: oklch(0.21 0.006 285.885);
|
|
322
|
-
--card-foreground: oklch(0.985 0 0);
|
|
323
|
-
--popover: oklch(0.21 0.006 285.885);
|
|
324
|
-
--popover-foreground: oklch(0.985 0 0);
|
|
325
|
-
--primary: oklch(0.92 0.004 286.32);
|
|
326
|
-
--primary-foreground: oklch(0.21 0.006 285.885);
|
|
327
|
-
--secondary: oklch(0.274 0.006 286.033);
|
|
328
|
-
--secondary-foreground: oklch(0.985 0 0);
|
|
329
|
-
--muted: oklch(0.274 0.006 286.033);
|
|
330
|
-
--muted-foreground: oklch(0.705 0.015 286.067);
|
|
331
|
-
--accent: oklch(0.274 0.006 286.033);
|
|
332
|
-
--accent-foreground: oklch(0.985 0 0);
|
|
333
|
-
--destructive: oklch(0.704 0.191 22.216);
|
|
334
|
-
--border: oklch(1 0 0 / 10%);
|
|
335
|
-
--input: oklch(1 0 0 / 15%);
|
|
336
|
-
--ring: oklch(0.552 0.016 285.938);
|
|
337
|
-
--chart-1: oklch(0.488 0.243 264.376);
|
|
338
|
-
--chart-2: oklch(0.696 0.17 162.48);
|
|
339
|
-
--chart-3: oklch(0.769 0.188 70.08);
|
|
340
|
-
--chart-4: oklch(0.627 0.265 303.9);
|
|
341
|
-
--chart-5: oklch(0.645 0.246 16.439);
|
|
342
|
-
--sidebar: oklch(0.21 0.006 285.885);
|
|
343
|
-
--sidebar-foreground: oklch(0.985 0 0);
|
|
344
|
-
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
345
|
-
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
346
|
-
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
347
|
-
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
348
|
-
--sidebar-border: oklch(1 0 0 / 10%);
|
|
349
|
-
--sidebar-ring: oklch(0.552 0.016 285.938);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
@layer base {
|
|
353
|
-
* {
|
|
354
|
-
@apply border-border outline-ring/50;
|
|
355
|
-
}
|
|
356
|
-
body {
|
|
357
|
-
@apply bg-background text-foreground;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
## app/layout.tsx
|
|
364
|
-
|
|
365
|
-
```tsx
|
|
366
|
-
import type { Metadata } from "next";
|
|
367
|
-
import { Inter } from "next/font/google";
|
|
368
|
-
import { AI } from "@/app/actions";
|
|
369
|
-
import { MyRuntimeProvider } from "@/app/MyRuntimeProvider";
|
|
370
|
-
import type React from "react";
|
|
371
|
-
|
|
372
|
-
import "./globals.css";
|
|
373
|
-
|
|
374
|
-
const inter = Inter({ subsets: ["latin"] });
|
|
375
|
-
|
|
376
|
-
export const metadata: Metadata = {
|
|
377
|
-
title: "Create Next App",
|
|
378
|
-
description: "Generated by create next app",
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
export default function RootLayout({
|
|
382
|
-
children,
|
|
383
|
-
}: Readonly<{
|
|
384
|
-
children: React.ReactNode;
|
|
385
|
-
}>) {
|
|
386
|
-
return (
|
|
387
|
-
<html lang="en">
|
|
388
|
-
<AI>
|
|
389
|
-
<MyRuntimeProvider>
|
|
390
|
-
<body
|
|
391
|
-
className={inter.className}
|
|
392
|
-
style={{ backgroundColor: "transparent" }}
|
|
393
|
-
>
|
|
394
|
-
{children}
|
|
395
|
-
</body>
|
|
396
|
-
</MyRuntimeProvider>
|
|
397
|
-
</AI>
|
|
398
|
-
</html>
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
## app/MyRuntimeProvider.tsx
|
|
405
|
-
|
|
406
|
-
```tsx
|
|
407
|
-
"use client";
|
|
408
|
-
|
|
409
|
-
import {
|
|
410
|
-
type AppendMessage,
|
|
411
|
-
AssistantRuntimeProvider,
|
|
412
|
-
} from "@assistant-ui/react";
|
|
413
|
-
import { useVercelRSCRuntime } from "@assistant-ui/react-ai-sdk-v4";
|
|
414
|
-
import { useActions, useUIState } from "ai/rsc";
|
|
415
|
-
import { nanoid } from "nanoid";
|
|
416
|
-
|
|
417
|
-
import type { AI } from "@/app/actions";
|
|
418
|
-
|
|
419
|
-
export function MyRuntimeProvider({
|
|
420
|
-
children,
|
|
421
|
-
}: Readonly<{
|
|
422
|
-
children: React.ReactNode;
|
|
423
|
-
}>) {
|
|
424
|
-
const { continueConversation } = useActions();
|
|
425
|
-
const [messages, setMessages] = useUIState<typeof AI>();
|
|
426
|
-
|
|
427
|
-
const onNew = async (m: AppendMessage) => {
|
|
428
|
-
if (m.content[0]?.type !== "text")
|
|
429
|
-
throw new Error("Only text messages are supported");
|
|
430
|
-
|
|
431
|
-
const input = m.content[0].text;
|
|
432
|
-
setMessages((currentConversation) => [
|
|
433
|
-
...currentConversation,
|
|
434
|
-
{ id: nanoid(), role: "user", display: input },
|
|
435
|
-
]);
|
|
436
|
-
|
|
437
|
-
const message = await continueConversation(input);
|
|
438
|
-
|
|
439
|
-
setMessages((currentConversation) => [...currentConversation, message]);
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
const runtime = useVercelRSCRuntime({ messages, onNew });
|
|
443
|
-
|
|
444
|
-
return (
|
|
445
|
-
<AssistantRuntimeProvider runtime={runtime}>
|
|
446
|
-
{children}
|
|
447
|
-
</AssistantRuntimeProvider>
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
## app/page.tsx
|
|
454
|
-
|
|
455
|
-
```tsx
|
|
456
|
-
"use client";
|
|
457
|
-
|
|
458
|
-
import { Suspense } from "react";
|
|
459
|
-
import { AssistantModal } from "@/components/ui/assistant-ui/assistant-modal";
|
|
460
|
-
|
|
461
|
-
function Home() {
|
|
462
|
-
return (
|
|
463
|
-
<Suspense fallback={<div>Loading...</div>}>
|
|
464
|
-
<div className="fixed bottom-4 right-4 size-12 rounded-full shadow">
|
|
465
|
-
<AssistantModal />
|
|
466
|
-
</div>
|
|
467
|
-
</Suspense>
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
export default function Page() {
|
|
472
|
-
return (
|
|
473
|
-
<Suspense fallback={<div>Loading...</div>}>
|
|
474
|
-
<Home />
|
|
475
|
-
</Suspense>
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
## components.json
|
|
482
|
-
|
|
483
|
-
```json
|
|
484
|
-
{
|
|
485
|
-
"$schema": "https://ui.shadcn.com/schema.json",
|
|
486
|
-
"style": "default",
|
|
487
|
-
"rsc": true,
|
|
488
|
-
"tsx": true,
|
|
489
|
-
"tailwind": {
|
|
490
|
-
"config": "",
|
|
491
|
-
"css": "src/app/globals.css",
|
|
492
|
-
"baseColor": "slate",
|
|
493
|
-
"cssVariables": true,
|
|
494
|
-
"prefix": ""
|
|
495
|
-
},
|
|
496
|
-
"aliases": {
|
|
497
|
-
"components": "@/components",
|
|
498
|
-
"utils": "@/lib/utils"
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
## components/ui/assistant-ui/assistant-modal.tsx
|
|
505
|
-
|
|
506
|
-
```tsx
|
|
507
|
-
import { BotIcon, ChevronDownIcon } from "lucide-react";
|
|
508
|
-
import { Thread } from "@/components/ui/assistant-ui/thread";
|
|
509
|
-
import { Button } from "@/components/ui/button";
|
|
510
|
-
import {
|
|
511
|
-
Popover,
|
|
512
|
-
PopoverContent,
|
|
513
|
-
PopoverTrigger,
|
|
514
|
-
} from "@/components/ui/popover";
|
|
515
|
-
import {
|
|
516
|
-
Tooltip,
|
|
517
|
-
TooltipContent,
|
|
518
|
-
TooltipTrigger,
|
|
519
|
-
} from "@/components/ui/tooltip";
|
|
520
|
-
import { cn } from "@/lib/utils";
|
|
521
|
-
import { useEffect, forwardRef, useState } from "react";
|
|
522
|
-
|
|
523
|
-
export const AssistantModal = () => {
|
|
524
|
-
const [open, setOpen] = useState(false);
|
|
525
|
-
|
|
526
|
-
useEffect(() => {
|
|
527
|
-
const height = open ? 770 : 70; // Set the height based on the modal state
|
|
528
|
-
const width = open ? 720 : 70; // Set the width based on the modal state
|
|
529
|
-
window.parent.postMessage(
|
|
530
|
-
{
|
|
531
|
-
type: "resize",
|
|
532
|
-
height: height,
|
|
533
|
-
width: width,
|
|
534
|
-
},
|
|
535
|
-
"*",
|
|
536
|
-
);
|
|
537
|
-
}, [open]);
|
|
538
|
-
|
|
539
|
-
return (
|
|
540
|
-
<Popover open={open} onOpenChange={setOpen}>
|
|
541
|
-
<PopoverTrigger asChild>
|
|
542
|
-
<FloatingAssistantButton />
|
|
543
|
-
</PopoverTrigger>
|
|
544
|
-
<PopoverContent
|
|
545
|
-
side="top"
|
|
546
|
-
align="end"
|
|
547
|
-
className="fixed bottom-0 right-0 z-50 h-[700px] w-[700px] overflow-y-auto rounded-2xl p-0"
|
|
548
|
-
>
|
|
549
|
-
<Thread />
|
|
550
|
-
</PopoverContent>
|
|
551
|
-
</Popover>
|
|
552
|
-
);
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
type FloatingAssistantButton = { "data-state"?: "open" | "closed" };
|
|
556
|
-
|
|
557
|
-
const FloatingAssistantButton = forwardRef<
|
|
558
|
-
HTMLButtonElement,
|
|
559
|
-
FloatingAssistantButton
|
|
560
|
-
>(({ "data-state": state, ...rest }, forwardedRef) => {
|
|
561
|
-
const tooltip = state === "open" ? "Close Assistant" : "Open Assistant";
|
|
562
|
-
return (
|
|
563
|
-
<Tooltip>
|
|
564
|
-
<TooltipTrigger asChild>
|
|
565
|
-
<Button
|
|
566
|
-
variant="default"
|
|
567
|
-
size="icon"
|
|
568
|
-
className="fixed bottom-4 right-4 size-12 rounded-full shadow transition-transform hover:scale-110 active:scale-90"
|
|
569
|
-
{...rest}
|
|
570
|
-
ref={forwardedRef}
|
|
571
|
-
style={{ zIndex: 1000 }}
|
|
572
|
-
>
|
|
573
|
-
<BotIcon
|
|
574
|
-
className={cn(
|
|
575
|
-
"absolute size-6 transition-all",
|
|
576
|
-
state === "open" && "rotate-90 scale-0",
|
|
577
|
-
state === "closed" && "rotate-0 scale-100",
|
|
578
|
-
)}
|
|
579
|
-
/>
|
|
580
|
-
|
|
581
|
-
<ChevronDownIcon
|
|
582
|
-
className={cn(
|
|
583
|
-
"absolute size-6 transition-all",
|
|
584
|
-
state === "open" && "rotate-0 scale-100",
|
|
585
|
-
state === "closed" && "-rotate-90 scale-0",
|
|
586
|
-
)}
|
|
587
|
-
/>
|
|
588
|
-
<span className="sr-only">{tooltip}</span>
|
|
589
|
-
</Button>
|
|
590
|
-
</TooltipTrigger>
|
|
591
|
-
<TooltipContent side="left">{tooltip}</TooltipContent>
|
|
592
|
-
</Tooltip>
|
|
593
|
-
);
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
FloatingAssistantButton.displayName = "FloatingAssistantButton";
|
|
597
|
-
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
## components/ui/assistant-ui/thread.tsx
|
|
601
|
-
|
|
602
|
-
```tsx
|
|
603
|
-
"use client";
|
|
604
|
-
|
|
605
|
-
import {
|
|
606
|
-
ComposerPrimitive,
|
|
607
|
-
MessagePrimitive,
|
|
608
|
-
ThreadPrimitive,
|
|
609
|
-
} from "@assistant-ui/react";
|
|
610
|
-
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
|
611
|
-
import type { ComponentPropsWithRef, FC, PropsWithChildren } from "react";
|
|
612
|
-
import { Button } from "@/components/ui/button";
|
|
613
|
-
import {
|
|
614
|
-
Tooltip,
|
|
615
|
-
TooltipContent,
|
|
616
|
-
TooltipTrigger,
|
|
617
|
-
} from "@/components/ui/tooltip";
|
|
618
|
-
import { cn } from "@/lib/utils";
|
|
619
|
-
import { ArrowDownIcon, SendHorizontalIcon } from "lucide-react";
|
|
620
|
-
import Image from "next/image";
|
|
621
|
-
import { RSCDisplay } from "@assistant-ui/react-ai-sdk-v4";
|
|
622
|
-
|
|
623
|
-
export const Thread: FC = () => {
|
|
624
|
-
return (
|
|
625
|
-
<ThreadPrimitive.Root className="flex h-full flex-col items-center pb-3">
|
|
626
|
-
<ThreadPrimitive.Viewport className="flex w-full flex-grow flex-col items-center overflow-y-scroll scroll-smooth px-4 pt-12">
|
|
627
|
-
<ThreadPrimitive.Empty>
|
|
628
|
-
<ThreadEmpty />
|
|
629
|
-
</ThreadPrimitive.Empty>
|
|
630
|
-
|
|
631
|
-
<ThreadPrimitive.Messages
|
|
632
|
-
components={{
|
|
633
|
-
UserMessage,
|
|
634
|
-
AssistantMessage,
|
|
635
|
-
}}
|
|
636
|
-
/>
|
|
637
|
-
<ThreadScrollToBottom />
|
|
638
|
-
</ThreadPrimitive.Viewport>
|
|
639
|
-
|
|
640
|
-
<Composer />
|
|
641
|
-
</ThreadPrimitive.Root>
|
|
642
|
-
);
|
|
643
|
-
};
|
|
644
|
-
|
|
645
|
-
const ThreadEmpty: FC = () => {
|
|
646
|
-
return (
|
|
647
|
-
<div className="flex w-full max-w-2xl grow flex-col justify-end px-4 py-6">
|
|
648
|
-
{" "}
|
|
649
|
-
{/* Stick to bottom */}
|
|
650
|
-
<div className="mb-1 flex flex-grow flex-col items-center justify-center">
|
|
651
|
-
{" "}
|
|
652
|
-
{/* Reduced margin-bottom */}
|
|
653
|
-
<Image
|
|
654
|
-
src="/image.png"
|
|
655
|
-
alt="Your Logo"
|
|
656
|
-
className="mb-4 w-1/2 max-w-xs"
|
|
657
|
-
width={320}
|
|
658
|
-
height={164}
|
|
659
|
-
/>{" "}
|
|
660
|
-
{/* Smaller image */}
|
|
661
|
-
<div className="flex items-center">
|
|
662
|
-
<Avatar className="mr-4" style={{ width: "20px", height: "20px" }}>
|
|
663
|
-
{" "}
|
|
664
|
-
{/* Adjusted size */}
|
|
665
|
-
<AvatarImage src="/favicon.ico" alt="AI" />
|
|
666
|
-
<AvatarFallback>AI</AvatarFallback>
|
|
667
|
-
</Avatar>
|
|
668
|
-
<p className="mt-4">
|
|
669
|
-
Hi, do you know what product you are looking for, or you have a
|
|
670
|
-
general question?
|
|
671
|
-
</p>
|
|
672
|
-
</div>
|
|
673
|
-
</div>
|
|
674
|
-
<div className="flex flex-col gap-4 self-stretch sm:flex-row">
|
|
675
|
-
<ThreadSuggestion prompt="I need help with product search">
|
|
676
|
-
<p className="mb-2 font-semibold">Product search</p>
|
|
677
|
-
</ThreadSuggestion>
|
|
678
|
-
<ThreadSuggestion prompt="I need to talk to human agent support">
|
|
679
|
-
<p className="mb-2 font-semibold">Human agent</p>
|
|
680
|
-
</ThreadSuggestion>
|
|
681
|
-
</div>
|
|
682
|
-
</div>
|
|
683
|
-
);
|
|
684
|
-
};
|
|
685
|
-
|
|
686
|
-
const ThreadSuggestion: FC<PropsWithChildren<{ prompt: string }>> = ({
|
|
687
|
-
prompt,
|
|
688
|
-
children,
|
|
689
|
-
}) => {
|
|
690
|
-
return (
|
|
691
|
-
<ThreadPrimitive.Suggestion
|
|
692
|
-
prompt={prompt}
|
|
693
|
-
method="replace"
|
|
694
|
-
autoSend
|
|
695
|
-
asChild
|
|
696
|
-
>
|
|
697
|
-
<Button
|
|
698
|
-
variant="outline"
|
|
699
|
-
className="text-md flex h-full items-center justify-center sm:basis-full"
|
|
700
|
-
>
|
|
701
|
-
{children}
|
|
702
|
-
</Button>
|
|
703
|
-
</ThreadPrimitive.Suggestion>
|
|
704
|
-
);
|
|
705
|
-
};
|
|
706
|
-
|
|
707
|
-
const ThreadScrollToBottom: FC = () => {
|
|
708
|
-
return (
|
|
709
|
-
<div className="sticky bottom-0">
|
|
710
|
-
<ThreadPrimitive.ScrollToBottom asChild>
|
|
711
|
-
<IconButton
|
|
712
|
-
tooltip="Scroll to bottom"
|
|
713
|
-
variant="outline"
|
|
714
|
-
className="absolute -top-10 rounded-full disabled:invisible"
|
|
715
|
-
>
|
|
716
|
-
<ArrowDownIcon className="size-4" />
|
|
717
|
-
</IconButton>
|
|
718
|
-
</ThreadPrimitive.ScrollToBottom>
|
|
719
|
-
</div>
|
|
720
|
-
);
|
|
721
|
-
};
|
|
722
|
-
|
|
723
|
-
const Composer: FC = () => {
|
|
724
|
-
return (
|
|
725
|
-
<ComposerPrimitive.Root className="flex w-[calc(100%-32px)] max-w-[42rem] items-end rounded-lg border p-0.5 transition-shadow focus-within:shadow-sm">
|
|
726
|
-
<ComposerPrimitive.Input
|
|
727
|
-
placeholder="Write a message..."
|
|
728
|
-
className="placeholder:text-foreground/50 h-12 max-h-40 flex-grow resize-none bg-transparent p-3.5 text-sm outline-none"
|
|
729
|
-
/>
|
|
730
|
-
<ComposerPrimitive.Send className="bg-foreground m-2 flex h-8 w-8 items-center justify-center rounded-md text-2xl font-bold shadow transition-opacity disabled:opacity-10">
|
|
731
|
-
<SendHorizontalIcon className="text-background size-4" />
|
|
732
|
-
</ComposerPrimitive.Send>
|
|
733
|
-
</ComposerPrimitive.Root>
|
|
734
|
-
);
|
|
735
|
-
};
|
|
736
|
-
|
|
737
|
-
const UserMessage: FC = () => {
|
|
738
|
-
return (
|
|
739
|
-
<MessagePrimitive.Root className="relative mb-6 flex w-full max-w-2xl flex-col items-end gap-2 pl-24">
|
|
740
|
-
<div className="relative mr-1 flex items-start gap-3">
|
|
741
|
-
<p className="bg-foreground/5 text-foreground max-w-xl whitespace-pre-line break-words rounded-3xl px-5 py-2.5">
|
|
742
|
-
<MessagePrimitive.Parts components={{ Text: RSCDisplay }} />
|
|
743
|
-
</p>
|
|
744
|
-
</div>
|
|
745
|
-
</MessagePrimitive.Root>
|
|
746
|
-
);
|
|
747
|
-
};
|
|
748
|
-
|
|
749
|
-
const AssistantMessage: FC = () => {
|
|
750
|
-
return (
|
|
751
|
-
<MessagePrimitive.Root className="relative mb-6 flex w-full max-w-2xl gap-3">
|
|
752
|
-
<Avatar>
|
|
753
|
-
<AvatarFallback>A</AvatarFallback>
|
|
754
|
-
</Avatar>
|
|
755
|
-
|
|
756
|
-
<div className="mt-2 flex-grow">
|
|
757
|
-
<p className="text-foreground max-w-xl whitespace-pre-line break-words">
|
|
758
|
-
<MessagePrimitive.Parts components={{ Text: RSCDisplay }} />
|
|
759
|
-
</p>
|
|
760
|
-
</div>
|
|
761
|
-
</MessagePrimitive.Root>
|
|
762
|
-
);
|
|
763
|
-
};
|
|
764
|
-
|
|
765
|
-
type IconButton = ComponentPropsWithRef<typeof Button> & { tooltip: string };
|
|
766
|
-
|
|
767
|
-
const IconButton: FC<IconButton> = ({
|
|
768
|
-
children,
|
|
769
|
-
tooltip,
|
|
770
|
-
className,
|
|
771
|
-
...rest
|
|
772
|
-
}) => {
|
|
773
|
-
return (
|
|
774
|
-
<Tooltip>
|
|
775
|
-
<TooltipTrigger asChild>
|
|
776
|
-
<Button
|
|
777
|
-
variant="ghost"
|
|
778
|
-
size="icon"
|
|
779
|
-
className={cn("size-auto p-1", className)}
|
|
780
|
-
{...rest}
|
|
781
|
-
>
|
|
782
|
-
{children}
|
|
783
|
-
<span className="sr-only">{tooltip}</span>
|
|
784
|
-
</Button>
|
|
785
|
-
</TooltipTrigger>
|
|
786
|
-
<TooltipContent side="bottom">{tooltip}</TooltipContent>
|
|
787
|
-
</Tooltip>
|
|
788
|
-
);
|
|
789
|
-
};
|
|
790
|
-
|
|
791
|
-
```
|
|
792
|
-
|
|
793
|
-
## components/ui/avatar.tsx
|
|
794
|
-
|
|
795
|
-
```tsx
|
|
796
|
-
"use client";
|
|
797
|
-
|
|
798
|
-
import * as React from "react";
|
|
799
|
-
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
|
800
|
-
|
|
801
|
-
import { cn } from "@/lib/utils";
|
|
802
|
-
|
|
803
|
-
const Avatar = React.forwardRef<
|
|
804
|
-
React.ComponentRef<typeof AvatarPrimitive.Root>,
|
|
805
|
-
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
|
806
|
-
>(({ className, ...props }, ref) => (
|
|
807
|
-
<AvatarPrimitive.Root
|
|
808
|
-
ref={ref}
|
|
809
|
-
className={cn(
|
|
810
|
-
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
|
811
|
-
className,
|
|
812
|
-
)}
|
|
813
|
-
{...props}
|
|
814
|
-
/>
|
|
815
|
-
));
|
|
816
|
-
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
|
817
|
-
|
|
818
|
-
const AvatarImage = React.forwardRef<
|
|
819
|
-
React.ComponentRef<typeof AvatarPrimitive.Image>,
|
|
820
|
-
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
821
|
-
>(({ className, ...props }, ref) => (
|
|
822
|
-
<AvatarPrimitive.Image
|
|
823
|
-
ref={ref}
|
|
824
|
-
className={cn("aspect-square h-full w-full", className)}
|
|
825
|
-
src="/favicon.ico"
|
|
826
|
-
alt="AI"
|
|
827
|
-
{...props}
|
|
828
|
-
/>
|
|
829
|
-
));
|
|
830
|
-
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
|
831
|
-
|
|
832
|
-
const AvatarFallback = React.forwardRef<
|
|
833
|
-
React.ComponentRef<typeof AvatarPrimitive.Fallback>,
|
|
834
|
-
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
835
|
-
>(({ className, ...props }, ref) => (
|
|
836
|
-
<AvatarPrimitive.Fallback
|
|
837
|
-
ref={ref}
|
|
838
|
-
className={cn(
|
|
839
|
-
"bg-muted flex h-full w-full items-center justify-center rounded-full",
|
|
840
|
-
className,
|
|
841
|
-
)}
|
|
842
|
-
{...props}
|
|
843
|
-
/>
|
|
844
|
-
));
|
|
845
|
-
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
|
846
|
-
|
|
847
|
-
export { Avatar, AvatarImage, AvatarFallback };
|
|
848
|
-
|
|
849
|
-
```
|
|
850
|
-
|
|
851
|
-
## components/ui/button.tsx
|
|
852
|
-
|
|
853
|
-
```tsx
|
|
854
|
-
import * as React from "react";
|
|
855
|
-
import { Slot } from "@radix-ui/react-slot";
|
|
856
|
-
import { cva, type VariantProps } from "class-variance-authority";
|
|
857
|
-
|
|
858
|
-
import { cn } from "@/lib/utils";
|
|
859
|
-
|
|
860
|
-
const buttonVariants = cva(
|
|
861
|
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
862
|
-
{
|
|
863
|
-
variants: {
|
|
864
|
-
variant: {
|
|
865
|
-
default:
|
|
866
|
-
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
867
|
-
destructive:
|
|
868
|
-
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
869
|
-
outline:
|
|
870
|
-
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
871
|
-
secondary:
|
|
872
|
-
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
873
|
-
ghost:
|
|
874
|
-
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
875
|
-
link: "text-primary underline-offset-4 hover:underline",
|
|
876
|
-
},
|
|
877
|
-
size: {
|
|
878
|
-
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
879
|
-
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
880
|
-
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
881
|
-
icon: "size-9",
|
|
882
|
-
},
|
|
883
|
-
},
|
|
884
|
-
defaultVariants: {
|
|
885
|
-
variant: "default",
|
|
886
|
-
size: "default",
|
|
887
|
-
},
|
|
888
|
-
},
|
|
889
|
-
);
|
|
890
|
-
|
|
891
|
-
function Button({
|
|
892
|
-
className,
|
|
893
|
-
variant,
|
|
894
|
-
size,
|
|
895
|
-
asChild = false,
|
|
896
|
-
...props
|
|
897
|
-
}: React.ComponentProps<"button"> &
|
|
898
|
-
VariantProps<typeof buttonVariants> & {
|
|
899
|
-
asChild?: boolean;
|
|
900
|
-
}) {
|
|
901
|
-
const Comp = asChild ? Slot : "button";
|
|
902
|
-
|
|
903
|
-
return (
|
|
904
|
-
<Comp
|
|
905
|
-
data-slot="button"
|
|
906
|
-
className={cn(buttonVariants({ variant, size, className }))}
|
|
907
|
-
{...props}
|
|
908
|
-
/>
|
|
909
|
-
);
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
export { Button, buttonVariants };
|
|
913
|
-
|
|
914
|
-
```
|
|
915
|
-
|
|
916
|
-
## components/ui/card.tsx
|
|
917
|
-
|
|
918
|
-
```tsx
|
|
919
|
-
import * as React from "react";
|
|
920
|
-
|
|
921
|
-
import { cn } from "@/lib/utils";
|
|
922
|
-
|
|
923
|
-
const Card = React.forwardRef<
|
|
924
|
-
HTMLDivElement,
|
|
925
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
926
|
-
>(({ className, ...props }, ref) => (
|
|
927
|
-
<div
|
|
928
|
-
ref={ref}
|
|
929
|
-
className={cn(
|
|
930
|
-
"bg-card text-card-foreground rounded-lg border shadow-sm",
|
|
931
|
-
className,
|
|
932
|
-
)}
|
|
933
|
-
{...props}
|
|
934
|
-
/>
|
|
935
|
-
));
|
|
936
|
-
Card.displayName = "Card";
|
|
937
|
-
|
|
938
|
-
const CardHeader = React.forwardRef<
|
|
939
|
-
HTMLDivElement,
|
|
940
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
941
|
-
>(({ className, ...props }, ref) => (
|
|
942
|
-
<div
|
|
943
|
-
ref={ref}
|
|
944
|
-
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
945
|
-
{...props}
|
|
946
|
-
/>
|
|
947
|
-
));
|
|
948
|
-
CardHeader.displayName = "CardHeader";
|
|
949
|
-
|
|
950
|
-
const CardTitle = React.forwardRef<
|
|
951
|
-
HTMLParagraphElement,
|
|
952
|
-
React.HTMLAttributes<HTMLHeadingElement>
|
|
953
|
-
>(({ className, ...props }, ref) => (
|
|
954
|
-
<h3
|
|
955
|
-
ref={ref}
|
|
956
|
-
className={cn(
|
|
957
|
-
"text-2xl font-semibold leading-none tracking-tight",
|
|
958
|
-
className,
|
|
959
|
-
)}
|
|
960
|
-
{...props}
|
|
961
|
-
/>
|
|
962
|
-
));
|
|
963
|
-
CardTitle.displayName = "CardTitle";
|
|
964
|
-
|
|
965
|
-
const CardDescription = React.forwardRef<
|
|
966
|
-
HTMLParagraphElement,
|
|
967
|
-
React.HTMLAttributes<HTMLParagraphElement>
|
|
968
|
-
>(({ className, ...props }, ref) => (
|
|
969
|
-
<p
|
|
970
|
-
ref={ref}
|
|
971
|
-
className={cn("text-muted-foreground text-sm", className)}
|
|
972
|
-
{...props}
|
|
973
|
-
/>
|
|
974
|
-
));
|
|
975
|
-
CardDescription.displayName = "CardDescription";
|
|
976
|
-
|
|
977
|
-
const CardContent = React.forwardRef<
|
|
978
|
-
HTMLDivElement,
|
|
979
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
980
|
-
>(({ className, ...props }, ref) => (
|
|
981
|
-
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
982
|
-
));
|
|
983
|
-
CardContent.displayName = "CardContent";
|
|
984
|
-
|
|
985
|
-
const CardFooter = React.forwardRef<
|
|
986
|
-
HTMLDivElement,
|
|
987
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
988
|
-
>(({ className, ...props }, ref) => (
|
|
989
|
-
<div
|
|
990
|
-
ref={ref}
|
|
991
|
-
className={cn("flex items-center p-6 pt-0", className)}
|
|
992
|
-
{...props}
|
|
993
|
-
/>
|
|
994
|
-
));
|
|
995
|
-
CardFooter.displayName = "CardFooter";
|
|
996
|
-
|
|
997
|
-
export {
|
|
998
|
-
Card,
|
|
999
|
-
CardHeader,
|
|
1000
|
-
CardFooter,
|
|
1001
|
-
CardTitle,
|
|
1002
|
-
CardDescription,
|
|
1003
|
-
CardContent,
|
|
1004
|
-
};
|
|
1005
|
-
|
|
1006
|
-
```
|
|
1007
|
-
|
|
1008
|
-
## components/ui/carousel.tsx
|
|
1009
|
-
|
|
1010
|
-
```tsx
|
|
1011
|
-
"use client";
|
|
1012
|
-
|
|
1013
|
-
import * as React from "react";
|
|
1014
|
-
import useEmblaCarousel, {
|
|
1015
|
-
type UseEmblaCarouselType,
|
|
1016
|
-
} from "embla-carousel-react";
|
|
1017
|
-
import { ArrowLeft, ArrowRight } from "lucide-react";
|
|
1018
|
-
|
|
1019
|
-
import { cn } from "@/lib/utils";
|
|
1020
|
-
import { Button } from "@/components/ui/button";
|
|
1021
|
-
|
|
1022
|
-
type CarouselApi = UseEmblaCarouselType[1];
|
|
1023
|
-
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
|
1024
|
-
type CarouselOptions = UseCarouselParameters[0];
|
|
1025
|
-
type CarouselPlugin = UseCarouselParameters[1];
|
|
1026
|
-
|
|
1027
|
-
type CarouselProps = {
|
|
1028
|
-
opts?: CarouselOptions;
|
|
1029
|
-
plugins?: CarouselPlugin;
|
|
1030
|
-
orientation?: "horizontal" | "vertical";
|
|
1031
|
-
setApi?: (api: CarouselApi) => void;
|
|
1032
|
-
};
|
|
1033
|
-
|
|
1034
|
-
type CarouselContextProps = {
|
|
1035
|
-
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
|
1036
|
-
api: ReturnType<typeof useEmblaCarousel>[1];
|
|
1037
|
-
scrollPrev: () => void;
|
|
1038
|
-
scrollNext: () => void;
|
|
1039
|
-
canScrollPrev: boolean;
|
|
1040
|
-
canScrollNext: boolean;
|
|
1041
|
-
} & CarouselProps;
|
|
1042
|
-
|
|
1043
|
-
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
|
1044
|
-
|
|
1045
|
-
function useCarousel() {
|
|
1046
|
-
const context = React.useContext(CarouselContext);
|
|
1047
|
-
|
|
1048
|
-
if (!context) {
|
|
1049
|
-
throw new Error("useCarousel must be used within a <Carousel />");
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
return context;
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
const Carousel = React.forwardRef<
|
|
1056
|
-
HTMLDivElement,
|
|
1057
|
-
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
|
1058
|
-
>(
|
|
1059
|
-
(
|
|
1060
|
-
{
|
|
1061
|
-
orientation = "horizontal",
|
|
1062
|
-
opts,
|
|
1063
|
-
setApi,
|
|
1064
|
-
plugins,
|
|
1065
|
-
className,
|
|
1066
|
-
children,
|
|
1067
|
-
...props
|
|
1068
|
-
},
|
|
1069
|
-
ref,
|
|
1070
|
-
) => {
|
|
1071
|
-
const [carouselRef, api] = useEmblaCarousel(
|
|
1072
|
-
{
|
|
1073
|
-
...opts,
|
|
1074
|
-
axis: orientation === "horizontal" ? "x" : "y",
|
|
1075
|
-
},
|
|
1076
|
-
plugins,
|
|
1077
|
-
);
|
|
1078
|
-
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
|
1079
|
-
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
|
1080
|
-
|
|
1081
|
-
const onSelect = React.useCallback((api: CarouselApi) => {
|
|
1082
|
-
if (!api) {
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
setCanScrollPrev(api.canScrollPrev());
|
|
1087
|
-
setCanScrollNext(api.canScrollNext());
|
|
1088
|
-
}, []);
|
|
1089
|
-
|
|
1090
|
-
const scrollPrev = React.useCallback(() => {
|
|
1091
|
-
api?.scrollPrev();
|
|
1092
|
-
}, [api]);
|
|
1093
|
-
|
|
1094
|
-
const scrollNext = React.useCallback(() => {
|
|
1095
|
-
api?.scrollNext();
|
|
1096
|
-
}, [api]);
|
|
1097
|
-
|
|
1098
|
-
const handleKeyDown = React.useCallback(
|
|
1099
|
-
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
1100
|
-
if (event.key === "ArrowLeft") {
|
|
1101
|
-
event.preventDefault();
|
|
1102
|
-
scrollPrev();
|
|
1103
|
-
} else if (event.key === "ArrowRight") {
|
|
1104
|
-
event.preventDefault();
|
|
1105
|
-
scrollNext();
|
|
1106
|
-
}
|
|
1107
|
-
},
|
|
1108
|
-
[scrollPrev, scrollNext],
|
|
1109
|
-
);
|
|
1110
|
-
|
|
1111
|
-
React.useEffect(() => {
|
|
1112
|
-
if (!api || !setApi) {
|
|
1113
|
-
return;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
setApi(api);
|
|
1117
|
-
}, [api, setApi]);
|
|
1118
|
-
|
|
1119
|
-
React.useEffect(() => {
|
|
1120
|
-
if (!api) {
|
|
1121
|
-
return;
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
onSelect(api);
|
|
1125
|
-
api.on("reInit", onSelect);
|
|
1126
|
-
api.on("select", onSelect);
|
|
1127
|
-
|
|
1128
|
-
return () => {
|
|
1129
|
-
api?.off("select", onSelect);
|
|
1130
|
-
};
|
|
1131
|
-
}, [api, onSelect]);
|
|
1132
|
-
|
|
1133
|
-
return (
|
|
1134
|
-
<CarouselContext.Provider
|
|
1135
|
-
value={{
|
|
1136
|
-
carouselRef,
|
|
1137
|
-
api: api,
|
|
1138
|
-
opts,
|
|
1139
|
-
orientation:
|
|
1140
|
-
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
|
1141
|
-
scrollPrev,
|
|
1142
|
-
scrollNext,
|
|
1143
|
-
canScrollPrev,
|
|
1144
|
-
canScrollNext,
|
|
1145
|
-
}}
|
|
1146
|
-
>
|
|
1147
|
-
<div
|
|
1148
|
-
ref={ref}
|
|
1149
|
-
onKeyDownCapture={handleKeyDown}
|
|
1150
|
-
className={cn("relative", className)}
|
|
1151
|
-
role="region"
|
|
1152
|
-
aria-roledescription="carousel"
|
|
1153
|
-
{...props}
|
|
1154
|
-
>
|
|
1155
|
-
{children}
|
|
1156
|
-
</div>
|
|
1157
|
-
</CarouselContext.Provider>
|
|
1158
|
-
);
|
|
1159
|
-
},
|
|
1160
|
-
);
|
|
1161
|
-
Carousel.displayName = "Carousel";
|
|
1162
|
-
|
|
1163
|
-
const CarouselContent = React.forwardRef<
|
|
1164
|
-
HTMLDivElement,
|
|
1165
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
1166
|
-
>(({ className, ...props }, ref) => {
|
|
1167
|
-
const { carouselRef, orientation } = useCarousel();
|
|
1168
|
-
|
|
1169
|
-
return (
|
|
1170
|
-
<div ref={carouselRef} className="overflow-hidden">
|
|
1171
|
-
<div
|
|
1172
|
-
ref={ref}
|
|
1173
|
-
className={cn(
|
|
1174
|
-
"flex",
|
|
1175
|
-
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
|
1176
|
-
className,
|
|
1177
|
-
)}
|
|
1178
|
-
{...props}
|
|
1179
|
-
/>
|
|
1180
|
-
</div>
|
|
1181
|
-
);
|
|
1182
|
-
});
|
|
1183
|
-
CarouselContent.displayName = "CarouselContent";
|
|
1184
|
-
|
|
1185
|
-
const CarouselItem = React.forwardRef<
|
|
1186
|
-
HTMLDivElement,
|
|
1187
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
1188
|
-
>(({ className, ...props }, ref) => {
|
|
1189
|
-
const { orientation } = useCarousel();
|
|
1190
|
-
|
|
1191
|
-
return (
|
|
1192
|
-
<div
|
|
1193
|
-
ref={ref}
|
|
1194
|
-
role="group"
|
|
1195
|
-
aria-roledescription="slide"
|
|
1196
|
-
className={cn(
|
|
1197
|
-
"min-w-0 shrink-0 grow-0 basis-full",
|
|
1198
|
-
orientation === "horizontal" ? "pl-4" : "pt-4",
|
|
1199
|
-
className,
|
|
1200
|
-
)}
|
|
1201
|
-
{...props}
|
|
1202
|
-
/>
|
|
1203
|
-
);
|
|
1204
|
-
});
|
|
1205
|
-
CarouselItem.displayName = "CarouselItem";
|
|
1206
|
-
|
|
1207
|
-
const CarouselPrevious = React.forwardRef<
|
|
1208
|
-
HTMLButtonElement,
|
|
1209
|
-
React.ComponentProps<typeof Button>
|
|
1210
|
-
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
|
1211
|
-
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
|
1212
|
-
|
|
1213
|
-
return (
|
|
1214
|
-
<Button
|
|
1215
|
-
ref={ref}
|
|
1216
|
-
variant={variant}
|
|
1217
|
-
size={size}
|
|
1218
|
-
className={cn(
|
|
1219
|
-
"absolute h-8 w-8 rounded-full",
|
|
1220
|
-
orientation === "horizontal"
|
|
1221
|
-
? "-left-12 top-1/2 -translate-y-1/2"
|
|
1222
|
-
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
1223
|
-
className,
|
|
1224
|
-
)}
|
|
1225
|
-
disabled={!canScrollPrev}
|
|
1226
|
-
onClick={scrollPrev}
|
|
1227
|
-
{...props}
|
|
1228
|
-
>
|
|
1229
|
-
<ArrowLeft className="h-4 w-4" />
|
|
1230
|
-
<span className="sr-only">Previous slide</span>
|
|
1231
|
-
</Button>
|
|
1232
|
-
);
|
|
1233
|
-
});
|
|
1234
|
-
CarouselPrevious.displayName = "CarouselPrevious";
|
|
1235
|
-
|
|
1236
|
-
const CarouselNext = React.forwardRef<
|
|
1237
|
-
HTMLButtonElement,
|
|
1238
|
-
React.ComponentProps<typeof Button>
|
|
1239
|
-
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
|
1240
|
-
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
|
1241
|
-
|
|
1242
|
-
return (
|
|
1243
|
-
<Button
|
|
1244
|
-
ref={ref}
|
|
1245
|
-
variant={variant}
|
|
1246
|
-
size={size}
|
|
1247
|
-
className={cn(
|
|
1248
|
-
"absolute h-8 w-8 rounded-full",
|
|
1249
|
-
orientation === "horizontal"
|
|
1250
|
-
? "-right-12 top-1/2 -translate-y-1/2"
|
|
1251
|
-
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
1252
|
-
className,
|
|
1253
|
-
)}
|
|
1254
|
-
disabled={!canScrollNext}
|
|
1255
|
-
onClick={scrollNext}
|
|
1256
|
-
{...props}
|
|
1257
|
-
>
|
|
1258
|
-
<ArrowRight className="h-4 w-4" />
|
|
1259
|
-
<span className="sr-only">Next slide</span>
|
|
1260
|
-
</Button>
|
|
1261
|
-
);
|
|
1262
|
-
});
|
|
1263
|
-
CarouselNext.displayName = "CarouselNext";
|
|
1264
|
-
|
|
1265
|
-
export {
|
|
1266
|
-
type CarouselApi,
|
|
1267
|
-
Carousel,
|
|
1268
|
-
CarouselContent,
|
|
1269
|
-
CarouselItem,
|
|
1270
|
-
CarouselPrevious,
|
|
1271
|
-
CarouselNext,
|
|
1272
|
-
};
|
|
1273
|
-
|
|
1274
|
-
```
|
|
1275
|
-
|
|
1276
|
-
## components/ui/popover.tsx
|
|
1277
|
-
|
|
1278
|
-
```tsx
|
|
1279
|
-
"use client";
|
|
1280
|
-
|
|
1281
|
-
import * as React from "react";
|
|
1282
|
-
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
1283
|
-
|
|
1284
|
-
import { cn } from "@/lib/utils";
|
|
1285
|
-
|
|
1286
|
-
const Popover = PopoverPrimitive.Root;
|
|
1287
|
-
|
|
1288
|
-
const PopoverTrigger = PopoverPrimitive.Trigger;
|
|
1289
|
-
|
|
1290
|
-
const PopoverContent = React.forwardRef<
|
|
1291
|
-
React.ComponentRef<typeof PopoverPrimitive.Content>,
|
|
1292
|
-
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
|
1293
|
-
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
|
1294
|
-
<PopoverPrimitive.Portal>
|
|
1295
|
-
<PopoverPrimitive.Content
|
|
1296
|
-
ref={ref}
|
|
1297
|
-
align={align}
|
|
1298
|
-
sideOffset={sideOffset}
|
|
1299
|
-
className={cn(
|
|
1300
|
-
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out 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 z-50 w-72 rounded-md border p-4 shadow-md outline-none",
|
|
1301
|
-
className,
|
|
1302
|
-
)}
|
|
1303
|
-
{...props}
|
|
1304
|
-
/>
|
|
1305
|
-
</PopoverPrimitive.Portal>
|
|
1306
|
-
));
|
|
1307
|
-
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
|
1308
|
-
|
|
1309
|
-
export { Popover, PopoverTrigger, PopoverContent };
|
|
1310
|
-
|
|
1311
|
-
```
|
|
1312
|
-
|
|
1313
|
-
## components/ui/productcarousel.tsx
|
|
1314
|
-
|
|
1315
|
-
```tsx
|
|
1316
|
-
"use client";
|
|
1317
|
-
|
|
1318
|
-
import * as React from "react";
|
|
1319
|
-
import Autoplay from "embla-carousel-autoplay";
|
|
1320
|
-
|
|
1321
|
-
import { Card, CardContent } from "@/components/ui/card";
|
|
1322
|
-
import {
|
|
1323
|
-
Carousel,
|
|
1324
|
-
CarouselContent,
|
|
1325
|
-
CarouselItem,
|
|
1326
|
-
CarouselNext,
|
|
1327
|
-
CarouselPrevious,
|
|
1328
|
-
} from "@/components/ui/carousel";
|
|
1329
|
-
|
|
1330
|
-
interface Product {
|
|
1331
|
-
thumbnail: string;
|
|
1332
|
-
title: string;
|
|
1333
|
-
description: string;
|
|
1334
|
-
metadata_3: string;
|
|
1335
|
-
link: string;
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
interface ProductCarouselProps {
|
|
1339
|
-
products: Product[];
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
const productStyle = {
|
|
1343
|
-
display: "flex",
|
|
1344
|
-
flexDirection: "column" as const,
|
|
1345
|
-
alignItems: "center",
|
|
1346
|
-
textAlign: "center" as const,
|
|
1347
|
-
};
|
|
1348
|
-
|
|
1349
|
-
const imageContainerStyle = {
|
|
1350
|
-
justifyContent: "center",
|
|
1351
|
-
maxWidth: "100%",
|
|
1352
|
-
overflow: "hidden",
|
|
1353
|
-
};
|
|
1354
|
-
|
|
1355
|
-
const productLinkStyle = {
|
|
1356
|
-
display: "flex",
|
|
1357
|
-
justifyContent: "center",
|
|
1358
|
-
alignItems: "center",
|
|
1359
|
-
maxWidth: "100%",
|
|
1360
|
-
height: "100%",
|
|
1361
|
-
};
|
|
1362
|
-
|
|
1363
|
-
const productNameStyle = {
|
|
1364
|
-
marginTop: "0.5rem",
|
|
1365
|
-
};
|
|
1366
|
-
|
|
1367
|
-
const productPriceStyle = {
|
|
1368
|
-
marginTop: "0.5rem",
|
|
1369
|
-
};
|
|
1370
|
-
|
|
1371
|
-
export function CarouselPlugin({ products }: ProductCarouselProps) {
|
|
1372
|
-
const plugin = React.useRef(
|
|
1373
|
-
Autoplay({ delay: 2000, stopOnInteraction: true }),
|
|
1374
|
-
);
|
|
1375
|
-
|
|
1376
|
-
return (
|
|
1377
|
-
<Carousel
|
|
1378
|
-
plugins={[plugin.current]}
|
|
1379
|
-
className="relative w-full max-w-2xl"
|
|
1380
|
-
onMouseEnter={plugin.current.stop}
|
|
1381
|
-
onMouseLeave={plugin.current.reset}
|
|
1382
|
-
>
|
|
1383
|
-
<CarouselContent className="w-full">
|
|
1384
|
-
{products.map((product, index) => (
|
|
1385
|
-
<CarouselItem key={index} className="basis-1/2">
|
|
1386
|
-
<div className="relative w-full p-1">
|
|
1387
|
-
<Card className="h-full w-full">
|
|
1388
|
-
<CardContent className="flex flex-col items-center justify-center p-6">
|
|
1389
|
-
<div className="product" style={productStyle}>
|
|
1390
|
-
<div
|
|
1391
|
-
className="image-container"
|
|
1392
|
-
style={imageContainerStyle}
|
|
1393
|
-
>
|
|
1394
|
-
<a
|
|
1395
|
-
className="product-link product_image"
|
|
1396
|
-
href={product.link}
|
|
1397
|
-
style={productLinkStyle}
|
|
1398
|
-
data-mpn={product.metadata_3}
|
|
1399
|
-
data-query={product.title}
|
|
1400
|
-
data-intent="product_search"
|
|
1401
|
-
data-order={index}
|
|
1402
|
-
>
|
|
1403
|
-
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
1404
|
-
<img
|
|
1405
|
-
className="img-fluid mb-3"
|
|
1406
|
-
src={product.thumbnail}
|
|
1407
|
-
alt={product.title}
|
|
1408
|
-
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
|
1409
|
-
/>
|
|
1410
|
-
</a>
|
|
1411
|
-
<div className="productName" style={productNameStyle}>
|
|
1412
|
-
<a
|
|
1413
|
-
className="product-name-link"
|
|
1414
|
-
href={product.link}
|
|
1415
|
-
data-mpn={product.metadata_3}
|
|
1416
|
-
>
|
|
1417
|
-
{product.title}
|
|
1418
|
-
</a>
|
|
1419
|
-
</div>
|
|
1420
|
-
<p>
|
|
1421
|
-
<span
|
|
1422
|
-
className="productPrice"
|
|
1423
|
-
style={productPriceStyle}
|
|
1424
|
-
dangerouslySetInnerHTML={{
|
|
1425
|
-
__html: product.metadata_3,
|
|
1426
|
-
}}
|
|
1427
|
-
/>
|
|
1428
|
-
</p>
|
|
1429
|
-
{/* <div className="actionGroup" style={actionGroupStyle}>
|
|
1430
|
-
<a
|
|
1431
|
-
className="chat_buy_now"
|
|
1432
|
-
href={product.link}
|
|
1433
|
-
data-mpn={product.metadata_3}
|
|
1434
|
-
>
|
|
1435
|
-
Buy Now
|
|
1436
|
-
</a>
|
|
1437
|
-
</div>
|
|
1438
|
-
<div
|
|
1439
|
-
className="see_more_products"
|
|
1440
|
-
style={seeMoreProductsStyle}
|
|
1441
|
-
data-mpn={product.metadata_3}
|
|
1442
|
-
data-image={product.thumbnail}
|
|
1443
|
-
data-title={product.title}
|
|
1444
|
-
>
|
|
1445
|
-
See more like this
|
|
1446
|
-
</div> */}
|
|
1447
|
-
</div>
|
|
1448
|
-
</div>
|
|
1449
|
-
</CardContent>
|
|
1450
|
-
</Card>
|
|
1451
|
-
</div>
|
|
1452
|
-
</CarouselItem>
|
|
1453
|
-
))}
|
|
1454
|
-
</CarouselContent>
|
|
1455
|
-
<CarouselPrevious className="absolute left-2 top-1/2 z-10 -translate-y-1/2 transform" />
|
|
1456
|
-
<CarouselNext className="absolute right-2 top-1/2 z-10 -translate-y-1/2 transform" />
|
|
1457
|
-
</Carousel>
|
|
1458
|
-
);
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
```
|
|
1462
|
-
|
|
1463
|
-
## components/ui/tooltip.tsx
|
|
1464
|
-
|
|
1465
|
-
```tsx
|
|
1466
|
-
"use client";
|
|
1467
|
-
|
|
1468
|
-
import * as React from "react";
|
|
1469
|
-
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
1470
|
-
|
|
1471
|
-
import { cn } from "@/lib/utils";
|
|
1472
|
-
|
|
1473
|
-
function TooltipProvider({
|
|
1474
|
-
delayDuration = 0,
|
|
1475
|
-
...props
|
|
1476
|
-
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
1477
|
-
return (
|
|
1478
|
-
<TooltipPrimitive.Provider
|
|
1479
|
-
data-slot="tooltip-provider"
|
|
1480
|
-
delayDuration={delayDuration}
|
|
1481
|
-
{...props}
|
|
1482
|
-
/>
|
|
1483
|
-
);
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
function Tooltip({
|
|
1487
|
-
...props
|
|
1488
|
-
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
1489
|
-
return (
|
|
1490
|
-
<TooltipProvider>
|
|
1491
|
-
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
1492
|
-
</TooltipProvider>
|
|
1493
|
-
);
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
function TooltipTrigger({
|
|
1497
|
-
...props
|
|
1498
|
-
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
1499
|
-
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
function TooltipContent({
|
|
1503
|
-
className,
|
|
1504
|
-
sideOffset = 0,
|
|
1505
|
-
children,
|
|
1506
|
-
...props
|
|
1507
|
-
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
1508
|
-
return (
|
|
1509
|
-
<TooltipPrimitive.Portal>
|
|
1510
|
-
<TooltipPrimitive.Content
|
|
1511
|
-
data-slot="tooltip-content"
|
|
1512
|
-
sideOffset={sideOffset}
|
|
1513
|
-
className={cn(
|
|
1514
|
-
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit origin-[--radix-tooltip-content-transform-origin] text-balance rounded-md px-3 py-1.5 text-xs",
|
|
1515
|
-
className,
|
|
1516
|
-
)}
|
|
1517
|
-
{...props}
|
|
1518
|
-
>
|
|
1519
|
-
{children}
|
|
1520
|
-
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
|
1521
|
-
</TooltipPrimitive.Content>
|
|
1522
|
-
</TooltipPrimitive.Portal>
|
|
1523
|
-
);
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|
1527
|
-
|
|
1528
|
-
```
|
|
1529
|
-
|
|
1530
|
-
## eslint.config.ts
|
|
1531
|
-
|
|
1532
|
-
```typescript
|
|
1533
|
-
export { default } from "@assistant-ui/x-buildutils/eslint";
|
|
1534
|
-
|
|
1535
|
-
```
|
|
1536
|
-
|
|
1537
|
-
## lib/utils.ts
|
|
1538
|
-
|
|
1539
|
-
```typescript
|
|
1540
|
-
import { type ClassValue, clsx } from "clsx";
|
|
1541
|
-
import { twMerge } from "tailwind-merge";
|
|
1542
|
-
|
|
1543
|
-
export function cn(...inputs: ClassValue[]) {
|
|
1544
|
-
return twMerge(clsx(inputs));
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
```
|
|
1548
|
-
|
|
1549
|
-
## next.config.ts
|
|
1550
|
-
|
|
1551
|
-
```typescript
|
|
1552
|
-
import type { NextConfig } from "next";
|
|
1553
|
-
|
|
1554
|
-
const nextConfig: NextConfig = {
|
|
1555
|
-
/* config options here */
|
|
1556
|
-
};
|
|
1557
|
-
|
|
1558
|
-
export default nextConfig;
|
|
1559
|
-
|
|
1560
|
-
```
|
|
1561
|
-
|
|
1562
|
-
## package.json
|
|
1563
|
-
|
|
1564
|
-
```json
|
|
1565
|
-
{
|
|
1566
|
-
"name": "search-agent-for-e-commerce",
|
|
1567
|
-
"version": "0.1.0",
|
|
1568
|
-
"private": true,
|
|
1569
|
-
"scripts": {
|
|
1570
|
-
"dev": "next dev --turbo",
|
|
1571
|
-
"build": "next build",
|
|
1572
|
-
"start": "next start",
|
|
1573
|
-
"lint": "next lint"
|
|
1574
|
-
},
|
|
1575
|
-
"dependencies": {
|
|
1576
|
-
"@ai-sdk/openai": "^1.3.22",
|
|
1577
|
-
"@assistant-ui/react": "workspace:*",
|
|
1578
|
-
"@assistant-ui/react-ai-sdk-v4": "workspace:*",
|
|
1579
|
-
"@radix-ui/react-avatar": "^1.1.10",
|
|
1580
|
-
"@radix-ui/react-popover": "^1.1.14",
|
|
1581
|
-
"@radix-ui/react-slot": "^1.2.3",
|
|
1582
|
-
"@radix-ui/react-tooltip": "^1.2.7",
|
|
1583
|
-
"ai": "^4.3.16",
|
|
1584
|
-
"class-variance-authority": "^0.7.1",
|
|
1585
|
-
"clsx": "^2.1.1",
|
|
1586
|
-
"embla-carousel-autoplay": "^8.6.0",
|
|
1587
|
-
"embla-carousel-react": "^8.6.0",
|
|
1588
|
-
"lucide-react": "^0.535.0",
|
|
1589
|
-
"nanoid": "5.1.5",
|
|
1590
|
-
"next": "15.4.5",
|
|
1591
|
-
"react": "19.1.1",
|
|
1592
|
-
"react-dom": "19.1.1",
|
|
1593
|
-
"tailwind-merge": "^3.3.1",
|
|
1594
|
-
"tw-animate-css": "^1.3.6",
|
|
1595
|
-
"zod": "^4.0.14"
|
|
1596
|
-
},
|
|
1597
|
-
"devDependencies": {
|
|
1598
|
-
"@assistant-ui/x-buildutils": "workspace:*",
|
|
1599
|
-
"@types/node": "^24.1.0",
|
|
1600
|
-
"@types/react": "^19.1.9",
|
|
1601
|
-
"@types/react-dom": "^19.1.7",
|
|
1602
|
-
"eslint": "^9",
|
|
1603
|
-
"eslint-config-next": "15.4.5",
|
|
1604
|
-
"postcss": "^8.5.6",
|
|
1605
|
-
"tailwindcss": "^4.1.11",
|
|
1606
|
-
"typescript": "^5.9.2"
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
```
|
|
1611
|
-
|
|
1612
|
-
## README.md
|
|
1613
|
-
|
|
1614
|
-
```markdown
|
|
1615
|
-
## Getting Started
|
|
1616
|
-
|
|
1617
|
-
1. Clone the repository:
|
|
1618
|
-
|
|
1619
|
-
```sh
|
|
1620
|
-
git clone https://github.com/assistant-ui/assistant-ui.git
|
|
1621
|
-
```
|
|
1622
|
-
|
|
1623
|
-
2. Navigate to the project directory:
|
|
1624
|
-
|
|
1625
|
-
```sh
|
|
1626
|
-
cd assistant-ui/examples/search-agent-for-e-commerce
|
|
1627
|
-
```
|
|
1628
|
-
|
|
1629
|
-
3. Create a `.env` file with the following variable:
|
|
1630
|
-
|
|
1631
|
-
```sh
|
|
1632
|
-
OPENAI_API_KEY="skXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
|
1633
|
-
```
|
|
1634
|
-
|
|
1635
|
-
4. Make the `start.sh` script executable:
|
|
1636
|
-
|
|
1637
|
-
```sh
|
|
1638
|
-
chmod +x start.sh
|
|
1639
|
-
```
|
|
1640
|
-
|
|
1641
|
-
5. Start the servers:
|
|
1642
|
-
|
|
1643
|
-
```sh
|
|
1644
|
-
./start.sh
|
|
1645
|
-
```
|
|
1646
|
-
|
|
1647
|
-
6. Open the dummy e-commerce website in your browser:
|
|
1648
|
-
[http://localhost:8080/dummy-ecommerce-website.html](http://localhost:8080/dummy-ecommerce-website.html)
|
|
1649
|
-
|
|
1650
|
-
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
|
1651
|
-
|
|
1652
|
-
## Learn More
|
|
1653
|
-
|
|
1654
|
-
This project uses:
|
|
1655
|
-
|
|
1656
|
-
- assistant-ui components
|
|
1657
|
-
- shadcn components
|
|
1658
|
-
- Vercel AI SDK
|
|
1659
|
-
|
|
1660
|
-
```
|
|
1661
|
-
|
|
1662
|
-
## tsconfig.json
|
|
1663
|
-
|
|
1664
|
-
```json
|
|
1665
|
-
{
|
|
1666
|
-
"extends": "@assistant-ui/x-buildutils/ts/base",
|
|
1667
|
-
"compilerOptions": {
|
|
1668
|
-
"target": "ES6",
|
|
1669
|
-
"module": "ESNext",
|
|
1670
|
-
"incremental": true,
|
|
1671
|
-
"plugins": [
|
|
1672
|
-
{
|
|
1673
|
-
"name": "next"
|
|
1674
|
-
}
|
|
1675
|
-
],
|
|
1676
|
-
"allowJs": true,
|
|
1677
|
-
"strictNullChecks": true,
|
|
1678
|
-
"jsx": "preserve",
|
|
1679
|
-
"paths": {
|
|
1680
|
-
"@/*": ["./*"],
|
|
1681
|
-
"@assistant-ui/*": ["../../packages/*/src"],
|
|
1682
|
-
"@assistant-ui/react/*": ["../../packages/react/src/*"],
|
|
1683
|
-
"assistant-stream": ["../../packages/assistant-stream/src"],
|
|
1684
|
-
"assistant-stream/*": ["../../packages/assistant-stream/src/*"]
|
|
1685
|
-
}
|
|
1686
|
-
},
|
|
1687
|
-
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
1688
|
-
"exclude": ["node_modules"]
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
```
|
|
1692
|
-
|
|
1693
|
-
## vercel.json
|
|
1694
|
-
|
|
1695
|
-
```json
|
|
1696
|
-
{
|
|
1697
|
-
"version": 2,
|
|
1698
|
-
"builds": [
|
|
1699
|
-
{
|
|
1700
|
-
"src": "src/app/api/chat/validate.ts",
|
|
1701
|
-
"use": "@vercel/node"
|
|
1702
|
-
},
|
|
1703
|
-
{
|
|
1704
|
-
"src": "next.config.ts",
|
|
1705
|
-
"use": "@vercel/next"
|
|
1706
|
-
}
|
|
1707
|
-
],
|
|
1708
|
-
"routes": [
|
|
1709
|
-
{
|
|
1710
|
-
"src": "/api/validate",
|
|
1711
|
-
"dest": "src/app/api/chat/validate.ts"
|
|
1712
|
-
},
|
|
1713
|
-
{
|
|
1714
|
-
"src": "/(.*)",
|
|
1715
|
-
"dest": "/"
|
|
1716
|
-
}
|
|
1717
|
-
]
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
```
|
|
1721
|
-
|