create-mcp-use-app 2.0.0-beta.3 → 2.0.0-beta.4
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 +68 -37
- package/dist/index.js +10 -8
- package/dist/templates/blank/tsconfig.json +2 -1
- package/dist/templates/mcp-apps/index.ts +71 -101
- package/dist/templates/mcp-apps/public/logo-mcp-use.svg +8 -0
- package/dist/templates/mcp-apps/tsconfig.json +2 -1
- package/dist/templates/mcp-apps/views/my-view/MeshGradient.tsx +299 -0
- package/dist/templates/mcp-apps/views/my-view/Tooltip.tsx +75 -0
- package/dist/templates/mcp-apps/views/my-view/icons.tsx +142 -0
- package/dist/templates/mcp-apps/views/my-view/view.css +5 -0
- package/dist/templates/mcp-apps/views/my-view/view.tsx +396 -0
- package/dist/templates/{starter → mcp-server}/README.md +1 -1
- package/dist/templates/{starter → mcp-server}/index.ts +0 -18
- package/dist/templates/{starter → mcp-server}/tsconfig.json +2 -1
- package/package.json +2 -2
- package/dist/templates/mcp-apps/public/fruits/apple.webp +0 -0
- package/dist/templates/mcp-apps/public/fruits/apricot.webp +0 -0
- package/dist/templates/mcp-apps/public/fruits/avocado.webp +0 -0
- package/dist/templates/mcp-apps/public/fruits/banana.webp +0 -0
- package/dist/templates/mcp-apps/public/fruits/blueberry.webp +0 -0
- package/dist/templates/mcp-apps/public/fruits/cherries.webp +0 -0
- package/dist/templates/mcp-apps/public/fruits/coconut.webp +0 -0
- package/dist/templates/mcp-apps/public/fruits/grapes.webp +0 -0
- package/dist/templates/mcp-apps/public/fruits/lemon.webp +0 -0
- package/dist/templates/mcp-apps/public/fruits/mango.webp +0 -0
- package/dist/templates/mcp-apps/public/fruits/orange.webp +0 -0
- package/dist/templates/mcp-apps/public/fruits/pear.webp +0 -0
- package/dist/templates/mcp-apps/public/fruits/pineapple.webp +0 -0
- package/dist/templates/mcp-apps/public/fruits/plum.webp +0 -0
- package/dist/templates/mcp-apps/public/fruits/strawberry.webp +0 -0
- package/dist/templates/mcp-apps/public/fruits/watermelon.webp +0 -0
- package/dist/templates/mcp-apps/views/product-search-result/components/Accordion.tsx +0 -35
- package/dist/templates/mcp-apps/views/product-search-result/components/AccordionItem.tsx +0 -35
- package/dist/templates/mcp-apps/views/product-search-result/components/Carousel.tsx +0 -52
- package/dist/templates/mcp-apps/views/product-search-result/components/CarouselItem.tsx +0 -91
- package/dist/templates/mcp-apps/views/product-search-result/components/CarouselSkeleton.tsx +0 -18
- package/dist/templates/mcp-apps/views/product-search-result/hooks/useCarouselAnimation.ts +0 -72
- package/dist/templates/mcp-apps/views/product-search-result/view.css +0 -81
- package/dist/templates/mcp-apps/views/product-search-result/view.tsx +0 -371
- /package/dist/templates/{starter → mcp-server}/gitignore +0 -0
- /package/dist/templates/{starter → mcp-server}/package.json +0 -0
- /package/dist/templates/{starter → mcp-server}/public/icon.svg +0 -0
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { Image } from "mcp-use/react";
|
|
2
|
-
|
|
3
|
-
export interface CarouselItemProps {
|
|
4
|
-
fruit: string;
|
|
5
|
-
color: string;
|
|
6
|
-
isFavorite?: boolean;
|
|
7
|
-
isHighlighted?: boolean;
|
|
8
|
-
onClick: () => void;
|
|
9
|
-
onToggleFavorite?: () => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function HeartIcon({ filled }: { filled: boolean }) {
|
|
13
|
-
if (filled) {
|
|
14
|
-
return (
|
|
15
|
-
<svg
|
|
16
|
-
width="16"
|
|
17
|
-
height="16"
|
|
18
|
-
viewBox="0 0 24 24"
|
|
19
|
-
fill="currentColor"
|
|
20
|
-
aria-hidden
|
|
21
|
-
>
|
|
22
|
-
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
|
|
23
|
-
</svg>
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
return (
|
|
27
|
-
<svg
|
|
28
|
-
width="16"
|
|
29
|
-
height="16"
|
|
30
|
-
viewBox="0 0 24 24"
|
|
31
|
-
fill="none"
|
|
32
|
-
stroke="currentColor"
|
|
33
|
-
strokeWidth="2"
|
|
34
|
-
aria-hidden
|
|
35
|
-
>
|
|
36
|
-
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
|
|
37
|
-
</svg>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function CarouselItem({
|
|
42
|
-
fruit,
|
|
43
|
-
color,
|
|
44
|
-
isFavorite,
|
|
45
|
-
isHighlighted,
|
|
46
|
-
onClick,
|
|
47
|
-
onToggleFavorite,
|
|
48
|
-
}: CarouselItemProps) {
|
|
49
|
-
return (
|
|
50
|
-
<div
|
|
51
|
-
className={`carousel-item shrink-0 size-52 rounded-xl border border-neutral-200 dark:border-neutral-700 ${color}${isHighlighted ? " carousel-item-highlighted" : ""}`}
|
|
52
|
-
onClick={onClick}
|
|
53
|
-
role="button"
|
|
54
|
-
tabIndex={0}
|
|
55
|
-
onKeyDown={(e) => {
|
|
56
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
57
|
-
e.preventDefault();
|
|
58
|
-
onClick();
|
|
59
|
-
}
|
|
60
|
-
}}
|
|
61
|
-
>
|
|
62
|
-
{onToggleFavorite && (
|
|
63
|
-
<button
|
|
64
|
-
type="button"
|
|
65
|
-
onClick={(e) => {
|
|
66
|
-
e.stopPropagation();
|
|
67
|
-
onToggleFavorite();
|
|
68
|
-
}}
|
|
69
|
-
className={`absolute top-2 right-2 z-10 rounded-full p-1.5 transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-800 ${
|
|
70
|
-
isFavorite
|
|
71
|
-
? "text-red-500"
|
|
72
|
-
: "text-neutral-500 dark:text-neutral-400"
|
|
73
|
-
}`}
|
|
74
|
-
aria-label={isFavorite ? "Remove from favorites" : "Add to favorites"}
|
|
75
|
-
>
|
|
76
|
-
<HeartIcon filled={isFavorite === true} />
|
|
77
|
-
</button>
|
|
78
|
-
)}
|
|
79
|
-
<div className="carousel-item-bg">
|
|
80
|
-
<Image src={`/fruits/${fruit}.webp`} alt={fruit} />
|
|
81
|
-
</div>
|
|
82
|
-
<div className="carousel-item-content">
|
|
83
|
-
<Image
|
|
84
|
-
src={`/fruits/${fruit}.webp`}
|
|
85
|
-
alt={fruit}
|
|
86
|
-
className="h-24 w-24 object-contain"
|
|
87
|
-
/>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
const SKELETON_COUNT = 6;
|
|
2
|
-
|
|
3
|
-
export function CarouselSkeleton() {
|
|
4
|
-
return (
|
|
5
|
-
<div className="carousel-scroll-container w-full overflow-x-auto overflow-y-visible pl-8">
|
|
6
|
-
<div className="overflow-hidden">
|
|
7
|
-
<div className="flex gap-4">
|
|
8
|
-
{Array.from({ length: SKELETON_COUNT }).map((_, i) => (
|
|
9
|
-
<div
|
|
10
|
-
key={i}
|
|
11
|
-
className="carousel-item shrink-0 size-52 animate-pulse rounded-xl border border-neutral-200 bg-neutral-100 dark:border-neutral-700 dark:bg-neutral-800"
|
|
12
|
-
/>
|
|
13
|
-
))}
|
|
14
|
-
</div>
|
|
15
|
-
</div>
|
|
16
|
-
</div>
|
|
17
|
-
);
|
|
18
|
-
}
|
|
@@ -1,72 +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
|
-
const distance = Math.sqrt(
|
|
28
|
-
relativeX * relativeX + relativeY * relativeY
|
|
29
|
-
);
|
|
30
|
-
const maxDistance = Math.max(rect.width, rect.height) * 2;
|
|
31
|
-
const normalizedDistance = Math.min(distance / maxDistance, 1);
|
|
32
|
-
const proximity = Math.pow(1 - normalizedDistance, 2);
|
|
33
|
-
const opacity = 0.1 + proximity * 0.3;
|
|
34
|
-
const scale = 2.0 + proximity * 2.0;
|
|
35
|
-
|
|
36
|
-
article.style.setProperty("--pointer-x", x.toFixed(3));
|
|
37
|
-
article.style.setProperty("--pointer-y", y.toFixed(3));
|
|
38
|
-
article.style.setProperty("--icon-opacity", opacity.toFixed(3));
|
|
39
|
-
article.style.setProperty("--icon-scale", scale.toFixed(2));
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const handlePointerMove = (event: { clientX: number; clientY: number }) => {
|
|
44
|
-
lastPointerX = event.clientX;
|
|
45
|
-
lastPointerY = event.clientY;
|
|
46
|
-
updateItems();
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const handleScroll = () => {
|
|
50
|
-
updateItems();
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
document.addEventListener("pointermove", handlePointerMove);
|
|
54
|
-
window.addEventListener("scroll", handleScroll, true);
|
|
55
|
-
|
|
56
|
-
const scrollContainer = scrollContainerRef.current;
|
|
57
|
-
if (scrollContainer) {
|
|
58
|
-
scrollContainer.addEventListener("scroll", handleScroll);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
updateItems();
|
|
62
|
-
|
|
63
|
-
return () => {
|
|
64
|
-
document.removeEventListener("pointermove", handlePointerMove);
|
|
65
|
-
window.removeEventListener("scroll", handleScroll, true);
|
|
66
|
-
const container = scrollContainerRef.current;
|
|
67
|
-
if (container) {
|
|
68
|
-
container.removeEventListener("scroll", handleScroll);
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
}, [carouselContainerRef, scrollContainerRef]);
|
|
72
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/* Carousel scroll container with padding for blur effect */
|
|
2
|
-
.carousel-scroll-container {
|
|
3
|
-
padding-bottom: 2rem;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
/* Hover effect styles for carousel items */
|
|
7
|
-
.carousel-item {
|
|
8
|
-
position: relative;
|
|
9
|
-
container-type: size;
|
|
10
|
-
cursor: pointer;
|
|
11
|
-
transition-property: translate, scale;
|
|
12
|
-
transition-duration: 0.12s;
|
|
13
|
-
transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1);
|
|
14
|
-
-webkit-tap-highlight-color: transparent;
|
|
15
|
-
overflow: hidden;
|
|
16
|
-
border-radius: 12px;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
.carousel-item:active {
|
|
20
|
-
translate: 0 1px;
|
|
21
|
-
scale: 0.99;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.carousel-item-content {
|
|
25
|
-
position: relative;
|
|
26
|
-
z-index: 2;
|
|
27
|
-
width: 100%;
|
|
28
|
-
height: 100%;
|
|
29
|
-
display: flex;
|
|
30
|
-
align-items: center;
|
|
31
|
-
justify-content: center;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.carousel-item-bg {
|
|
35
|
-
position: absolute;
|
|
36
|
-
inset: 0;
|
|
37
|
-
display: grid;
|
|
38
|
-
place-items: center;
|
|
39
|
-
transform: translateZ(0);
|
|
40
|
-
filter: blur(28px) saturate(5) brightness(1.3);
|
|
41
|
-
translate: calc(var(--pointer-x, -10) * 50cqi)
|
|
42
|
-
calc(var(--pointer-y, -10) * 50cqh);
|
|
43
|
-
scale: var(--icon-scale, 3.4);
|
|
44
|
-
opacity: var(--icon-opacity, 0.25);
|
|
45
|
-
will-change: transform, filter, opacity;
|
|
46
|
-
pointer-events: none;
|
|
47
|
-
z-index: 1;
|
|
48
|
-
transition:
|
|
49
|
-
opacity 0.26s ease-out,
|
|
50
|
-
scale 0.26s ease-out;
|
|
51
|
-
border-radius: inherit;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.carousel-item-bg img {
|
|
55
|
-
width: 100px;
|
|
56
|
-
-webkit-user-select: none;
|
|
57
|
-
user-select: none;
|
|
58
|
-
-webkit-user-drag: none;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.carousel-item::after {
|
|
62
|
-
content: "";
|
|
63
|
-
position: absolute;
|
|
64
|
-
pointer-events: none;
|
|
65
|
-
inset: 0px;
|
|
66
|
-
border-radius: inherit;
|
|
67
|
-
border: 3px solid transparent;
|
|
68
|
-
backdrop-filter: blur(0px) saturate(4.2) brightness(2.5) contrast(2.5);
|
|
69
|
-
-webkit-backdrop-filter: blur(0px) saturate(4.2) brightness(2.5) contrast(2.5);
|
|
70
|
-
mask:
|
|
71
|
-
linear-gradient(#fff 0 100%) border-box,
|
|
72
|
-
linear-gradient(#fff 0 100%) padding-box;
|
|
73
|
-
mask-composite: exclude;
|
|
74
|
-
z-index: 3;
|
|
75
|
-
transform: translateZ(0);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.carousel-item-highlighted {
|
|
79
|
-
outline: 2px solid rgb(37 99 235);
|
|
80
|
-
outline-offset: 2px;
|
|
81
|
-
}
|
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Image,
|
|
3
|
-
ModelContext,
|
|
4
|
-
ThemeProvider,
|
|
5
|
-
ViewControls,
|
|
6
|
-
useCallTool,
|
|
7
|
-
useDisplayMode,
|
|
8
|
-
useHostContext,
|
|
9
|
-
useOpenExternal,
|
|
10
|
-
useSendFollowUp,
|
|
11
|
-
useToolContext,
|
|
12
|
-
useViewTheme,
|
|
13
|
-
useViewTool,
|
|
14
|
-
} from "mcp-use/react";
|
|
15
|
-
import { useCallback, useState } from "react";
|
|
16
|
-
import { z } from "zod";
|
|
17
|
-
|
|
18
|
-
import { Accordion } from "./components/Accordion.js";
|
|
19
|
-
import { Carousel } from "./components/Carousel.js";
|
|
20
|
-
import { CarouselSkeleton } from "./components/CarouselSkeleton.js";
|
|
21
|
-
import "./view.css";
|
|
22
|
-
|
|
23
|
-
const shellClass =
|
|
24
|
-
"relative rounded-3xl border border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-900";
|
|
25
|
-
|
|
26
|
-
const iconButtonClass =
|
|
27
|
-
"rounded-full border border-neutral-300 p-2 transition-colors hover:bg-neutral-100 dark:border-neutral-600 dark:hover:bg-neutral-800";
|
|
28
|
-
|
|
29
|
-
function HeartIcon({ filled }: { filled: boolean }) {
|
|
30
|
-
if (filled) {
|
|
31
|
-
return (
|
|
32
|
-
<svg
|
|
33
|
-
width="14"
|
|
34
|
-
height="14"
|
|
35
|
-
viewBox="0 0 24 24"
|
|
36
|
-
fill="currentColor"
|
|
37
|
-
aria-hidden
|
|
38
|
-
>
|
|
39
|
-
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
|
|
40
|
-
</svg>
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
return (
|
|
44
|
-
<svg
|
|
45
|
-
width="14"
|
|
46
|
-
height="14"
|
|
47
|
-
viewBox="0 0 24 24"
|
|
48
|
-
fill="none"
|
|
49
|
-
stroke="currentColor"
|
|
50
|
-
strokeWidth="2"
|
|
51
|
-
aria-hidden
|
|
52
|
-
>
|
|
53
|
-
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
|
|
54
|
-
</svg>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function ExpandIcon() {
|
|
59
|
-
return (
|
|
60
|
-
<svg
|
|
61
|
-
width="14"
|
|
62
|
-
height="14"
|
|
63
|
-
viewBox="0 0 24 24"
|
|
64
|
-
fill="none"
|
|
65
|
-
stroke="currentColor"
|
|
66
|
-
strokeWidth="2"
|
|
67
|
-
aria-hidden
|
|
68
|
-
>
|
|
69
|
-
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" />
|
|
70
|
-
</svg>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function PipIcon() {
|
|
75
|
-
return (
|
|
76
|
-
<svg
|
|
77
|
-
width="14"
|
|
78
|
-
height="14"
|
|
79
|
-
viewBox="0 0 24 24"
|
|
80
|
-
fill="none"
|
|
81
|
-
stroke="currentColor"
|
|
82
|
-
strokeWidth="2"
|
|
83
|
-
aria-hidden
|
|
84
|
-
>
|
|
85
|
-
<rect x="2" y="4" width="20" height="14" rx="2" />
|
|
86
|
-
<rect x="13" y="11" width="7" height="5" rx="1" fill="currentColor" />
|
|
87
|
-
</svg>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function CloseIcon() {
|
|
92
|
-
return (
|
|
93
|
-
<svg
|
|
94
|
-
width="14"
|
|
95
|
-
height="14"
|
|
96
|
-
viewBox="0 0 24 24"
|
|
97
|
-
fill="none"
|
|
98
|
-
stroke="currentColor"
|
|
99
|
-
strokeWidth="2"
|
|
100
|
-
aria-hidden
|
|
101
|
-
>
|
|
102
|
-
<line x1="18" y1="6" x2="6" y2="18" />
|
|
103
|
-
<line x1="6" y1="6" x2="18" y2="18" />
|
|
104
|
-
</svg>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function ProductSearchResultContent() {
|
|
109
|
-
const view = useToolContext<"search-fruits">();
|
|
110
|
-
const theme = useViewTheme();
|
|
111
|
-
const { locale, hostCapabilities } = useHostContext();
|
|
112
|
-
const { displayMode, availableDisplayModes, requestDisplayMode } =
|
|
113
|
-
useDisplayMode();
|
|
114
|
-
const sendFollowUp = useSendFollowUp();
|
|
115
|
-
const openExternal = useOpenExternal();
|
|
116
|
-
|
|
117
|
-
const [favorites, setFavorites] = useState<string[]>([]);
|
|
118
|
-
const [highlighted, setHighlighted] = useState<string | null>(null);
|
|
119
|
-
|
|
120
|
-
const details = useCallTool("get-fruit-details");
|
|
121
|
-
|
|
122
|
-
useViewTool(
|
|
123
|
-
{
|
|
124
|
-
name: "highlight-fruit",
|
|
125
|
-
description: "Highlight a visible result in the carousel",
|
|
126
|
-
inputSchema: z.object({ id: z.string() }),
|
|
127
|
-
},
|
|
128
|
-
async ({ id }) => {
|
|
129
|
-
setHighlighted(id);
|
|
130
|
-
return { content: [{ type: "text", text: `Highlighted ${id}` }] };
|
|
131
|
-
}
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
const toggleFavorite = useCallback((fruit: string) => {
|
|
135
|
-
setFavorites((current) =>
|
|
136
|
-
current.includes(fruit)
|
|
137
|
-
? current.filter((f) => f !== fruit)
|
|
138
|
-
: [...current, fruit]
|
|
139
|
-
);
|
|
140
|
-
}, []);
|
|
141
|
-
|
|
142
|
-
const accordionItems = [
|
|
143
|
-
{
|
|
144
|
-
question: "Demo of the autosize feature",
|
|
145
|
-
answer:
|
|
146
|
-
"This is a demo of the autosize feature. The view will automatically resize to fit the content, as supported by the MCP Apps specification.",
|
|
147
|
-
},
|
|
148
|
-
];
|
|
149
|
-
|
|
150
|
-
const root = theme === "dark" ? "dark" : "";
|
|
151
|
-
const lang = locale?.split("-")[0] ?? "en";
|
|
152
|
-
const isFullscreen = displayMode === "fullscreen";
|
|
153
|
-
const isPip = displayMode === "pip";
|
|
154
|
-
|
|
155
|
-
if (view.status === "error") {
|
|
156
|
-
return (
|
|
157
|
-
<div className={`${root} ${shellClass} p-8`} role="alert">
|
|
158
|
-
<p className="m-0 font-medium text-neutral-900 dark:text-neutral-100">
|
|
159
|
-
Search failed
|
|
160
|
-
</p>
|
|
161
|
-
<p className="mt-2 mb-0 text-sm text-neutral-600 dark:text-neutral-400">
|
|
162
|
-
{view.error.message}
|
|
163
|
-
</p>
|
|
164
|
-
</div>
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (view.status === "pending") {
|
|
169
|
-
return (
|
|
170
|
-
<div className={`${root} ${shellClass}`}>
|
|
171
|
-
<div className="p-8 pb-4">
|
|
172
|
-
<h5 className="mb-1 text-sm text-neutral-500 dark:text-neutral-400">
|
|
173
|
-
MCP-Apps Template
|
|
174
|
-
</h5>
|
|
175
|
-
<h2 className="mb-3 text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
|
|
176
|
-
Lovely Little Fruit Shop
|
|
177
|
-
</h2>
|
|
178
|
-
<div className="h-5 w-48 animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-800" />
|
|
179
|
-
</div>
|
|
180
|
-
<CarouselSkeleton />
|
|
181
|
-
</div>
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const { query, results } = view.toolOutput;
|
|
186
|
-
const selectedFruit = details.data?.structuredContent;
|
|
187
|
-
const isLoadingDetails = details.isPending;
|
|
188
|
-
|
|
189
|
-
return (
|
|
190
|
-
<div className={`${root} ${shellClass}`}>
|
|
191
|
-
<ModelContext
|
|
192
|
-
content={`User is viewing results for "${query}"; favorites: ${favorites.join(", ") || "none"}`}
|
|
193
|
-
/>
|
|
194
|
-
|
|
195
|
-
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
|
|
196
|
-
<span className="rounded-full px-2.5 py-1 text-xs font-medium tracking-wide text-neutral-500 uppercase dark:text-neutral-400">
|
|
197
|
-
{lang}
|
|
198
|
-
</span>
|
|
199
|
-
|
|
200
|
-
{favorites.length > 0 && (
|
|
201
|
-
<span className="flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium text-red-500">
|
|
202
|
-
<HeartIcon filled />
|
|
203
|
-
{favorites.length}
|
|
204
|
-
</span>
|
|
205
|
-
)}
|
|
206
|
-
|
|
207
|
-
{!isFullscreen && !isPip && (
|
|
208
|
-
<>
|
|
209
|
-
{availableDisplayModes.includes("pip") && (
|
|
210
|
-
<button
|
|
211
|
-
type="button"
|
|
212
|
-
className={iconButtonClass}
|
|
213
|
-
title="Picture-in-picture"
|
|
214
|
-
onClick={() => {
|
|
215
|
-
void requestDisplayMode({ mode: "pip" });
|
|
216
|
-
}}
|
|
217
|
-
>
|
|
218
|
-
<PipIcon />
|
|
219
|
-
</button>
|
|
220
|
-
)}
|
|
221
|
-
{availableDisplayModes.includes("fullscreen") && (
|
|
222
|
-
<button
|
|
223
|
-
type="button"
|
|
224
|
-
className={iconButtonClass}
|
|
225
|
-
title="Fullscreen"
|
|
226
|
-
onClick={() => {
|
|
227
|
-
void requestDisplayMode({ mode: "fullscreen" });
|
|
228
|
-
}}
|
|
229
|
-
>
|
|
230
|
-
<ExpandIcon />
|
|
231
|
-
</button>
|
|
232
|
-
)}
|
|
233
|
-
</>
|
|
234
|
-
)}
|
|
235
|
-
|
|
236
|
-
{(isFullscreen || isPip) && (
|
|
237
|
-
<button
|
|
238
|
-
type="button"
|
|
239
|
-
className={iconButtonClass}
|
|
240
|
-
title="Exit"
|
|
241
|
-
onClick={() => {
|
|
242
|
-
void requestDisplayMode({ mode: "inline" });
|
|
243
|
-
}}
|
|
244
|
-
>
|
|
245
|
-
<CloseIcon />
|
|
246
|
-
</button>
|
|
247
|
-
)}
|
|
248
|
-
</div>
|
|
249
|
-
|
|
250
|
-
<div className="p-8 pb-4">
|
|
251
|
-
<h5 className="mb-1 text-sm text-neutral-500 dark:text-neutral-400">
|
|
252
|
-
MCP-Apps Template
|
|
253
|
-
</h5>
|
|
254
|
-
<h2 className="mb-1 text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
|
|
255
|
-
Lovely Little Fruit Shop
|
|
256
|
-
</h2>
|
|
257
|
-
<p className="text-base text-neutral-600 dark:text-neutral-400">
|
|
258
|
-
{query
|
|
259
|
-
? `Showing results for "${query}"`
|
|
260
|
-
: "Tap a fruit to see details"}
|
|
261
|
-
</p>
|
|
262
|
-
</div>
|
|
263
|
-
|
|
264
|
-
<Carousel
|
|
265
|
-
results={results}
|
|
266
|
-
favorites={favorites}
|
|
267
|
-
highlighted={highlighted}
|
|
268
|
-
onSelectFruit={(fruit: string) => {
|
|
269
|
-
void details.callTool({ fruit });
|
|
270
|
-
}}
|
|
271
|
-
onToggleFavorite={toggleFavorite}
|
|
272
|
-
/>
|
|
273
|
-
|
|
274
|
-
{selectedFruit && (
|
|
275
|
-
<div className="mx-8 my-6 flex items-center gap-6 rounded-2xl border border-neutral-200 bg-neutral-50 p-5 dark:border-neutral-700 dark:bg-neutral-950">
|
|
276
|
-
<div
|
|
277
|
-
className={`shrink-0 rounded-xl p-4 ${
|
|
278
|
-
results.find((r) => r.fruit === selectedFruit.fruit)?.color ?? ""
|
|
279
|
-
}`}
|
|
280
|
-
>
|
|
281
|
-
<Image
|
|
282
|
-
src={`/fruits/${selectedFruit.fruit}.webp`}
|
|
283
|
-
alt={selectedFruit.fruit}
|
|
284
|
-
className="h-24 w-24 object-contain"
|
|
285
|
-
/>
|
|
286
|
-
</div>
|
|
287
|
-
<div className="flex-1">
|
|
288
|
-
{isLoadingDetails ? (
|
|
289
|
-
<div className="h-4 w-32 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
|
|
290
|
-
) : (
|
|
291
|
-
<>
|
|
292
|
-
<div className="mb-2 flex items-center gap-2">
|
|
293
|
-
<h3 className="text-lg font-semibold capitalize">
|
|
294
|
-
{selectedFruit.fruit}
|
|
295
|
-
</h3>
|
|
296
|
-
<button
|
|
297
|
-
type="button"
|
|
298
|
-
onClick={() => toggleFavorite(selectedFruit.fruit)}
|
|
299
|
-
className={`rounded-full p-1 transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-800 ${
|
|
300
|
-
favorites.includes(selectedFruit.fruit)
|
|
301
|
-
? "text-red-500"
|
|
302
|
-
: "text-neutral-500"
|
|
303
|
-
}`}
|
|
304
|
-
title={
|
|
305
|
-
favorites.includes(selectedFruit.fruit)
|
|
306
|
-
? "Remove from favorites"
|
|
307
|
-
: "Add to favorites"
|
|
308
|
-
}
|
|
309
|
-
>
|
|
310
|
-
<HeartIcon
|
|
311
|
-
filled={favorites.includes(selectedFruit.fruit)}
|
|
312
|
-
/>
|
|
313
|
-
</button>
|
|
314
|
-
</div>
|
|
315
|
-
<ul className="space-y-1">
|
|
316
|
-
{selectedFruit.facts.map((fact: string) => (
|
|
317
|
-
<li
|
|
318
|
-
key={fact}
|
|
319
|
-
className="flex items-start gap-2 text-sm text-neutral-600 dark:text-neutral-400"
|
|
320
|
-
>
|
|
321
|
-
<span className="mt-0.5 text-blue-600">•</span>
|
|
322
|
-
{fact}
|
|
323
|
-
</li>
|
|
324
|
-
))}
|
|
325
|
-
</ul>
|
|
326
|
-
<div className="mt-3 flex flex-wrap gap-2">
|
|
327
|
-
{hostCapabilities?.message !== undefined && (
|
|
328
|
-
<button
|
|
329
|
-
type="button"
|
|
330
|
-
onClick={() => {
|
|
331
|
-
void sendFollowUp({
|
|
332
|
-
prompt: `Tell me more interesting facts about ${selectedFruit.fruit}`,
|
|
333
|
-
}).catch(() => {});
|
|
334
|
-
}}
|
|
335
|
-
className="cursor-pointer rounded-lg bg-blue-50 px-3 py-1.5 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-100 dark:bg-blue-950 dark:text-blue-300 dark:hover:bg-blue-900"
|
|
336
|
-
>
|
|
337
|
-
Ask the AI for more about {selectedFruit.fruit}
|
|
338
|
-
</button>
|
|
339
|
-
)}
|
|
340
|
-
<button
|
|
341
|
-
type="button"
|
|
342
|
-
onClick={() => {
|
|
343
|
-
void openExternal({
|
|
344
|
-
url: `https://images.example.com/producers/${selectedFruit.fruit}`,
|
|
345
|
-
});
|
|
346
|
-
}}
|
|
347
|
-
className="cursor-pointer rounded-lg border border-neutral-300 px-3 py-1.5 text-xs font-medium text-neutral-700 transition-colors hover:bg-neutral-100 dark:border-neutral-600 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
|
348
|
-
>
|
|
349
|
-
View producer
|
|
350
|
-
</button>
|
|
351
|
-
</div>
|
|
352
|
-
</>
|
|
353
|
-
)}
|
|
354
|
-
</div>
|
|
355
|
-
</div>
|
|
356
|
-
)}
|
|
357
|
-
|
|
358
|
-
<Accordion items={accordionItems} />
|
|
359
|
-
</div>
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
export default function ProductSearchResult() {
|
|
364
|
-
return (
|
|
365
|
-
<ThemeProvider>
|
|
366
|
-
<ViewControls>
|
|
367
|
-
<ProductSearchResultContent />
|
|
368
|
-
</ViewControls>
|
|
369
|
-
</ThemeProvider>
|
|
370
|
-
);
|
|
371
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|