create-mcp-use-app 0.14.17 → 2.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -8
- package/dist/ansi.d.ts +9 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1768 -629
- package/dist/prompts.d.ts +16 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/templates/blank/index.ts +4 -92
- package/dist/templates/blank/package.json +3 -12
- package/dist/templates/mcp-apps/index.ts +28 -122
- package/dist/templates/mcp-apps/package.json +7 -14
- package/dist/templates/mcp-apps/resources/product-search-result/view.tsx +26 -0
- package/dist/templates/mcp-apps/src/register.d.ts +7 -0
- package/dist/templates/mcp-apps/tsconfig.json +8 -14
- package/dist/templates/starter/index.ts +43 -80
- package/dist/templates/starter/package.json +5 -18
- package/package.json +4 -12
- package/dist/templates/mcp-apps/pnpm-workspace.yaml +0 -2
- package/dist/templates/mcp-apps/resources/product-search-result/components/Accordion.tsx +0 -35
- package/dist/templates/mcp-apps/resources/product-search-result/components/AccordionItem.tsx +0 -32
- package/dist/templates/mcp-apps/resources/product-search-result/components/Carousel.tsx +0 -51
- package/dist/templates/mcp-apps/resources/product-search-result/components/CarouselItem.tsx +0 -54
- package/dist/templates/mcp-apps/resources/product-search-result/components/CarouselSkeleton.tsx +0 -20
- package/dist/templates/mcp-apps/resources/product-search-result/hooks/useCarouselAnimation.ts +0 -78
- package/dist/templates/mcp-apps/resources/product-search-result/types.ts +0 -20
- package/dist/templates/mcp-apps/resources/product-search-result/widget.tsx +0 -288
- package/dist/templates/mcp-apps/resources/styles.css +0 -117
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-mcp-use-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "2.0.0-beta.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Create MCP-Use apps with one command",
|
|
6
6
|
"author": "mcp-use, Inc.",
|
|
@@ -35,24 +35,16 @@
|
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": "^20.19.0 || >=22.12.0"
|
|
37
37
|
},
|
|
38
|
-
"dependencies": {
|
|
39
|
-
"chalk": "^5.6.2",
|
|
40
|
-
"commander": "^14.0.2",
|
|
41
|
-
"ink": "^6.6.0",
|
|
42
|
-
"ink-select-input": "^6.2.0",
|
|
43
|
-
"ink-text-input": "^6.0.0",
|
|
44
|
-
"ora": "^9.0.0",
|
|
45
|
-
"react": "^19.2.3",
|
|
46
|
-
"tar": "^7.5.16"
|
|
47
|
-
},
|
|
38
|
+
"dependencies": {},
|
|
48
39
|
"devDependencies": {
|
|
40
|
+
"@clack/prompts": "^1.7.0",
|
|
49
41
|
"vitest": "^4.1.0"
|
|
50
42
|
},
|
|
51
43
|
"publishConfig": {
|
|
52
44
|
"access": "public"
|
|
53
45
|
},
|
|
54
46
|
"scripts": {
|
|
55
|
-
"build": "npm run clean && tsup src/index.
|
|
47
|
+
"build": "npm run clean && tsup src/index.ts --format esm && tsc --emitDeclarationOnly --declaration && npm run copy-templates",
|
|
56
48
|
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
|
57
49
|
"copy-templates": "node scripts/copy-templates.js",
|
|
58
50
|
"dev": "tsc --build --watch",
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
2
|
-
import { AccordionItem } from "./AccordionItem";
|
|
3
|
-
|
|
4
|
-
export interface AccordionItemData {
|
|
5
|
-
question: string;
|
|
6
|
-
answer: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface AccordionProps {
|
|
10
|
-
items: AccordionItemData[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const Accordion: React.FC<AccordionProps> = ({ items }) => {
|
|
14
|
-
const [openAccordionIndex, setOpenAccordionIndex] = useState<number | null>(
|
|
15
|
-
null
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<div className="p-8 pt-4 border-t border-subtle mt-4">
|
|
20
|
-
<div className="rounded-lg border border-default overflow-hidden">
|
|
21
|
-
{items.map((item, index) => (
|
|
22
|
-
<AccordionItem
|
|
23
|
-
key={index}
|
|
24
|
-
question={item.question}
|
|
25
|
-
answer={item.answer}
|
|
26
|
-
isOpen={openAccordionIndex === index}
|
|
27
|
-
onToggle={() =>
|
|
28
|
-
setOpenAccordionIndex(openAccordionIndex === index ? null : index)
|
|
29
|
-
}
|
|
30
|
-
/>
|
|
31
|
-
))}
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
);
|
|
35
|
-
};
|
package/dist/templates/mcp-apps/resources/product-search-result/components/AccordionItem.tsx
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { Animate } from "@openai/apps-sdk-ui/components/Transition";
|
|
2
|
-
import React from "react";
|
|
3
|
-
import type { AccordionItemProps } from "../types";
|
|
4
|
-
|
|
5
|
-
export const AccordionItem: React.FC<AccordionItemProps> = ({
|
|
6
|
-
question,
|
|
7
|
-
answer,
|
|
8
|
-
isOpen,
|
|
9
|
-
onToggle,
|
|
10
|
-
}) => {
|
|
11
|
-
return (
|
|
12
|
-
<div className="border-b border-subtle last:border-b-0">
|
|
13
|
-
<button
|
|
14
|
-
type="button"
|
|
15
|
-
onClick={onToggle}
|
|
16
|
-
className="w-full flex items-center justify-between p-4 text-left hover:bg-primary-soft-hover transition-colors"
|
|
17
|
-
>
|
|
18
|
-
<span className="font-medium text-default">{question}</span>
|
|
19
|
-
<span className="text-xl text-tertiary transition-transform duration-200">
|
|
20
|
-
{isOpen ? "−" : "+"}
|
|
21
|
-
</span>
|
|
22
|
-
</button>
|
|
23
|
-
<Animate enter={{ y: 0, delay: 150, duration: 450 }} exit={{ y: -8 }}>
|
|
24
|
-
{isOpen && (
|
|
25
|
-
<div key="content" className="pb-4 text-secondary px-4">
|
|
26
|
-
{answer}
|
|
27
|
-
</div>
|
|
28
|
-
)}
|
|
29
|
-
</Animate>
|
|
30
|
-
</div>
|
|
31
|
-
);
|
|
32
|
-
};
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { Animate } from "@openai/apps-sdk-ui/components/Transition";
|
|
2
|
-
import React, { useRef } from "react";
|
|
3
|
-
import { CarouselItem } from "./CarouselItem";
|
|
4
|
-
import { useCarouselAnimation } from "../hooks/useCarouselAnimation";
|
|
5
|
-
|
|
6
|
-
interface CarouselProps {
|
|
7
|
-
results: Array<{ fruit: string; color: string }>;
|
|
8
|
-
favorites?: string[];
|
|
9
|
-
onSelectFruit: (fruit: string) => void;
|
|
10
|
-
onToggleFavorite?: (fruit: string) => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const Carousel: React.FC<CarouselProps> = ({
|
|
14
|
-
results,
|
|
15
|
-
favorites = [],
|
|
16
|
-
onSelectFruit,
|
|
17
|
-
onToggleFavorite,
|
|
18
|
-
}) => {
|
|
19
|
-
const carouselContainerRef = useRef<HTMLDivElement>(null);
|
|
20
|
-
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
21
|
-
|
|
22
|
-
useCarouselAnimation(carouselContainerRef, scrollContainerRef);
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<div
|
|
26
|
-
ref={scrollContainerRef}
|
|
27
|
-
className="carousel-scroll-container w-full overflow-x-auto overflow-y-visible pl-8"
|
|
28
|
-
>
|
|
29
|
-
<div ref={carouselContainerRef} className="overflow-visible">
|
|
30
|
-
<Animate className="flex gap-4">
|
|
31
|
-
{results.map((item, index) => {
|
|
32
|
-
const fruit =
|
|
33
|
-
item.fruit ?? (item as { name?: string }).name ?? "Item";
|
|
34
|
-
return (
|
|
35
|
-
<CarouselItem
|
|
36
|
-
key={`${fruit}-${index}`}
|
|
37
|
-
fruit={fruit}
|
|
38
|
-
color={item.color ?? "bg-default/10"}
|
|
39
|
-
isFavorite={favorites.includes(fruit)}
|
|
40
|
-
onClick={() => onSelectFruit(fruit)}
|
|
41
|
-
onToggleFavorite={
|
|
42
|
-
onToggleFavorite ? () => onToggleFavorite(fruit) : undefined
|
|
43
|
-
}
|
|
44
|
-
/>
|
|
45
|
-
);
|
|
46
|
-
})}
|
|
47
|
-
</Animate>
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
50
|
-
);
|
|
51
|
-
};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { Button } from "@openai/apps-sdk-ui/components/Button";
|
|
2
|
-
import { HeartFilled, HeartXs } from "@openai/apps-sdk-ui/components/Icon";
|
|
3
|
-
import { Image } from "mcp-use/react";
|
|
4
|
-
import React from "react";
|
|
5
|
-
|
|
6
|
-
export interface CarouselItemProps {
|
|
7
|
-
fruit: string;
|
|
8
|
-
color: string;
|
|
9
|
-
isFavorite?: boolean;
|
|
10
|
-
onClick: () => void;
|
|
11
|
-
onToggleFavorite?: () => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const CarouselItem: React.FC<CarouselItemProps> = ({
|
|
15
|
-
fruit,
|
|
16
|
-
color,
|
|
17
|
-
isFavorite,
|
|
18
|
-
onClick,
|
|
19
|
-
onToggleFavorite,
|
|
20
|
-
}) => {
|
|
21
|
-
return (
|
|
22
|
-
<div
|
|
23
|
-
className={`carousel-item size-52 rounded-xl border border-subtle ${color} cursor-pointer`}
|
|
24
|
-
onClick={onClick}
|
|
25
|
-
>
|
|
26
|
-
{onToggleFavorite && (
|
|
27
|
-
<Button
|
|
28
|
-
color="secondary"
|
|
29
|
-
pill
|
|
30
|
-
size="md"
|
|
31
|
-
uniform
|
|
32
|
-
variant="ghost"
|
|
33
|
-
onClick={(e) => {
|
|
34
|
-
e.stopPropagation();
|
|
35
|
-
onToggleFavorite();
|
|
36
|
-
}}
|
|
37
|
-
className={`absolute top-2 right-2 z-10 ${isFavorite ? "text-danger/80" : "text-secondary"}`}
|
|
38
|
-
>
|
|
39
|
-
{isFavorite ? <HeartFilled /> : <HeartXs />}
|
|
40
|
-
</Button>
|
|
41
|
-
)}
|
|
42
|
-
<div className="carousel-item-bg">
|
|
43
|
-
<Image src={"/fruits/" + fruit + ".png"} alt={fruit} />
|
|
44
|
-
</div>
|
|
45
|
-
<div className="carousel-item-content">
|
|
46
|
-
<Image
|
|
47
|
-
src={"/fruits/" + fruit + ".png"}
|
|
48
|
-
alt={fruit}
|
|
49
|
-
className="w-24 h-24 object-contain"
|
|
50
|
-
/>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
);
|
|
54
|
-
};
|
package/dist/templates/mcp-apps/resources/product-search-result/components/CarouselSkeleton.tsx
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
|
|
3
|
-
const SKELETON_COUNT = 6;
|
|
4
|
-
|
|
5
|
-
export const CarouselSkeleton: React.FC = () => {
|
|
6
|
-
return (
|
|
7
|
-
<div className="carousel-scroll-container w-full overflow-x-auto overflow-y-visible pl-8">
|
|
8
|
-
<div className="overflow-hidden">
|
|
9
|
-
<div className="flex gap-4">
|
|
10
|
-
{Array.from({ length: SKELETON_COUNT }).map((_, i) => (
|
|
11
|
-
<div
|
|
12
|
-
key={i}
|
|
13
|
-
className="carousel-item shrink-0 size-52 rounded-xl border border-subtle animate-pulse bg-gray-100"
|
|
14
|
-
></div>
|
|
15
|
-
))}
|
|
16
|
-
</div>
|
|
17
|
-
</div>
|
|
18
|
-
</div>
|
|
19
|
-
);
|
|
20
|
-
};
|
package/dist/templates/mcp-apps/resources/product-search-result/hooks/useCarouselAnimation.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { useEffect, type RefObject } from "react";
|
|
2
|
-
|
|
3
|
-
export function useCarouselAnimation(
|
|
4
|
-
carouselContainerRef: RefObject<HTMLDivElement | null>,
|
|
5
|
-
scrollContainerRef: RefObject<HTMLDivElement | null>
|
|
6
|
-
): void {
|
|
7
|
-
useEffect(() => {
|
|
8
|
-
let lastPointerX = 0;
|
|
9
|
-
let lastPointerY = 0;
|
|
10
|
-
|
|
11
|
-
const updateItems = () => {
|
|
12
|
-
const container = carouselContainerRef.current;
|
|
13
|
-
if (!container) return;
|
|
14
|
-
|
|
15
|
-
const articles =
|
|
16
|
-
container.querySelectorAll<HTMLElement>(".carousel-item");
|
|
17
|
-
|
|
18
|
-
articles.forEach((article) => {
|
|
19
|
-
const rect = article.getBoundingClientRect();
|
|
20
|
-
const centerX = rect.left + rect.width / 2;
|
|
21
|
-
const centerY = rect.top + rect.height / 2;
|
|
22
|
-
const relativeX = lastPointerX - centerX;
|
|
23
|
-
const relativeY = lastPointerY - centerY;
|
|
24
|
-
const x = relativeX / (rect.width / 2);
|
|
25
|
-
const y = relativeY / (rect.height / 2);
|
|
26
|
-
|
|
27
|
-
// Calculate distance from cursor to center of item
|
|
28
|
-
const distance = Math.sqrt(
|
|
29
|
-
relativeX * relativeX + relativeY * relativeY
|
|
30
|
-
);
|
|
31
|
-
// Use a larger max distance to make the effect work across gaps
|
|
32
|
-
const maxDistance = Math.max(rect.width, rect.height) * 2;
|
|
33
|
-
const normalizedDistance = Math.min(distance / maxDistance, 1);
|
|
34
|
-
|
|
35
|
-
// Closer items get higher opacity and scale
|
|
36
|
-
// Use exponential falloff for smoother transition
|
|
37
|
-
const proximity = Math.pow(1 - normalizedDistance, 2);
|
|
38
|
-
const opacity = 0.1 + proximity * 0.3; // Range from 0.1 to 0.4
|
|
39
|
-
const scale = 2.0 + proximity * 2.0; // Range from 2.0 to 4.0
|
|
40
|
-
|
|
41
|
-
article.style.setProperty("--pointer-x", x.toFixed(3));
|
|
42
|
-
article.style.setProperty("--pointer-y", y.toFixed(3));
|
|
43
|
-
article.style.setProperty("--icon-opacity", opacity.toFixed(3));
|
|
44
|
-
article.style.setProperty("--icon-scale", scale.toFixed(2));
|
|
45
|
-
});
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const handlePointerMove = (event: { clientX: number; clientY: number }) => {
|
|
49
|
-
lastPointerX = event.clientX;
|
|
50
|
-
lastPointerY = event.clientY;
|
|
51
|
-
updateItems();
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const handleScroll = () => {
|
|
55
|
-
updateItems();
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
document.addEventListener("pointermove", handlePointerMove);
|
|
59
|
-
window.addEventListener("scroll", handleScroll, true);
|
|
60
|
-
|
|
61
|
-
const scrollContainer = scrollContainerRef.current;
|
|
62
|
-
if (scrollContainer) {
|
|
63
|
-
scrollContainer.addEventListener("scroll", handleScroll);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Initial update
|
|
67
|
-
updateItems();
|
|
68
|
-
|
|
69
|
-
return () => {
|
|
70
|
-
document.removeEventListener("pointermove", handlePointerMove);
|
|
71
|
-
window.removeEventListener("scroll", handleScroll, true);
|
|
72
|
-
const container = scrollContainerRef.current;
|
|
73
|
-
if (container) {
|
|
74
|
-
container.removeEventListener("scroll", handleScroll);
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
}, [carouselContainerRef, scrollContainerRef]);
|
|
78
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export const propSchema = z.object({
|
|
4
|
-
query: z.string().describe("The search query"),
|
|
5
|
-
results: z.array(
|
|
6
|
-
z.object({
|
|
7
|
-
fruit: z.string().describe("Fruit name"),
|
|
8
|
-
color: z.string().describe("Tailwind background color class"),
|
|
9
|
-
})
|
|
10
|
-
),
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
export type ProductSearchResultProps = z.infer<typeof propSchema>;
|
|
14
|
-
|
|
15
|
-
export type AccordionItemProps = {
|
|
16
|
-
question: string;
|
|
17
|
-
answer: string;
|
|
18
|
-
isOpen: boolean;
|
|
19
|
-
onToggle: () => void;
|
|
20
|
-
};
|
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
import { AppsSDKUIProvider } from "@openai/apps-sdk-ui/components/AppsSDKUIProvider";
|
|
2
|
-
import {
|
|
3
|
-
Image,
|
|
4
|
-
McpUseProvider,
|
|
5
|
-
useCallTool,
|
|
6
|
-
useWidget,
|
|
7
|
-
type WidgetMetadata,
|
|
8
|
-
} from "mcp-use/react";
|
|
9
|
-
import React, { useCallback } from "react";
|
|
10
|
-
import { Link } from "react-router";
|
|
11
|
-
import "../styles.css";
|
|
12
|
-
import { Carousel } from "./components/Carousel";
|
|
13
|
-
import { CarouselSkeleton } from "./components/CarouselSkeleton";
|
|
14
|
-
import { Accordion } from "./components/Accordion";
|
|
15
|
-
import type { ProductSearchResultProps } from "./types";
|
|
16
|
-
import { propSchema } from "./types";
|
|
17
|
-
import { Button } from "@openai/apps-sdk-ui/components/Button";
|
|
18
|
-
import {
|
|
19
|
-
Expand,
|
|
20
|
-
HeartFilled,
|
|
21
|
-
HeartXs,
|
|
22
|
-
PictureInPicture,
|
|
23
|
-
} from "@openai/apps-sdk-ui/components/Icon";
|
|
24
|
-
export const widgetMetadata: WidgetMetadata = {
|
|
25
|
-
description:
|
|
26
|
-
"Display product search results with filtering, state management, and tool interactions",
|
|
27
|
-
props: propSchema,
|
|
28
|
-
exposeAsTool: false,
|
|
29
|
-
metadata: {
|
|
30
|
-
prefersBorder: false,
|
|
31
|
-
invoking: "Loading product search results...",
|
|
32
|
-
invoked: "Product search results loaded",
|
|
33
|
-
csp: {
|
|
34
|
-
// Widget-specific
|
|
35
|
-
resourceDomains: ["https://cdn.openai.com"],
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
type FavoritesState = { favorites: string[] };
|
|
40
|
-
|
|
41
|
-
const ProductSearchResult: React.FC = () => {
|
|
42
|
-
const {
|
|
43
|
-
props,
|
|
44
|
-
isPending,
|
|
45
|
-
displayMode,
|
|
46
|
-
requestDisplayMode,
|
|
47
|
-
sendFollowUpMessage,
|
|
48
|
-
locale,
|
|
49
|
-
state,
|
|
50
|
-
setState,
|
|
51
|
-
} = useWidget<ProductSearchResultProps, FavoritesState>();
|
|
52
|
-
|
|
53
|
-
const {
|
|
54
|
-
callTool: getFruitDetails,
|
|
55
|
-
data: fruitDetails,
|
|
56
|
-
isPending: isLoadingDetails,
|
|
57
|
-
} = useCallTool("get-fruit-details");
|
|
58
|
-
|
|
59
|
-
const selectedFruit = fruitDetails?.structuredContent as
|
|
60
|
-
| { fruit: string; facts?: string[] }
|
|
61
|
-
| undefined;
|
|
62
|
-
const favorites = state?.favorites ?? [];
|
|
63
|
-
|
|
64
|
-
const toggleFavorite = useCallback(
|
|
65
|
-
(fruit: string) => {
|
|
66
|
-
const current = state?.favorites ?? [];
|
|
67
|
-
const next = current.includes(fruit)
|
|
68
|
-
? current.filter((f: string) => f !== fruit)
|
|
69
|
-
: [...current, fruit];
|
|
70
|
-
setState({ favorites: next });
|
|
71
|
-
},
|
|
72
|
-
[state, setState]
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
const accordionItems = [
|
|
76
|
-
{
|
|
77
|
-
question: "Demo of the autosize feature",
|
|
78
|
-
answer:
|
|
79
|
-
"This is a demo of the autosize feature. The widget will automatically resize to fit the content, as supported by the mcp-apps specification",
|
|
80
|
-
},
|
|
81
|
-
];
|
|
82
|
-
|
|
83
|
-
if (isPending) {
|
|
84
|
-
return (
|
|
85
|
-
<McpUseProvider>
|
|
86
|
-
<div className="relative bg-surface-elevated border border-default rounded-3xl">
|
|
87
|
-
<div className="p-8 pb-4">
|
|
88
|
-
<h5 className="text-secondary mb-1">MCP-Apps Template</h5>
|
|
89
|
-
<h2 className="heading-xl mb-3">Lovely Little Fruit Shop</h2>
|
|
90
|
-
<div className="h-5 w-48 rounded-md bg-default/10 animate-pulse" />
|
|
91
|
-
</div>
|
|
92
|
-
<CarouselSkeleton />
|
|
93
|
-
</div>
|
|
94
|
-
</McpUseProvider>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const { query, results } = props;
|
|
99
|
-
const isFullscreen = displayMode === "fullscreen";
|
|
100
|
-
const isPip = displayMode === "pip";
|
|
101
|
-
const lang = locale?.split("-")[0] ?? "en";
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<McpUseProvider>
|
|
105
|
-
<AppsSDKUIProvider linkComponent={Link}>
|
|
106
|
-
<div className="relative bg-surface-elevated border border-default rounded-3xl">
|
|
107
|
-
{/* Toolbar — top-right badges and controls */}
|
|
108
|
-
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
|
|
109
|
-
{/* Locale badge */}
|
|
110
|
-
<span className="px-2.5 py-1 text-xs font-medium rounded-full text-secondary uppercase tracking-wide">
|
|
111
|
-
{lang}
|
|
112
|
-
</span>
|
|
113
|
-
|
|
114
|
-
{/* Favorites count */}
|
|
115
|
-
{favorites.length > 0 && (
|
|
116
|
-
<Button
|
|
117
|
-
color="secondary"
|
|
118
|
-
pill
|
|
119
|
-
size="lg"
|
|
120
|
-
uniform
|
|
121
|
-
variant="ghost"
|
|
122
|
-
className="text-danger/80"
|
|
123
|
-
>
|
|
124
|
-
<HeartFilled />
|
|
125
|
-
{favorites.length}
|
|
126
|
-
</Button>
|
|
127
|
-
)}
|
|
128
|
-
|
|
129
|
-
{/* Display mode buttons */}
|
|
130
|
-
{!isFullscreen && !isPip && (
|
|
131
|
-
<>
|
|
132
|
-
<Button
|
|
133
|
-
color="secondary"
|
|
134
|
-
pill
|
|
135
|
-
size="lg"
|
|
136
|
-
uniform
|
|
137
|
-
variant="outline"
|
|
138
|
-
onClick={() => requestDisplayMode("pip")}
|
|
139
|
-
title="Picture-in-picture"
|
|
140
|
-
>
|
|
141
|
-
<PictureInPicture />
|
|
142
|
-
</Button>
|
|
143
|
-
<Button
|
|
144
|
-
color="secondary"
|
|
145
|
-
pill
|
|
146
|
-
size="lg"
|
|
147
|
-
uniform
|
|
148
|
-
variant="outline"
|
|
149
|
-
onClick={() => requestDisplayMode("fullscreen")}
|
|
150
|
-
title="Fullscreen"
|
|
151
|
-
>
|
|
152
|
-
<Expand />
|
|
153
|
-
</Button>
|
|
154
|
-
</>
|
|
155
|
-
)}
|
|
156
|
-
|
|
157
|
-
{(isFullscreen || isPip) && (
|
|
158
|
-
<Button
|
|
159
|
-
color="secondary"
|
|
160
|
-
pill
|
|
161
|
-
size="lg"
|
|
162
|
-
uniform
|
|
163
|
-
variant="outline"
|
|
164
|
-
onClick={() => requestDisplayMode("inline")}
|
|
165
|
-
title="Exit"
|
|
166
|
-
>
|
|
167
|
-
<svg
|
|
168
|
-
width="14"
|
|
169
|
-
height="14"
|
|
170
|
-
viewBox="0 0 24 24"
|
|
171
|
-
fill="none"
|
|
172
|
-
stroke="currentColor"
|
|
173
|
-
strokeWidth="2"
|
|
174
|
-
strokeLinecap="round"
|
|
175
|
-
strokeLinejoin="round"
|
|
176
|
-
>
|
|
177
|
-
<line x1="18" y1="6" x2="6" y2="18" />
|
|
178
|
-
<line x1="6" y1="6" x2="18" y2="18" />
|
|
179
|
-
</svg>
|
|
180
|
-
</Button>
|
|
181
|
-
)}
|
|
182
|
-
</div>
|
|
183
|
-
|
|
184
|
-
{/* Header */}
|
|
185
|
-
<div className="p-8 pb-4">
|
|
186
|
-
<h5 className="text-secondary mb-1">MCP-Apps Template</h5>
|
|
187
|
-
<h2 className="heading-xl mb-1">Lovely Little Fruit Shop</h2>
|
|
188
|
-
<p className="text-md text-secondary">
|
|
189
|
-
{query
|
|
190
|
-
? `Showing results for "${query}"`
|
|
191
|
-
: "Tap a fruit to see details"}
|
|
192
|
-
</p>
|
|
193
|
-
</div>
|
|
194
|
-
|
|
195
|
-
{/* Carousel */}
|
|
196
|
-
<Carousel
|
|
197
|
-
results={results}
|
|
198
|
-
favorites={favorites}
|
|
199
|
-
onSelectFruit={(fruit: string) => getFruitDetails({ fruit })}
|
|
200
|
-
onToggleFavorite={toggleFavorite}
|
|
201
|
-
/>
|
|
202
|
-
|
|
203
|
-
{/* Detail view */}
|
|
204
|
-
{selectedFruit && (
|
|
205
|
-
<div className="mx-8 my-6 rounded-2xl border border-default bg-surface p-5 flex items-center gap-6">
|
|
206
|
-
<div
|
|
207
|
-
className={`rounded-xl p-4 shrink-0 ${
|
|
208
|
-
results.find(
|
|
209
|
-
(r: { fruit: string }) => r.fruit === selectedFruit.fruit
|
|
210
|
-
)?.color ?? ""
|
|
211
|
-
}`}
|
|
212
|
-
>
|
|
213
|
-
<Image
|
|
214
|
-
src={`/fruits/${selectedFruit.fruit}.png`}
|
|
215
|
-
alt={selectedFruit.fruit}
|
|
216
|
-
className="w-24 h-24 object-contain"
|
|
217
|
-
/>
|
|
218
|
-
</div>
|
|
219
|
-
<div className="flex-1">
|
|
220
|
-
{isLoadingDetails ? (
|
|
221
|
-
<div className="animate-pulse h-4 w-32 bg-surface-elevated rounded" />
|
|
222
|
-
) : (
|
|
223
|
-
<>
|
|
224
|
-
<div className="flex items-center gap-2 mb-2">
|
|
225
|
-
<h3 className="font-semibold text-lg capitalize">
|
|
226
|
-
{selectedFruit.fruit}
|
|
227
|
-
</h3>
|
|
228
|
-
<Button
|
|
229
|
-
color="secondary"
|
|
230
|
-
pill
|
|
231
|
-
size="md"
|
|
232
|
-
uniform
|
|
233
|
-
variant="ghost"
|
|
234
|
-
onClick={() => toggleFavorite(selectedFruit.fruit)}
|
|
235
|
-
title={
|
|
236
|
-
favorites.includes(selectedFruit.fruit)
|
|
237
|
-
? "Remove from favorites"
|
|
238
|
-
: "Add to favorites"
|
|
239
|
-
}
|
|
240
|
-
className={
|
|
241
|
-
favorites.includes(selectedFruit.fruit)
|
|
242
|
-
? "text-danger/80"
|
|
243
|
-
: "text-secondary"
|
|
244
|
-
}
|
|
245
|
-
>
|
|
246
|
-
{favorites.includes(selectedFruit.fruit) ? (
|
|
247
|
-
<HeartFilled />
|
|
248
|
-
) : (
|
|
249
|
-
<HeartXs />
|
|
250
|
-
)}
|
|
251
|
-
</Button>
|
|
252
|
-
</div>
|
|
253
|
-
<ul className="space-y-1">
|
|
254
|
-
{(selectedFruit.facts ?? []).map((fact: string) => (
|
|
255
|
-
<li
|
|
256
|
-
key={fact}
|
|
257
|
-
className="text-sm text-secondary flex items-start gap-2"
|
|
258
|
-
>
|
|
259
|
-
<span className="text-info mt-0.5">•</span>
|
|
260
|
-
{fact}
|
|
261
|
-
</li>
|
|
262
|
-
))}
|
|
263
|
-
</ul>
|
|
264
|
-
{/* Follow-up message demo — sends a message to the LLM from the widget */}
|
|
265
|
-
<button
|
|
266
|
-
onClick={() =>
|
|
267
|
-
sendFollowUpMessage(
|
|
268
|
-
`Tell me more interesting facts about ${selectedFruit.fruit}`
|
|
269
|
-
)
|
|
270
|
-
}
|
|
271
|
-
className="mt-3 px-3 py-1.5 text-xs font-medium rounded-lg bg-info/10 text-info hover:bg-info/20 transition-colors cursor-pointer"
|
|
272
|
-
>
|
|
273
|
-
Ask the AI for more about {selectedFruit.fruit}
|
|
274
|
-
</button>
|
|
275
|
-
</>
|
|
276
|
-
)}
|
|
277
|
-
</div>
|
|
278
|
-
</div>
|
|
279
|
-
)}
|
|
280
|
-
|
|
281
|
-
<Accordion items={accordionItems} />
|
|
282
|
-
</div>
|
|
283
|
-
</AppsSDKUIProvider>
|
|
284
|
-
</McpUseProvider>
|
|
285
|
-
);
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
export default ProductSearchResult;
|