Template
Add AI store assistant with catalogue tool calls
- /api/chat streams via AI SDK v7 through OpenRouter (OPENROUTER_API_KEY), with a store-specific system prompt and five read-only tools: searchCatalogue, getProductDetails, listCollections, getCollectionProducts, browseProducts - Sidebar assistant on the right that opens from a launcher and expands, built on ai-elements (conversation, message, prompt-input, tool) with shimmer on in-flight tool calls - Extract services/shopify/catalog.ts so the Storefront fetchers have no React imports and can run in a Route Handler; the client hooks now re-export from it - ai-elements pulled in the canonical shadcn primitives, replacing the hand-rolled command/dialog/button variants; search dialog moved to the cmdk-based Command and CommandDialog forwards shouldFilter - Pin shiki to ^3.19.0 to match streamdown and drop the duplicate copy - Add .env.example documenting the required env vars Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jWbNNJLksC1QG8z8845FX
This commit is contained in:
co-authored by
Claude Opus 5
parent
2ef5639a2e
commit
107959a4c3
@@ -12,7 +12,7 @@ import {
|
||||
} from '@/components/ui/command';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Loader } from '@/app/components/ui/loader';
|
||||
import { RiSearchLine, RiImageLine } from '@remixicon/react';
|
||||
import { RiSearchLine, RiImageLine, RiCloseLine } from '@remixicon/react';
|
||||
import {
|
||||
searchSuggestions,
|
||||
type SearchSuggestion,
|
||||
@@ -91,23 +91,51 @@ const SearchDialog: React.FC = () => {
|
||||
<RiSearchLine className="size-5" />
|
||||
</Button>
|
||||
|
||||
<CommandDialog open={open} onOpenChange={(next) => !next && close()}>
|
||||
<CommandInput
|
||||
autoFocus
|
||||
value={term}
|
||||
onValueChange={setTerm}
|
||||
onClear={() => setTerm('')}
|
||||
onClose={close}
|
||||
placeholder="Search products"
|
||||
aria-label="Search products"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') goToSearchPage();
|
||||
}}
|
||||
/>
|
||||
<CommandDialog
|
||||
open={open}
|
||||
onOpenChange={(next) => !next && close()}
|
||||
title="Search"
|
||||
description="Search products"
|
||||
showCloseButton={false}
|
||||
className="top-24 max-w-xl translate-y-0"
|
||||
// Results come from the Storefront API, so cmdk must not re-filter them.
|
||||
shouldFilter={false}
|
||||
>
|
||||
<div className="relative">
|
||||
<CommandInput
|
||||
value={term}
|
||||
onValueChange={setTerm}
|
||||
placeholder="Search products"
|
||||
className="pr-28"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') goToSearchPage();
|
||||
}}
|
||||
/>
|
||||
<div className="absolute right-2 top-0 flex h-9 items-center gap-1">
|
||||
{hasQuery && (
|
||||
<Button
|
||||
onClick={() => setTerm('')}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="font-normal text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={close}
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Close search"
|
||||
>
|
||||
<RiCloseLine className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasQuery && (
|
||||
<>
|
||||
<div className="px-4 pb-2">
|
||||
<div className="px-3 pt-3">
|
||||
<button
|
||||
onClick={goToSearchPage}
|
||||
className="rounded-full border border-border px-3 py-1 text-sm text-foreground transition-colors hover:border-foreground"
|
||||
@@ -116,7 +144,7 @@ const SearchDialog: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<CommandList>
|
||||
<CommandList className="max-h-80">
|
||||
{searching && results.length === 0 ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<Loader size={20} />
|
||||
@@ -128,7 +156,9 @@ const SearchDialog: React.FC = () => {
|
||||
{results.map((product) => (
|
||||
<CommandItem
|
||||
key={product.id}
|
||||
onClick={() => goToProduct(product.handle)}
|
||||
value={product.handle}
|
||||
onSelect={() => goToProduct(product.handle)}
|
||||
className="gap-3 py-2"
|
||||
>
|
||||
<div className="h-14 w-14 shrink-0 overflow-hidden bg-zinc-100">
|
||||
{product.featuredImage ? (
|
||||
@@ -139,7 +169,7 @@ const SearchDialog: React.FC = () => {
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center text-zinc-400">
|
||||
<RiImageLine size={18} />
|
||||
<RiImageLine className="size-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user