Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/toolkits/toolkits/etsy/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from "zod";
import { EtsyTools } from "./tools/tools";

import { getListings } from "@/toolkits/toolkits/etsy/tools/get-listings/base";
import { getShopInfo } from "@/toolkits/toolkits/etsy/tools/get-shop-info/base";

import type { ToolkitConfig } from "@/toolkits/types";

Expand All @@ -14,6 +15,7 @@ export const baseEtsyToolkitConfig: ToolkitConfig<
> = {
tools: {
[EtsyTools.getListings]: getListings,
[EtsyTools.getShopInfo]: getShopInfo,
},
parameters: etsyParameters,
};
2 changes: 2 additions & 0 deletions src/toolkits/toolkits/etsy/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { baseEtsyToolkitConfig } from "./base";
import { createClientToolkit } from "@/toolkits/create-toolkit";

import { getListingsClientConfig } from "@/toolkits/toolkits/etsy/tools/get-listings/client";
import { getShopInfoClientConfig } from "@/toolkits/toolkits/etsy/tools/get-shop-info/client";

import { ToolkitGroups } from "@/toolkits/types";
import { EtsyTools } from "./tools/tools";
Expand Down Expand Up @@ -37,5 +38,6 @@ export const etsyClientToolkit = createClientToolkit(
},
{
[EtsyTools.getListings]: getListingsClientConfig,
[EtsyTools.getShopInfo]: getShopInfoClientConfig,
},
);
5 changes: 4 additions & 1 deletion src/toolkits/toolkits/etsy/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import { EtsyTools } from "./tools/tools";
import { EtsySecurityDataStorage } from "./security-data-storage";

import { getListingsServerConfig } from "@/toolkits/toolkits/etsy/tools/get-listings/server";
import { getShopInfoServerConfig } from "@/toolkits/toolkits/etsy/tools/get-shop-info/server";

export const etsyToolkitServer = createServerToolkit(
baseEtsyToolkitConfig,
"You have access to the Etsy toolkit for general account management. Currently, this toolkit provides:\n" +
"- **Get Listings**: Retrieves all listings and their image URLs associated with the shop associated with the signed-in user.\n\n",
"- **Get Listings**: Retrieves all listings and their image URLs associated with the shop associated with the signed-in user.\n" +
"- **Get Shop Info**: Retrieves the signed-in user's Etsy shop profile, status, and summary metrics.\n\n",
async () => {
const account = await api.accounts.getAccountByProvider("etsy");

Expand All @@ -33,6 +35,7 @@ export const etsyToolkitServer = createServerToolkit(

return {
[EtsyTools.getListings]: getListingsServerConfig(etsy),
[EtsyTools.getShopInfo]: getShopInfoServerConfig(etsy),
};
},
);
13 changes: 13 additions & 0 deletions src/toolkits/toolkits/etsy/tools/get-shop-info/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from "zod";
import { createBaseTool } from "@/toolkits/create-tool";

import type { IShop } from "etsy-ts";

export const getShopInfo = createBaseTool({
description:
"Fetches the Etsy shop profile and summary metrics for the authenticated user.",
inputSchema: z.object({}),
outputSchema: z.object({
shop: z.custom<IShop>(),
}),
});
111 changes: 111 additions & 0 deletions src/toolkits/toolkits/etsy/tools/get-shop-info/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
ExternalLink,
Heart,
Package,
ShoppingBag,
Star,
Store,
} from "lucide-react";

import type { ClientToolConfig } from "@/toolkits/types";
import type { getShopInfo } from "./base";
import type React from "react";

export const getShopInfoClientConfig: ClientToolConfig<
typeof getShopInfo.inputSchema.shape,
typeof getShopInfo.outputSchema.shape
> = {
CallComponent: ({ isPartial }) => (
<div className="flex items-center gap-2">
<Store className="h-4 w-4" />
<span className="text-sm font-medium">Fetching Etsy shop info</span>
{isPartial && <span className="animate-pulse">...</span>}
</div>
),
ResultComponent: ({ result: { shop } }) => (
<div className="space-y-3">
<div className="flex items-start gap-3">
<div className="bg-muted rounded-md p-2">
<Store className="h-5 w-5" />
</div>
<div className="min-w-0">
<h2 className="truncate text-lg font-bold">
{shop.shop_name ?? "Etsy shop"}
</h2>
{shop.title && (
<p className="text-muted-foreground line-clamp-2 text-sm">
{shop.title}
</p>
)}
<div className="text-muted-foreground mt-1 text-xs">
{shop.is_vacation ? "Vacation mode" : "Open"} /{" "}
{shop.currency_code ?? "Currency unavailable"}
</div>
</div>
</div>

<div className="grid grid-cols-2 gap-2 text-sm">
<ShopMetric
icon={Package}
label="Active listings"
value={formatCount(shop.listing_active_count)}
/>
<ShopMetric
icon={ShoppingBag}
label="Sales"
value={formatCount(shop.transaction_sold_count)}
/>
<ShopMetric
icon={Heart}
label="Favorers"
value={formatCount(shop.num_favorers)}
/>
<ShopMetric
icon={Star}
label="Rating"
value={formatRating(shop.review_average, shop.review_count)}
/>
</div>

{shop.url && (
<a
href={shop.url}
target="_blank"
rel="noreferrer"
className="text-primary inline-flex items-center gap-1 text-sm font-medium"
>
Open shop
<ExternalLink className="h-3.5 w-3.5" />
</a>
)}
</div>
),
};

const ShopMetric = ({
icon: Icon,
label,
value,
}: {
icon: React.ComponentType<{ className?: string }>;
label: string;
value: string;
}) => (
<div className="bg-muted/50 rounded-md p-2">
<div className="text-muted-foreground flex items-center gap-1.5 text-xs">
<Icon className="h-3.5 w-3.5" />
{label}
</div>
<div className="mt-1 font-medium">{value}</div>
</div>
);

const formatCount = (value?: number | null) => value?.toLocaleString() ?? "0";

const formatRating = (rating?: number | null, count?: number | null) => {
if (rating === undefined || rating === null) return "No reviews";

const reviewCount = count?.toLocaleString() ?? "0";

return `${rating.toFixed(1)} (${reviewCount})`;
};
36 changes: 36 additions & 0 deletions src/toolkits/toolkits/etsy/tools/get-shop-info/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Etsy } from "etsy-ts";

import type { ServerToolConfig } from "@/toolkits/types";
import type { getShopInfo } from "./base";

export const getShopInfoServerConfig = (
etsy: Etsy,
): ServerToolConfig<
typeof getShopInfo.inputSchema.shape,
typeof getShopInfo.outputSchema.shape
> => {
return {
callback: async () => {
try {
const user = await etsy.User.getMe();
const userId = user.data.user_id;

if (!userId) throw new Error("Missing Etsy user ID");

const shop = await etsy.Shop.getShopByOwnerUserId(userId);

if (!shop.data.shop_id) throw new Error("Missing Etsy shop ID");

return {
shop: shop.data,
};
} catch (error) {
console.error("Etsy API error:", error);
throw new Error("Failed to fetch shop info from Etsy");
}
},
message:
"Successfully retrieved the Etsy shop info. The user is shown the response in the UI. Do not reiterate it. " +
"If you called this tool because the user asked a question, answer the question.",
};
};
1 change: 1 addition & 0 deletions src/toolkits/toolkits/etsy/tools/tools.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export enum EtsyTools {
getListings = "get-listings",
getShopInfo = "get-shop-info",
}