Skip to content

Commit

Permalink
feat(sa): algolia search functions, move layout to root
Browse files Browse the repository at this point in the history
remove concierge mode

prompt adjustments
  • Loading branch information
ddaoxuan committed Jan 13, 2025
1 parent 102380a commit 3c9359b
Show file tree
Hide file tree
Showing 95 changed files with 1,539 additions and 823 deletions.
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
"use client"

import * as React from "react"

import { ShoppingBag, ShoppingCart, ArrowUpIcon } from "lucide-react"

import { ShoppingBag, ShoppingCart } from "lucide-react"
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "components/ui/sidebar"
import { Button } from "components/ui/button"
import { ScrollArea } from "components/ui/scroll-area"
import { Textarea } from "components/ui/textarea"
import { toast } from "sonner"
import { cn } from "utils/cn"
import { StopIcon } from "components/icons/stop-icon"
import { sanitizeUIMessages } from "lib/ai/chat"
import { MessageList } from "components/ui/message-list"
import { useAiCommerce } from "./ai-commerce-context"
import { Suggestions } from "./chat-suggestions"

export function AiCommerceSidebar({ handleSubmit, setInput, messages, isLoading, input, setMessages }) {
export function AiCommerceSidebar() {
const { handleSubmit, setInput, messages, isLoading, input } = useAiCommerce()
const textareaRef = React.useRef<HTMLTextAreaElement>(null)

const handleSubmitForm = () => {
handleSubmit()
const handleSubmitForm = (e: React.FormEvent<HTMLFormElement>) => {
handleSubmit(e)
}

const handleInput = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
Expand Down Expand Up @@ -51,69 +49,53 @@ export function AiCommerceSidebar({ handleSubmit, setInput, messages, isLoading,

return (
<Sidebar variant="inset">
<SidebarHeader className="p-4">
<h2 className="text-lg font-semibold text-sidebar-foreground">AI Shopping Assistant</h2>
<SidebarHeader className="border-b border-border px-4 py-2">
<SidebarMenu>
<SidebarMenuItem>
<div className="flex w-full items-center justify-center">
<span className="font-semibold">AI-Commerce</span>
</div>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<ScrollArea className="h-[calc(100vh-12rem)] px-4">
<ScrollArea className="px-4">
<div className="flex flex-col space-y-4">
<MessageList
messages={messages.filter((message) => !!message.content)}
messages={messages}
showTimeStamps={false}
isTyping={isLoading && messages[messages.length - 1].role === "user" && messages.length > 0}
messageOptions={{ animation: "scale", showTimeStamp: true, showToolMessages: false }}
messageOptions={{ animation: "scale", showTimeStamp: true }}
/>
</div>
</ScrollArea>
</SidebarContent>
<SidebarFooter className="p-4">
<div className="flex items-center space-x-2">
<Textarea
ref={textareaRef}
placeholder="Send a message..."
value={input}
onChange={handleInput}
className={cn("max-h-[calc(75dvh)] min-h-[24px] resize-none overflow-hidden rounded-xl bg-muted text-base")}
rows={3}
autoFocus
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault()
<form>
<div className="flex items-center space-x-2">
<Textarea
ref={textareaRef}
placeholder="Send a message..."
value={input}
onChange={handleInput}
className={cn("max-h-[calc(50dvh)] min-h-[24px] resize-none overflow-hidden rounded-xl bg-muted text-base")}
rows={3}
autoFocus
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault()

if (isLoading) {
toast.error("Please wait for the model to finish its response!")
} else {
handleSubmitForm()
if (isLoading) {
toast.error("Please wait for the model to finish its response!")
} else {
handleSubmitForm(event as unknown as React.FormEvent<HTMLFormElement>)
}
}
}
}}
/>
</div>
<div className="z-50">
{isLoading ? (
<Button
className="absolute bottom-2 right-2 m-0.5 h-fit rounded-full border p-1.5 dark:border-zinc-600"
onClick={(event) => {
event.preventDefault()
stop()
setMessages((messages) => sanitizeUIMessages(messages))
}}
>
<StopIcon size={20} />
</Button>
) : (
<Button
className="absolute bottom-2 right-2 m-0.5 h-fit rounded-full border p-1.5 dark:border-zinc-600"
onClick={(event) => {
event.preventDefault()
handleSubmitForm()
}}
disabled={input.length === 0}
>
<ArrowUpIcon size={14} />
</Button>
)}
</div>
/>
</div>
<Suggestions />
</form>
<SidebarMenu className="mt-4">
<SidebarMenuItem>
<SidebarMenuButton className="w-full justify-start text-sidebar-foreground transition-all hover:scale-105 hover:bg-sidebar-accent hover:text-secondary-foreground">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client"

import * as React from "react"
import type { ChatRequestOptions, CreateMessage, Message } from "ai"
import { useChat } from "ai/react"

interface AiCommerceContextType {
messages: Message[]
append: (message: Message | CreateMessage, chatRequestOptions?: ChatRequestOptions) => Promise<string | null | undefined>
input: string
isLoading: boolean
handleSubmit: (e?: { preventDefault: () => void }) => void
setInput: (value: string) => void
}

const AiCommerceContext = React.createContext<AiCommerceContextType | undefined>(undefined)

export function useAiCommerce() {
const context = React.useContext(AiCommerceContext)
if (!context) {
throw new Error("useAiCommerce must be used within an AiCommerceProvider")
}
return context
}

interface AiCommerceProviderProps {
children: React.ReactNode
}

export function AiCommerceProvider({ children }: AiCommerceProviderProps) {
const { append, messages, input, handleSubmit, setInput, isLoading } = useChat({
api: "/api/search",
maxSteps: 10,
})

const value = React.useMemo(
() => ({
messages,
input,
isLoading,
handleSubmit,
setInput,
append,
}),
[messages, input, isLoading, handleSubmit, setInput]
)

return <AiCommerceContext.Provider value={value}>{children}</AiCommerceContext.Provider>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useEffect, useState } from "react"
import { motion } from "framer-motion"
import { useChat } from "ai/react"
import { toast } from "sonner"
import { useAiCommerce } from "./ai-commerce-context"
import { PromptSuggestion, PromptSuggestions } from "components/ui/prompt-suggestions"

const INIT_SUGGESTIONS = ["Show me electronics under 250$", "Show me best-selling lips", "What are current products on sale?", "I'm looking for sportswear"]

export function Suggestions() {
const { isLoading, append, messages } = useAiCommerce()
const [currentSuggestions, setCurrentSuggestions] = useState<string[]>(INIT_SUGGESTIONS)
const { data: streamingData, append: appendSuggestions } = useChat({
api: "/api/suggestions",
})

useEffect(() => {
if (!streamingData) return

const newSuggestions = streamingData.filter((message: any) => message.type === "suggestion" && message.content.content).map((message: any) => message.content.content)

if (newSuggestions.length > 0) {
// Start from an empty array and take up to 5 new suggestions
setCurrentSuggestions(newSuggestions.slice(newSuggestions.length - 5))
}
}, [streamingData])

const handleClick = (suggestion: string) => {
if (isLoading) {
toast.error("Please wait for the model to finish its response!")
return
}

append({ role: "user", content: suggestion })
appendSuggestions({
role: "user",
content: messages
.filter((message) => message.role === "user")
.map((message) => message.content)
.join(""),
})
// Clear current suggestions after clicking
setCurrentSuggestions([])
}

return (
<PromptSuggestions className="my-4">
{currentSuggestions.map((suggestion, index) => (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 20 }} transition={{ delay: 0.1 * index }} key={`suggestion-${index}`}>
<PromptSuggestion disabled={isLoading} value={suggestion} onClick={() => handleClick(suggestion)}>
{suggestion}
</PromptSuggestion>
</motion.div>
))}
</PromptSuggestions>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Metadata } from "next"
import { CategoryView } from "components/category/category-view"

export const revalidate = 86400
export const dynamic = "force-static"

interface CategoryPageProps {
params: Promise<{ slug: string; page: string }>
}

export async function generateMetadata(props: CategoryPageProps): Promise<Metadata> {
const params = await props.params
return {
title: `${params.slug} | Enterprise Commerce`,
description: "In excepteur elit mollit in.",
}
}

export async function generateStaticParams() {
return []
}

export default async function CategoryPage(props: CategoryPageProps) {
const params = await props.params
return <CategoryView searchParams={{ page: params.page }} params={params} basePath="ai" />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Metadata } from "next"
import { isDemoMode } from "utils/demo-utils"
import { getCategories } from "lib/algolia"
import { CategoryView } from "components/category/category-view"

export const revalidate = 86400
export const dynamic = "force-static"

interface CategoryPageProps {
params: Promise<{ slug: string }>
}

export async function generateMetadata(props: CategoryPageProps): Promise<Metadata> {
const params = await props.params
return {
title: `${params.slug} | Enterprise Commerce`,
description: "In excepteur elit mollit in.",
}
}

export async function generateStaticParams() {
if (isDemoMode()) return []

const { hits } = await getCategories({
hitsPerPage: 50,
attributesToRetrieve: ["handle"],
})

return hits.map(({ handle }) => ({ slug: handle }))
}

export default async function CategoryPage(props: CategoryPageProps) {
const params = await props.params
return <CategoryView params={params} basePath="ai" />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Metadata } from "next"
import { SearchParamsType } from "types"
import { CategoryView } from "components/category/category-view"

export const runtime = "nodejs"

export const revalidate = 86400

interface ProductListingPageProps {
searchParams: Promise<SearchParamsType>
params: Promise<{ slug: string }>
}

export async function generateMetadata(props: ProductListingPageProps): Promise<Metadata> {
const params = await props.params
return {
title: `${params.slug} | Enterprise Commerce`,
description: "In excepteur elit mollit in.",
}
}

export default async function ProductListingPage(props: ProductListingPageProps) {
const params = await props.params
const searchParams = await props.searchParams
return <CategoryView params={params} searchParams={searchParams} basePath="ai" />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { PageSkeleton } from "components/product/page-skeleton"

export default function Loading() {
return <PageSkeleton />
}
Loading

0 comments on commit 3c9359b

Please sign in to comment.