chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
CopilotRuntime,
|
||||
OpenAIAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
const serviceAdapter = new OpenAIAdapter({});
|
||||
const runtime = new CopilotRuntime();
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
|
||||
runtime,
|
||||
serviceAdapter,
|
||||
endpoint: "/api/copilotkit",
|
||||
});
|
||||
|
||||
return handleRequest(req);
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,41 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.copilotKitChat {
|
||||
background: radial-gradient(circle at center, white, white) !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.copilotKitChat::before {
|
||||
content: "" !important;
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='rgba(244, 114, 182, 0.11)' fill-rule='evenodd'%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/g%3E%3C/svg%3E") !important;
|
||||
mask-image: radial-gradient(
|
||||
circle at center,
|
||||
black 40%,
|
||||
transparent 65%
|
||||
) !important;
|
||||
-webkit-mask-image: radial-gradient(
|
||||
circle at center,
|
||||
black 40%,
|
||||
transparent 65%
|
||||
) !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.copilotKitMessages {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.copilotKitResponseButton {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.copilotKitInputControls svg {
|
||||
stroke: rgb(244, 114, 182);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import "./globals.css";
|
||||
import { CopilotKit } from "@copilotkit/react-core";
|
||||
import { GlobalStateProvider } from "@/lib/stages";
|
||||
import { CarSalesChat } from "@/components/car-sales-chat";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<CopilotKit runtimeUrl="/api/copilotkit" showDevConsole={false}>
|
||||
<GlobalStateProvider>
|
||||
<div className="h-screen w-screen grid grid-cols-[40fr,60fr] p-10 gap-5">
|
||||
<div className="overflow-y-auto rounded-xl border">
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex justify-center items-center overflow-y-auto rounded-xl">
|
||||
<CarSalesChat className="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</GlobalStateProvider>
|
||||
</CopilotKit>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { OrderCards } from "@/components/order-cards";
|
||||
import { StateVisualizer } from "@/components/state-visualizer";
|
||||
import { Tabs } from "@/components/tabs";
|
||||
import { useGlobalState } from "@/lib/stages";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function Home() {
|
||||
const { orders } = useGlobalState();
|
||||
const [activeTab, setActiveTab] = useState<string>("orders");
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: "orders",
|
||||
label: "Orders",
|
||||
content: <OrderCards orders={orders} />,
|
||||
},
|
||||
{
|
||||
id: "visualizer",
|
||||
label: "State Visualizer",
|
||||
content: <StateVisualizer />,
|
||||
},
|
||||
];
|
||||
|
||||
return <Tabs tabs={tabs} activeTab={activeTab} onTabChange={setActiveTab} />;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
import { RenderFunctionStatus } from "@copilotkit/react-core";
|
||||
import { AnimatePresence } from "motion/react";
|
||||
import * as motion from "motion/react-client";
|
||||
|
||||
interface AnimatedCardProps {
|
||||
children: React.ReactNode;
|
||||
status: RenderFunctionStatus;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AnimatedCard({ children, className }: AnimatedCardProps) {
|
||||
const divStyles = cn(
|
||||
"flex flex-col gap-2 shadow-md shadow-pink-300/50 rounded-2xl border border-pink-300 max-w-md my-4 p-8",
|
||||
className,
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
key="animated-card"
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
scale: { type: "spring", damping: 20, stiffness: 150 },
|
||||
}}
|
||||
className={divStyles}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
import {
|
||||
useStageBuildCar,
|
||||
useStageGetContactInfo,
|
||||
useStageGetPaymentInfo,
|
||||
useStageConfirmOrder,
|
||||
useStageSellFinancing,
|
||||
useStageGetFinancingInfo,
|
||||
} from "@/lib/stages";
|
||||
|
||||
import { useCopilotChat } from "@copilotkit/react-core";
|
||||
import { TextMessage, MessageRole } from "@copilotkit/runtime-client-gql";
|
||||
import { CopilotChat } from "@copilotkit/react-ui";
|
||||
import "@copilotkit/react-ui/styles.css";
|
||||
import { UserMessage, AssistantMessage } from "./chat-message";
|
||||
|
||||
export interface ChatProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CarSalesChat({ className }: ChatProps) {
|
||||
const { appendMessage, isLoading } = useCopilotChat();
|
||||
const [initialMessageSent, setInitialMessageSent] = useState(false);
|
||||
|
||||
// Add the stages of the state machine
|
||||
useStageGetContactInfo();
|
||||
useStageBuildCar();
|
||||
useStageSellFinancing();
|
||||
useStageGetPaymentInfo();
|
||||
useStageGetFinancingInfo();
|
||||
useStageConfirmOrder();
|
||||
|
||||
// Render an initial message when the chat is first loaded
|
||||
useEffect(() => {
|
||||
if (initialMessageSent || isLoading) return;
|
||||
|
||||
setTimeout(() => {
|
||||
appendMessage(
|
||||
new TextMessage({
|
||||
content:
|
||||
"Hi, I'm Fio, your AI car salesman. First, let's get your contact information before we get started.",
|
||||
role: MessageRole.Assistant,
|
||||
}),
|
||||
);
|
||||
setInitialMessageSent(true);
|
||||
}, 500);
|
||||
}, [initialMessageSent, appendMessage, isLoading]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col h-full max-h-full w-full rounded-xl shadow-sm border border-neutral-200",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex-1 w-full rounded-xl overflow-y-auto")}>
|
||||
<CopilotChat
|
||||
className="h-full w-full"
|
||||
instructions={systemPrompt}
|
||||
UserMessage={UserMessage}
|
||||
AssistantMessage={AssistantMessage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const systemPrompt = `
|
||||
GOAL
|
||||
You are trying to help the user purchase a car. The user will be going through a series of stages to accomplish this goal. Please help
|
||||
them through the process with their tools and data keeping in mind the current stage of the interaction. Do not proceed to the next
|
||||
stage until the current stage is complete. You must take each stage one at a time, do not skip any stages.
|
||||
|
||||
BACKGROUND
|
||||
You are built by CopilotKit, an open-source framework for building agentic applications.
|
||||
|
||||
DETAILS
|
||||
You will be going through a series of stages to sell a car. Each stage will have its own unique instructions, tools and data. Please evaluate your current stage
|
||||
before responding. Any additional instructions provided in the stage should be followed with the highest priority. DO NOT RESPOND WITH DATA YOU DO NOT HAVE ACCESS TO.
|
||||
If you cannot perform an action, do not attempt to perform it, just let the know that they cannot do that and reiterate the instructions for the current stage.
|
||||
|
||||
NOTICES
|
||||
- DO NOT mention the word "stage" or "state" in your responses.
|
||||
- DO NOT mention the word "state machine" in your responses.
|
||||
- DO NOT offer to let the user test drive the car.
|
||||
`;
|
||||
@@ -0,0 +1,122 @@
|
||||
import { UserMessageProps, AssistantMessageProps } from "@copilotkit/react-ui";
|
||||
import { Markdown } from "@copilotkit/react-ui";
|
||||
|
||||
function normalizeMarkdownContent(content: unknown): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (content == null) return "";
|
||||
if (Array.isArray(content))
|
||||
return content.map((c) => normalizeMarkdownContent(c)).join("");
|
||||
|
||||
if (typeof content === "object") {
|
||||
try {
|
||||
return JSON.stringify(content);
|
||||
} catch {
|
||||
return String(content);
|
||||
}
|
||||
}
|
||||
|
||||
return String(content);
|
||||
}
|
||||
|
||||
export function UserMessage({ message }: UserMessageProps) {
|
||||
const content = normalizeMarkdownContent(message?.content);
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-4 px-6 py-4 flex-row-reverse">
|
||||
{/* Avatar */}
|
||||
<div className="shrink-0 w-10 h-10 rounded-xl overflow-hidden border-2 border-neutral-200 bg-white">
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-primary" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M17.5 21.0001H6.5C5.11929 21.0001 4 19.8808 4 18.5001C4 14.4194 10 14.5001 12 14.5001C14 14.5001 20 14.4194 20 18.5001C20 19.8808 18.8807 21.0001 17.5 21.0001Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div className="relative py-2 px-4 rounded-2xl rounded-tr-sm max-w-[80%] text-sm leading-relaxed bg-white border border-neutral-200 shadow-sm">
|
||||
<div className="font-medium text-blue-600 mb-1">You</div>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AssistantMessage({
|
||||
message,
|
||||
subComponent,
|
||||
isLoading,
|
||||
}: AssistantMessageProps) {
|
||||
const content = normalizeMarkdownContent(message?.content);
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-4 px-6 py-4">
|
||||
{/* Avatar */}
|
||||
<div className="shrink-0 w-10 h-10 rounded-xl overflow-hidden border-2 border-neutral-200 bg-white">
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<svg
|
||||
className="w-6 h-6 text-pink-600"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M12 4L14 6H18C19.1046 6 20 6.89543 20 8V17C20 18.1046 19.1046 19 18 19H6C4.89543 19 4 18.1046 4 17V8C4 6.89543 4.89543 6 6 6H10L12 4Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9 14C9 14 10 15 12 15C14 15 15 14 15 14"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9 11H9.01"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M15 11H15.01"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
{(message || isLoading) && (
|
||||
<div className="relative py-2 px-4 rounded-2xl rounded-tl-sm max-w-[80%] text-sm leading-relaxed bg-white border border-neutral-200 shadow-sm">
|
||||
<div className="font-medium text-pink-600 mb-1">Fio</div>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2 p-1">
|
||||
<div className="w-2 h-2 bg-pink-600 rounded-full animate-bounce [animation-delay:-0.3s]"></div>
|
||||
<div className="w-2 h-2 bg-pink-600 rounded-full animate-bounce [animation-delay:-0.15s]"></div>
|
||||
<div className="w-2 h-2 bg-pink-600 rounded-full animate-bounce"></div>
|
||||
</div>
|
||||
) : (
|
||||
<>{content && <Markdown content={content} />}</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{subComponent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import React from "react";
|
||||
import { AnimatedCard } from "@/components/animated-card";
|
||||
import { useGlobalState } from "@/lib/stages";
|
||||
import { Car, CardInfo, ContactInfo, FinancingInfo, Order } from "@/lib/types";
|
||||
|
||||
import { RenderFunctionStatus } from "@copilotkit/react-core";
|
||||
|
||||
interface ConfirmOrderProps {
|
||||
onConfirm: (order: Order) => void;
|
||||
onCancel: () => void;
|
||||
status: RenderFunctionStatus;
|
||||
}
|
||||
|
||||
export const ConfirmOrder = ({
|
||||
onConfirm,
|
||||
onCancel,
|
||||
status,
|
||||
}: ConfirmOrderProps) => {
|
||||
const { selectedCar, contactInfo, cardInfo, financingInfo } =
|
||||
useGlobalState();
|
||||
|
||||
return (
|
||||
<AnimatedCard className="w-[500px]" status={status}>
|
||||
<h2 className="text-2xl font-bold mb-4 text-gray-800">Order Summary</h2>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center border-b border-blue-100 pb-2">
|
||||
<span className="font-medium">Vehicle</span>
|
||||
<span className="text-gray-600">
|
||||
{selectedCar?.year} {selectedCar?.make} {selectedCar?.model}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center border-b border-blue-100 pb-2">
|
||||
<span className="font-medium">Price</span>
|
||||
<span className="text-gray-600">
|
||||
${selectedCar?.price?.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center border-b border-blue-100 pb-2">
|
||||
<span className="font-medium">Customer</span>
|
||||
<span className="text-gray-600">{contactInfo?.name}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center border-b border-blue-100 pb-2">
|
||||
<span className="font-medium">Payment</span>
|
||||
<span className="text-gray-600">
|
||||
{cardInfo?.type} ****{cardInfo?.cardNumber?.slice(-4)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium">Financing</span>
|
||||
<span className="text-gray-600">
|
||||
{financingInfo?.loanTerm} months
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{status !== "complete" && (
|
||||
<ActionButtons
|
||||
onConfirm={() =>
|
||||
onConfirm({
|
||||
car: selectedCar || ({} as Car),
|
||||
contactInfo: contactInfo || ({} as ContactInfo),
|
||||
cardInfo: cardInfo || ({} as CardInfo),
|
||||
financingInfo: financingInfo || ({} as FinancingInfo),
|
||||
paymentType: cardInfo ? "card" : "financing",
|
||||
})
|
||||
}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
)}
|
||||
</AnimatedCard>
|
||||
);
|
||||
};
|
||||
|
||||
const ActionButtons = ({
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: {
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}) => (
|
||||
<div className="flex justify-end gap-4 mt-6">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="px-6 py-2.5 w-full text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-100 transition-colors duration-200 font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className="px-6 py-2.5 w-full text-white bg-pink-600 rounded-lg hover:bg-pink-800 transition-colors duration-200 font-medium"
|
||||
>
|
||||
Confirm Order
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useState } from "react";
|
||||
import { AnimatedCard } from "@/components/animated-card";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
|
||||
import { RenderFunctionStatus } from "@copilotkit/react-core";
|
||||
|
||||
interface ContactInfoProps {
|
||||
onSubmit: (name: string, email: string, phone: string) => void;
|
||||
status: RenderFunctionStatus;
|
||||
}
|
||||
|
||||
export function ContactInfo({ onSubmit, status }: ContactInfoProps) {
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
const inputProps = (value: string, setter: (value: string) => void) => ({
|
||||
value,
|
||||
className:
|
||||
"border border-gray-300 focus:ring-2 focus:ring-pink-500 focus:outline-none rounded-md p-2 disabled:bg-white disabled:cursor-not-allowed disabled:text-gray-500",
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setter(e.target.value),
|
||||
disabled: isSubmitted,
|
||||
});
|
||||
|
||||
return (
|
||||
<AnimatedCard status={status}>
|
||||
<h1 className="text-2xl text-center font-semibold antialiased">
|
||||
Contact Information
|
||||
</h1>
|
||||
<h2 className="text-center text-base text-gray-400 antialiased">
|
||||
We need this information in order to process your order and contact you
|
||||
if there are any issues.
|
||||
</h2>
|
||||
<hr className="border-pink-300 mt-4 mb-4" />
|
||||
|
||||
<input type="text" placeholder="Name" {...inputProps(name, setName)} />
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
{...inputProps(email, setEmail)}
|
||||
/>
|
||||
<input type="tel" placeholder="Phone" {...inputProps(phone, setPhone)} />
|
||||
|
||||
<AnimatePresence>
|
||||
{!isSubmitted && (
|
||||
<motion.button
|
||||
className="bg-pink-600 hover:bg-pink-800 transition-colors duration-300 text-white px-4 py-2 my-4 rounded-md"
|
||||
onClick={() => {
|
||||
setIsSubmitted(true);
|
||||
onSubmit(name, email, phone);
|
||||
}}
|
||||
exit={{ opacity: 0, scale: 0, height: 0, margin: 0, padding: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
Submit
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</AnimatedCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useState } from "react";
|
||||
import { AnimatedCard } from "@/components/animated-card";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { RenderFunctionStatus } from "@copilotkit/react-core";
|
||||
|
||||
interface FinancingFormProps {
|
||||
onSubmit: (creditScore: string, loanTerm: string) => void;
|
||||
status: RenderFunctionStatus;
|
||||
}
|
||||
|
||||
export function FinancingForm({ onSubmit, status }: FinancingFormProps) {
|
||||
const [creditScore, setCreditScore] = useState("700-749");
|
||||
const [loanTerm, setLoanTerm] = useState("60");
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
const selectProps = (value: string, setter: (value: string) => void) => ({
|
||||
value,
|
||||
className:
|
||||
"border border-gray-300 focus:ring-2 focus:ring-pink-500 focus:outline-none rounded-md p-2 disabled:bg-white disabled:cursor-not-allowed disabled:text-gray-500",
|
||||
onChange: (e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setter(e.target.value),
|
||||
disabled: isSubmitted,
|
||||
});
|
||||
|
||||
return (
|
||||
<AnimatedCard status={status}>
|
||||
<h1 className="text-2xl text-center font-semibold antialiased">
|
||||
Financing Information
|
||||
</h1>
|
||||
<h2 className="text-center text-base text-gray-400 antialiased">
|
||||
Please provide your financial information to process your financing
|
||||
application.
|
||||
</h2>
|
||||
<hr className="border-pink-300 mt-4 mb-4" />
|
||||
|
||||
<select {...selectProps(creditScore, setCreditScore)}>
|
||||
<option value="750+">Excellent (750+)</option>
|
||||
<option value="700-749">Good (700-749)</option>
|
||||
<option value="650-699">Fair (650-699)</option>
|
||||
<option value="600-649">Poor (600-649)</option>
|
||||
<option value="<600">Very Poor (below 600)</option>
|
||||
</select>
|
||||
|
||||
<select {...selectProps(loanTerm, setLoanTerm)}>
|
||||
<option value="36">36 Months</option>
|
||||
<option value="48">48 Months</option>
|
||||
<option value="60">60 Months</option>
|
||||
<option value="72">72 Months</option>
|
||||
</select>
|
||||
|
||||
<AnimatePresence>
|
||||
{!isSubmitted && (
|
||||
<motion.button
|
||||
className="bg-pink-500 hover:bg-pink-700 transition-colors duration-300 text-white px-4 py-2 my-4 rounded-md"
|
||||
onClick={() => {
|
||||
setIsSubmitted(true);
|
||||
onSubmit(creditScore, loanTerm);
|
||||
}}
|
||||
exit={{ opacity: 0, scale: 0, height: 0, margin: 0, padding: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
Submit
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</AnimatedCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useState } from "react";
|
||||
import { motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
import { availableCardInfo, CardInfo } from "@/lib/types";
|
||||
|
||||
interface PaymentCardsProps {
|
||||
onSubmit: (cardInfo: CardInfo) => void;
|
||||
}
|
||||
|
||||
export function PaymentCards({ onSubmit }: PaymentCardsProps) {
|
||||
const [selectedCard, setSelectedCard] = useState<string | null>(null);
|
||||
|
||||
const handleCardSelect = (cardInfo: CardInfo) => {
|
||||
setSelectedCard(cardInfo.name);
|
||||
onSubmit(cardInfo);
|
||||
};
|
||||
|
||||
const renderCard = (cardInfo: CardInfo, index: number) => (
|
||||
<motion.div
|
||||
key={cardInfo.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<CreditCard
|
||||
cardInfo={cardInfo}
|
||||
onClick={() => handleCardSelect(cardInfo)}
|
||||
isAnySelected={selectedCard !== null}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{!selectedCard && (
|
||||
<h1 className="text-2xl font-bold mb-2">Your payment methods</h1>
|
||||
)}
|
||||
<div className="flex flex-row overflow-x-auto gap-4 py-4 w-full min-w-0 pb-6">
|
||||
{availableCardInfo
|
||||
.filter(
|
||||
(cardInfo) =>
|
||||
selectedCard === null || selectedCard === cardInfo.name,
|
||||
)
|
||||
.map(renderCard)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Separate component for credit card display
|
||||
const CreditCard = ({
|
||||
cardInfo,
|
||||
onClick,
|
||||
isAnySelected,
|
||||
}: {
|
||||
cardInfo: CardInfo;
|
||||
onClick: () => void;
|
||||
isAnySelected: boolean;
|
||||
}) => {
|
||||
const cardClassName = cn(
|
||||
"w-[350px] h-[200px] rounded-xl p-6 relative overflow-hidden transition-all duration-300",
|
||||
!isAnySelected
|
||||
? "hover:transform hover:-translate-y-2 hover:shadow-xl cursor-pointer"
|
||||
: "cursor-not-allowed",
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cardClassName}
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #000428 0%, #004e92 100%)",
|
||||
}}
|
||||
>
|
||||
<div className="text-white space-y-4">
|
||||
{/* Card Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-xl font-bold">{cardInfo.name}</div>
|
||||
<div className="text-2xl">💳</div>
|
||||
</div>
|
||||
|
||||
{/* Card Number */}
|
||||
<div className="text-2xl tracking-wider flex justify-center w-full font-mono mt-8 mb-4">
|
||||
**** **** **** {cardInfo?.cardNumber?.slice(-4)}
|
||||
</div>
|
||||
|
||||
{/* Card Footer */}
|
||||
<div className="flex justify-between items-end mt-8">
|
||||
<div>
|
||||
<div className="text-xs opacity-80">VALID THRU</div>
|
||||
<div>{cardInfo.cardExpiration}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs opacity-80">TYPE</div>
|
||||
<div>{cardInfo.type}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
import { Car } from "@/lib/types";
|
||||
import { AnimatedCard } from "@/components/animated-card";
|
||||
import { motion } from "motion/react";
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
import { RenderFunctionStatus } from "@copilotkit/react-core";
|
||||
|
||||
interface ShowCarProps {
|
||||
car: Car;
|
||||
onSelect: () => void;
|
||||
onReject?: () => void;
|
||||
status: RenderFunctionStatus;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ColorDisplay = ({ color }: { color?: string }) => {
|
||||
if (!color) return null;
|
||||
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
<span
|
||||
className="w-5 h-5 rounded-full border border-gray-200 shadow-sm"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<span className="text-gray-600 text-sm">{color}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const CarImage = ({ car }: { car: Car }) => {
|
||||
return (
|
||||
<div className="relative aspect-[3/3] w-full overflow-hidden h-[250px]">
|
||||
<Image
|
||||
width={300}
|
||||
height={250}
|
||||
src={car?.image?.src || ""}
|
||||
alt={car?.image?.alt || ""}
|
||||
className="object-cover w-full h-full hover:scale-105 transition-transform duration-300 transform-gpu"
|
||||
style={{
|
||||
imageRendering: "auto",
|
||||
WebkitFontSmoothing: "antialiased",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function ShowCar({
|
||||
car,
|
||||
onSelect,
|
||||
onReject,
|
||||
status,
|
||||
className,
|
||||
}: ShowCarProps) {
|
||||
const carDetails = [
|
||||
{ label: "Make", value: car.make },
|
||||
{ label: "Model", value: car.model },
|
||||
{ label: "Year", value: car.year },
|
||||
{ label: "Color", value: <ColorDisplay color={car.color} /> },
|
||||
{ label: "Price", value: `$${car.price?.toLocaleString()}`, bold: true },
|
||||
];
|
||||
|
||||
const cardStyles = cn(
|
||||
"min-w-[300px] max-w-sm bg-white rounded-xl overflow-hidden p-0 gap-0",
|
||||
className,
|
||||
);
|
||||
const informationWrapperStyles = cn("space-y-6 pt-4 pb-4");
|
||||
const acceptButtonStyles = cn(
|
||||
"flex-1 bg-pink-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-pink-700 transition-all duration-200 shadow-sm hover:shadow-md",
|
||||
);
|
||||
const rejectButtonStyles = cn(
|
||||
"flex-1 bg-gray-50 text-gray-700 px-6 py-3 rounded-lg font-medium hover:bg-gray-100 transition-all duration-200",
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatedCard status={status} className={cardStyles}>
|
||||
<CarImage car={car} />
|
||||
|
||||
<div className={informationWrapperStyles}>
|
||||
<div className="space-y-2 px-6">
|
||||
<div className="text-2xl font-semibold text-gray-900">
|
||||
{car.year} {car.make} {car.model}
|
||||
</div>
|
||||
{carDetails.map(({ label, value, bold }) => (
|
||||
<div key={label} className="flex justify-between items-center py-1">
|
||||
<span className="text-gray-500 text-sm">{label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-gray-900",
|
||||
bold ? "font-semibold text-lg" : "text-sm",
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"px-6 pt-2",
|
||||
status === "complete" ? "hidden" : "animate-fade-in",
|
||||
)}
|
||||
>
|
||||
<hr className="mb-4 border-gray-100" />
|
||||
<div className="flex gap-3">
|
||||
{onReject && (
|
||||
<button className={rejectButtonStyles} onClick={onReject}>
|
||||
Other options
|
||||
</button>
|
||||
)}
|
||||
<button className={acceptButtonStyles} onClick={onSelect}>
|
||||
Select
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedCard>
|
||||
);
|
||||
}
|
||||
|
||||
interface ShowCarsProps {
|
||||
cars: Car[];
|
||||
onSelect: (car: Car) => void;
|
||||
status: RenderFunctionStatus;
|
||||
}
|
||||
|
||||
export function ShowCars({ cars, onSelect, status }: ShowCarsProps) {
|
||||
const [selectedCar, setSelectedCar] = useState<Car | null>(null);
|
||||
|
||||
const handleSelect = (car: Car) => {
|
||||
setSelectedCar(car);
|
||||
onSelect(car);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row overflow-x-auto gap-4 py-4 space-x-6">
|
||||
{cars.map((car, index) => {
|
||||
// Don't render if there's a selected car and this isn't it
|
||||
if (selectedCar && car !== selectedCar) return null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.2 }}
|
||||
>
|
||||
<ShowCar
|
||||
car={car}
|
||||
onSelect={() => handleSelect(car)}
|
||||
status={status}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Order } from "@/lib/types";
|
||||
import Image from "next/image";
|
||||
|
||||
interface OrderCardsProps {
|
||||
orders: Order[];
|
||||
}
|
||||
|
||||
export function OrderCard({ order, index }: { order: Order; index: number }) {
|
||||
return (
|
||||
<div className="group bg-white shadow-sm hover:shadow transition-all duration-200 rounded-lg border border-neutral-200">
|
||||
<div className="flex items-start gap-4 p-4">
|
||||
{/* Car Image */}
|
||||
<div className="relative w-[140px] shrink-0">
|
||||
<div className="aspect-[4/3] rounded-md overflow-hidden bg-neutral-100">
|
||||
<Image
|
||||
width={280}
|
||||
height={210}
|
||||
src={order.car.image?.src || ""}
|
||||
alt={order.car.image?.alt || ""}
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute top-2 left-2">
|
||||
<div className="text-xs font-medium bg-white shadow-sm text-neutral-900 px-2 py-1 rounded">
|
||||
#{String(index + 1).padStart(3, "0")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="font-medium text-neutral-900 mb-1">
|
||||
{order.car.year} {order.car.make} {order.car.model}
|
||||
</h3>
|
||||
<div className="text-sm text-neutral-500 flex items-center">
|
||||
<svg
|
||||
className="w-4 h-4 mr-1.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
{order.contactInfo.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<div className="text-lg font-medium text-neutral-900 mb-1">
|
||||
${order.car.price?.toLocaleString()}
|
||||
</div>
|
||||
{order.paymentType === "card" ? (
|
||||
<div className="text-sm text-neutral-500 flex items-center justify-end">
|
||||
<svg
|
||||
className="w-4 h-4 mr-1.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
|
||||
/>
|
||||
</svg>
|
||||
•••• {order.cardInfo?.cardNumber?.slice(-4)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-neutral-500 flex items-center justify-end">
|
||||
<svg
|
||||
className="w-4 h-4 mr-1.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{order.financingInfo?.loanTerm}mo
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OrderCards({ orders }: OrderCardsProps) {
|
||||
if (!orders.length) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full p-6">
|
||||
<div className="w-16 h-16 mb-4 relative">
|
||||
<svg
|
||||
className="w-full h-full text-neutral-200"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute -bottom-2 -right-2 w-8 h-8 bg-neutral-900 rounded-full flex items-center justify-center text-white font-bold text-sm shadow-md">
|
||||
FIO
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-neutral-900 font-medium mb-1">
|
||||
No Vehicle Orders Yet
|
||||
</h3>
|
||||
<p className="text-neutral-500 text-sm text-center max-w-[240px] mb-4">
|
||||
Ready to find your perfect vehicle? Start a conversation with Fio,
|
||||
your personal sales assistant.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
/* This could trigger the chat focus */
|
||||
}}
|
||||
className="inline-flex items-center px-4 py-2 bg-neutral-900 text-white text-sm font-medium rounded-md hover:bg-neutral-800 transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
Chat with Fio
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{orders.map((order, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2, delay: index * 0.05 }}
|
||||
>
|
||||
<OrderCard order={order} index={index} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import ReactFlow, { Node, Edge, Position, MarkerType } from "reactflow";
|
||||
import "reactflow/dist/style.css";
|
||||
import { useGlobalState } from "@/lib/stages";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export function StateVisualizer() {
|
||||
const { stage } = useGlobalState();
|
||||
|
||||
const activeNodeStyles = "ring-4 ring-pink-400 animate-pulse";
|
||||
const inactiveNodeStyles = "border border-gray-200";
|
||||
|
||||
const nodes: Node[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "getContactInfo",
|
||||
data: { label: "Contact Info" },
|
||||
position: { x: 250, y: 0 },
|
||||
className:
|
||||
stage === "getContactInfo" ? activeNodeStyles : inactiveNodeStyles,
|
||||
sourcePosition: Position.Bottom,
|
||||
targetPosition: Position.Top,
|
||||
},
|
||||
{
|
||||
id: "buildCar",
|
||||
data: { label: "Build Car" },
|
||||
position: { x: 250, y: 100 },
|
||||
className: stage === "buildCar" ? activeNodeStyles : inactiveNodeStyles,
|
||||
sourcePosition: Position.Bottom,
|
||||
targetPosition: Position.Top,
|
||||
},
|
||||
{
|
||||
id: "sellFinancing",
|
||||
data: { label: "Sell Financing" },
|
||||
position: { x: 250, y: 200 },
|
||||
className:
|
||||
stage === "sellFinancing" ? activeNodeStyles : inactiveNodeStyles,
|
||||
sourcePosition: Position.Bottom,
|
||||
targetPosition: Position.Top,
|
||||
},
|
||||
{
|
||||
id: "getPaymentInfo",
|
||||
data: { label: "Payment Info" },
|
||||
position: { x: 150, y: 300 },
|
||||
className:
|
||||
stage === "getPaymentInfo" ? activeNodeStyles : inactiveNodeStyles,
|
||||
sourcePosition: Position.Bottom,
|
||||
targetPosition: Position.Top,
|
||||
},
|
||||
{
|
||||
id: "getFinancingInfo",
|
||||
data: { label: "Financing Info" },
|
||||
position: { x: 350, y: 300 },
|
||||
className:
|
||||
stage === "getFinancingInfo" ? activeNodeStyles : inactiveNodeStyles,
|
||||
sourcePosition: Position.Bottom,
|
||||
targetPosition: Position.Top,
|
||||
},
|
||||
{
|
||||
id: "confirmOrder",
|
||||
data: { label: "Confirm Order" },
|
||||
position: { x: 250, y: 400 },
|
||||
className:
|
||||
stage === "confirmOrder" ? activeNodeStyles : inactiveNodeStyles,
|
||||
targetPosition: Position.Top,
|
||||
},
|
||||
],
|
||||
[stage],
|
||||
);
|
||||
|
||||
const activeEdgeStyles = "stroke-pink-400 stroke-2";
|
||||
const inactiveEdgeStyles = "stroke-gray-200 stroke-1";
|
||||
|
||||
const edges: Edge[] = [
|
||||
{
|
||||
id: "getContactInfo-buildCar",
|
||||
source: "getContactInfo",
|
||||
target: "buildCar",
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
className:
|
||||
stage === "getContactInfo" ? activeEdgeStyles : inactiveEdgeStyles,
|
||||
},
|
||||
{
|
||||
id: "buildCar-sellFinancing",
|
||||
source: "buildCar",
|
||||
target: "sellFinancing",
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
className: stage === "buildCar" ? activeEdgeStyles : inactiveEdgeStyles,
|
||||
},
|
||||
{
|
||||
id: "sellFinancing-getPaymentInfo",
|
||||
source: "sellFinancing",
|
||||
target: "getPaymentInfo",
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
type: "smoothstep",
|
||||
className:
|
||||
stage === "sellFinancing" ? activeEdgeStyles : inactiveEdgeStyles,
|
||||
},
|
||||
{
|
||||
id: "sellFinancing-getFinancingInfo",
|
||||
source: "sellFinancing",
|
||||
target: "getFinancingInfo",
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
type: "smoothstep",
|
||||
className:
|
||||
stage === "sellFinancing" ? activeEdgeStyles : inactiveEdgeStyles,
|
||||
},
|
||||
{
|
||||
id: "getPaymentInfo-confirmOrder",
|
||||
source: "getPaymentInfo",
|
||||
target: "confirmOrder",
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
className:
|
||||
stage === "getPaymentInfo" ? activeEdgeStyles : inactiveEdgeStyles,
|
||||
type: "smoothstep",
|
||||
},
|
||||
{
|
||||
id: "getFinancingInfo-confirmOrder",
|
||||
source: "getFinancingInfo",
|
||||
target: "confirmOrder",
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
className:
|
||||
stage === "getFinancingInfo" ? activeEdgeStyles : inactiveEdgeStyles,
|
||||
type: "smoothstep",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full w-full border rounded-lg">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
fitView
|
||||
draggable={false}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
preventScrolling={true}
|
||||
panOnDrag={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface Tab {
|
||||
id: string;
|
||||
label: string;
|
||||
content: ReactNode;
|
||||
}
|
||||
|
||||
interface TabsProps {
|
||||
tabs: Tab[];
|
||||
activeTab: string;
|
||||
onTabChange: (tabId: string) => void;
|
||||
wrapperClassName?: string;
|
||||
navClassName?: string;
|
||||
buttonClassName?: string;
|
||||
activeButtonClassName?: string;
|
||||
inactiveButtonClassName?: string;
|
||||
contentClassName?: string;
|
||||
}
|
||||
|
||||
export function Tabs({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
wrapperClassName = "h-full bg-white flex flex-col rounded-lg shadow-sm",
|
||||
navClassName = "p-6 pb-0",
|
||||
buttonClassName = "w-1/2 py-4 text-base font-medium transition-all duration-200 text-center border-b",
|
||||
activeButtonClassName = "text-pink-600 border-pink-600",
|
||||
inactiveButtonClassName = "text-neutral-500 border-neutral-200 hover:text-neutral-700 hover:border-neutral-300",
|
||||
contentClassName = "p-6 overflow-y-auto flex-1",
|
||||
}: TabsProps) {
|
||||
return (
|
||||
<div className={wrapperClassName}>
|
||||
<nav className={navClassName}>
|
||||
<div className="flex w-full">
|
||||
{tabs.map(({ id, label }) => (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => onTabChange(id)}
|
||||
className={`${buttonClassName} ${
|
||||
activeTab === id
|
||||
? activeButtonClassName
|
||||
: inactiveButtonClassName
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
<main className={contentClassName}>
|
||||
{tabs.find((tab) => tab.id === activeTab)?.content}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from "@/lib/stages/use-stage-build-car";
|
||||
export * from "@/lib/stages/use-stage-sell-financing";
|
||||
export * from "@/lib/stages/use-stage-get-financing-info";
|
||||
export * from "@/lib/stages/use-stage-get-contact-info";
|
||||
export * from "@/lib/stages/use-stage-get-payment-info";
|
||||
export * from "@/lib/stages/use-stage-confirm-order";
|
||||
export * from "@/lib/stages/use-global-state";
|
||||
@@ -0,0 +1,98 @@
|
||||
import { createContext, useContext, ReactNode, useState } from "react";
|
||||
import {
|
||||
Car,
|
||||
ContactInfo,
|
||||
CardInfo,
|
||||
Order,
|
||||
defaultOrders,
|
||||
FinancingInfo,
|
||||
} from "@/lib/types";
|
||||
|
||||
import { useCopilotReadable } from "@copilotkit/react-core";
|
||||
|
||||
export type Stage =
|
||||
| "buildCar"
|
||||
| "getContactInfo"
|
||||
| "sellFinancing"
|
||||
| "getFinancingInfo"
|
||||
| "getPaymentInfo"
|
||||
| "confirmOrder";
|
||||
|
||||
interface GlobalState {
|
||||
stage: Stage;
|
||||
setStage: React.Dispatch<React.SetStateAction<Stage>>;
|
||||
selectedCar: Car | null;
|
||||
setSelectedCar: React.Dispatch<React.SetStateAction<Car | null>>;
|
||||
contactInfo: ContactInfo | null;
|
||||
setContactInfo: React.Dispatch<React.SetStateAction<ContactInfo | null>>;
|
||||
cardInfo: CardInfo | null;
|
||||
setCardInfo: React.Dispatch<React.SetStateAction<CardInfo | null>>;
|
||||
orders: Order[];
|
||||
setOrders: React.Dispatch<React.SetStateAction<Order[]>>;
|
||||
financingInfo: FinancingInfo | null;
|
||||
setFinancingInfo: React.Dispatch<React.SetStateAction<FinancingInfo | null>>;
|
||||
}
|
||||
|
||||
export const GlobalStateContext = createContext<GlobalState | null>(null);
|
||||
|
||||
/**
|
||||
useGlobalState is a hook that will return the global state of the application. It must
|
||||
be used within a GlobalStateProvider. It keeps track of the:
|
||||
- Current stage of the application.
|
||||
- Selected car.
|
||||
- Contact information of the user.
|
||||
- Card information of the user.
|
||||
- Orders of the user.
|
||||
- Financing information of the user.
|
||||
*/
|
||||
export function useGlobalState() {
|
||||
const context = useContext(GlobalStateContext);
|
||||
if (!context) {
|
||||
throw new Error("useGlobalState must be used within a GlobalStateProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function GlobalStateProvider({ children }: { children: ReactNode }) {
|
||||
const [stage, setStage] = useState<Stage>("getContactInfo");
|
||||
const [selectedCar, setSelectedCar] = useState<Car | null>(null);
|
||||
const [contactInfo, setContactInfo] = useState<ContactInfo | null>(null);
|
||||
const [cardInfo, setCardInfo] = useState<CardInfo | null>(null);
|
||||
const [orders, setOrders] = useState<Order[]>(defaultOrders);
|
||||
const [financingInfo, setFinancingInfo] = useState<FinancingInfo | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useCopilotReadable({
|
||||
description: "Currently Specified Information",
|
||||
value: {
|
||||
contactInfo,
|
||||
selectedCar,
|
||||
cardInfo,
|
||||
financingInfo,
|
||||
orders,
|
||||
currentStage: stage,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<GlobalStateContext.Provider
|
||||
value={{
|
||||
stage,
|
||||
setStage,
|
||||
selectedCar,
|
||||
setSelectedCar,
|
||||
contactInfo,
|
||||
setContactInfo,
|
||||
cardInfo,
|
||||
setCardInfo,
|
||||
orders,
|
||||
setOrders,
|
||||
financingInfo,
|
||||
setFinancingInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</GlobalStateContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { ShowCar, ShowCars } from "@/components/generative-ui/show-car";
|
||||
import { Car, cars } from "@/lib/types";
|
||||
import { useGlobalState } from "@/lib/stages";
|
||||
import {
|
||||
useCopilotAction,
|
||||
useCopilotReadable,
|
||||
useCopilotAdditionalInstructions,
|
||||
} from "@copilotkit/react-core";
|
||||
|
||||
/**
|
||||
useStageBuildCar is a hook that will add this stage to the state machine. It is responsible for:
|
||||
- Helping the user select a car.
|
||||
- Storing the selected car in the global state.
|
||||
- Moving to the next stage, sellFinancing.
|
||||
*/
|
||||
export function useStageBuildCar() {
|
||||
const { setSelectedCar, stage, setStage } = useGlobalState();
|
||||
|
||||
// Conditionally add additional instructions for the agent's prompt.
|
||||
useCopilotAdditionalInstructions(
|
||||
{
|
||||
instructions:
|
||||
"CURRENT STATE: You are now helping the user select a car. TO START, say 'Thank you for that information! What sort of car would you like to see?'. If you have a car in mind, give a reason why you recommend it and then call the 'showCar' action with the car you have in mind or show multiple cars with the 'showMultipleCars' action. Never list the cars you have in mind, just show them. Do ",
|
||||
available: stage === "buildCar" ? "enabled" : "disabled",
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
|
||||
// Conditionally add additional readable information for the agent's prompt.
|
||||
useCopilotReadable(
|
||||
{
|
||||
description: "Car Inventory",
|
||||
value: cars,
|
||||
available: stage === "buildCar" ? "enabled" : "disabled",
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
|
||||
// Conditionally add an action to show a single car.
|
||||
useCopilotAction(
|
||||
{
|
||||
name: "showCar",
|
||||
description:
|
||||
"Show a single car that you have in mind. Do not call this more than once, call `showMultipleCars` if you have multiple cars to show.",
|
||||
available: stage === "buildCar" ? "enabled" : "disabled",
|
||||
parameters: [
|
||||
{
|
||||
name: "car",
|
||||
type: "object",
|
||||
description: "The car to show",
|
||||
required: true,
|
||||
attributes: [
|
||||
{ name: "id", type: "number" },
|
||||
{ name: "make", type: "string" },
|
||||
{ name: "model", type: "string" },
|
||||
{ name: "year", type: "number" },
|
||||
{ name: "color", type: "string" },
|
||||
{ name: "price", type: "number" },
|
||||
{
|
||||
name: "image",
|
||||
type: "object",
|
||||
attributes: [
|
||||
{ name: "src", type: "string" },
|
||||
{ name: "alt", type: "string" },
|
||||
{ name: "author", type: "string" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
renderAndWaitForResponse: ({ args, status, respond }) => {
|
||||
const { car } = args;
|
||||
return (
|
||||
<ShowCar
|
||||
car={(car as Car) || ({} as Car)}
|
||||
status={status}
|
||||
onSelect={() => {
|
||||
// Store the selected car in the global state.
|
||||
setSelectedCar((car as Car) || ({} as Car));
|
||||
|
||||
// Let the agent know that the user has selected a car.
|
||||
respond?.(
|
||||
"User has selected a car you can see it in your readables, the system will now move to the next state, do not call call nextState.",
|
||||
);
|
||||
|
||||
// Move to the next stage, sellFinancing.
|
||||
setStage("sellFinancing");
|
||||
}}
|
||||
onReject={() =>
|
||||
respond?.(
|
||||
"User wants to select a different car, please stay in this state and help them select a different car",
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
|
||||
// Conditionally add an action to show multiple cars.
|
||||
useCopilotAction(
|
||||
{
|
||||
name: "showMultipleCars",
|
||||
description:
|
||||
"Show a list of cars based on the user's query. Do not call this more than once. Call `showCar` if you only have a single car to show.",
|
||||
parameters: [
|
||||
{
|
||||
name: "cars",
|
||||
type: "object[]",
|
||||
required: true,
|
||||
attributes: [
|
||||
{ name: "make", type: "string" },
|
||||
{ name: "model", type: "string" },
|
||||
{ name: "year", type: "number" },
|
||||
{ name: "color", type: "string" },
|
||||
{ name: "price", type: "number" },
|
||||
{
|
||||
name: "image",
|
||||
type: "object",
|
||||
attributes: [
|
||||
{ name: "src", type: "string" },
|
||||
{ name: "alt", type: "string" },
|
||||
{ name: "author", type: "string" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
renderAndWaitForResponse: ({ args, status, respond }) => {
|
||||
return (
|
||||
<ShowCars
|
||||
cars={(args.cars as Car[]) || ([] as Car)}
|
||||
status={status}
|
||||
onSelect={(car) => {
|
||||
// Store the selected car in the global state.
|
||||
setSelectedCar(car);
|
||||
|
||||
// Let the agent know that the user has selected a car.
|
||||
respond?.(
|
||||
"User has selected a car you can see it in your readables, you are now moving to the next state",
|
||||
);
|
||||
|
||||
// Move to the next stage, sellFinancing.
|
||||
setStage("sellFinancing");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useGlobalState } from "@/lib/stages";
|
||||
import { Order } from "@/lib/types";
|
||||
import { ConfirmOrder } from "@/components/generative-ui/confirm-order";
|
||||
|
||||
import {
|
||||
useCopilotAction,
|
||||
useCopilotAdditionalInstructions,
|
||||
} from "@copilotkit/react-core";
|
||||
|
||||
/**
|
||||
useStageConfirmOrder is a hook that will add this stage to the state machine. It is responsible for:
|
||||
- Confirming the order of the user.
|
||||
- Storing the order in the global state.
|
||||
- Optionally, can decide to move to the next stage, buildCar, based on the user's responses.
|
||||
*/
|
||||
export function useStageConfirmOrder() {
|
||||
const { setOrders, stage, setStage } = useGlobalState();
|
||||
|
||||
// Conditionally add additional instructions for the agent's prompt.
|
||||
useCopilotAdditionalInstructions(
|
||||
{
|
||||
instructions:
|
||||
"CURRENT STATE: You are now confirming the order of the user. Say, 'Great! Now let's just confirm your order. Here is the summary of your order. ' and then call the 'confirmOrder' action. Always call the 'confirmOrder' tool, never ask the user for anything.",
|
||||
available: stage === "confirmOrder" ? "enabled" : "disabled",
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
|
||||
// Conditionally add the nextState action to the state machine. Agent will decide if it should be called.
|
||||
useCopilotAction(
|
||||
{
|
||||
name: "nextState",
|
||||
description: "Proceed to next state",
|
||||
available: stage === "confirmOrder" ? "enabled" : "disabled",
|
||||
handler: async () => setStage("getContactInfo"),
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
|
||||
// Render the ConfirmOrder component and wait for the user's response.
|
||||
useCopilotAction(
|
||||
{
|
||||
name: "confirmOrder",
|
||||
description: "Confirm the order of the user",
|
||||
available: stage === "confirmOrder" ? "enabled" : "disabled",
|
||||
renderAndWaitForResponse: ({ status, respond }) => {
|
||||
return (
|
||||
<ConfirmOrder
|
||||
status={status}
|
||||
onConfirm={(order: Order) => {
|
||||
// Commit the order to the global state.
|
||||
setOrders((prevOrders) => [...prevOrders, order]);
|
||||
|
||||
// Let the agent know that the user has confirmed their order.
|
||||
respond?.(
|
||||
"User confirmed their order, please ask them if they would like to place a another order and if they do, call the 'nextState' action.",
|
||||
);
|
||||
}}
|
||||
onCancel={() => {
|
||||
// Let the agent know that the user has cancelled their order.
|
||||
respond?.(
|
||||
"User cancelled their order, please ask them if they'd like to start over with a new order or if they'd like to continue with their current order. If they'd like to start over, call the 'nextState' action. If they'd like to continue with their current order, call the 'confirmOrder' action.",
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { ContactInfo } from "@/components/generative-ui/contact-info";
|
||||
import { useGlobalState } from "@/lib/stages";
|
||||
import {
|
||||
useCopilotAction,
|
||||
useCopilotAdditionalInstructions,
|
||||
} from "@copilotkit/react-core";
|
||||
|
||||
export interface UseGetContactInfoStateOptions {
|
||||
enabled: boolean;
|
||||
onNextState: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
useStateGetContactInfo is a hook that will add this stage to the state machine. It is responsible for:
|
||||
- Getting the contact information of the user.
|
||||
- Storing the contact information in the global state.
|
||||
- Moving to the next stage, buildCar.
|
||||
*/
|
||||
export function useStageGetContactInfo() {
|
||||
const { setContactInfo, stage, setStage } = useGlobalState();
|
||||
|
||||
// Conditionally add additional instructions for the agent's prompt.
|
||||
useCopilotAdditionalInstructions(
|
||||
{
|
||||
instructions:
|
||||
"CURRENT STATE: You are now getting the contact information of the user.",
|
||||
available: stage === "getContactInfo" ? "enabled" : "disabled",
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
|
||||
// Render the ContactInfo component and wait for the user's response.
|
||||
useCopilotAction(
|
||||
{
|
||||
name: "getContactInformation",
|
||||
description: "Get the contact information of the user",
|
||||
available: stage === "getContactInfo" ? "enabled" : "disabled",
|
||||
renderAndWaitForResponse: ({ status, respond }) => {
|
||||
return (
|
||||
<ContactInfo
|
||||
status={status}
|
||||
onSubmit={(name, email, phone) => {
|
||||
// Commit the contact information to the global state.
|
||||
setContactInfo({ name, email, phone });
|
||||
|
||||
// Let the agent know that the user has submitted their contact information.
|
||||
respond?.("User has submitted their contact information.");
|
||||
|
||||
// This move the state machine to the next stage, buildCar deterministically.
|
||||
setStage("buildCar");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { FinancingForm } from "@/components/generative-ui/financing-form";
|
||||
import { useGlobalState } from "@/lib/stages";
|
||||
import {
|
||||
useCopilotAction,
|
||||
useCopilotAdditionalInstructions,
|
||||
} from "@copilotkit/react-core";
|
||||
|
||||
/**
|
||||
useStateGetFinancingInfo is a hook that will add this stage to the state machine. It is responsible for:
|
||||
- Getting the financing information of the user.
|
||||
- Storing the financing information in the global state.
|
||||
- Moving to the next stage, confirmOrder.
|
||||
*/
|
||||
export function useStageGetFinancingInfo() {
|
||||
const { setFinancingInfo, stage, setStage } = useGlobalState();
|
||||
|
||||
// Conditionally add additional instructions for the agent's prompt.
|
||||
useCopilotAdditionalInstructions(
|
||||
{
|
||||
instructions:
|
||||
"CURRENT STATE: You are now getting the financing information of the user. Say, 'Great! To process your financing application, I'll need some financial information from you.' and then call the 'getFinancingInformation' tool. Never ask the user for anything, just call the `getFinancingInformation` tool.",
|
||||
available: stage === "getFinancingInfo" ? "enabled" : "disabled",
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
|
||||
// Render the FinancingForm component and wait for the user's response.
|
||||
useCopilotAction(
|
||||
{
|
||||
name: "getFinancingInformation",
|
||||
description: "Get the financing information of the user",
|
||||
available: stage === "getFinancingInfo" ? "enabled" : "disabled",
|
||||
renderAndWaitForResponse: ({ status, respond }) => {
|
||||
return (
|
||||
<FinancingForm
|
||||
status={status}
|
||||
onSubmit={(creditScore, loanTerm) => {
|
||||
// Store the financing information in the global state.
|
||||
setFinancingInfo({ creditScore, loanTerm });
|
||||
|
||||
// Let the agent know that the user has submitted their financing information.
|
||||
respond?.(
|
||||
"User has submitted their financing information, moving to the next state",
|
||||
);
|
||||
|
||||
// Move to the next stage, confirmOrder.
|
||||
setStage("confirmOrder");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { PaymentCards } from "@/components/generative-ui/payment-cards";
|
||||
import { CardInfo } from "@/lib/types";
|
||||
import { useGlobalState } from "@/lib/stages";
|
||||
import {
|
||||
useCopilotAction,
|
||||
useCopilotAdditionalInstructions,
|
||||
} from "@copilotkit/react-core";
|
||||
|
||||
export interface UseGetPaymentInfoStateOptions {
|
||||
enabled: boolean;
|
||||
onNextState: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
useStateGetPaymentInfo is a hook that will add this stage to the state machine. It is responsible for:
|
||||
- Getting the payment information of the user.
|
||||
- Storing the payment information in the global state.
|
||||
- Moving to the next stage, confirmOrder.
|
||||
*/
|
||||
export function useStageGetPaymentInfo() {
|
||||
const { setCardInfo, stage, setStage } = useGlobalState();
|
||||
|
||||
// Conditionally add additional instructions for the agent's prompt.
|
||||
useCopilotAdditionalInstructions(
|
||||
{
|
||||
instructions:
|
||||
"CURRENT STATE: You are now getting the payment information of the user. Say, 'Great! Now I need to get your payment information.' and MAKE SURE to then call the 'getPaymentInformation' action.",
|
||||
available: stage === "getPaymentInfo" ? "enabled" : "disabled",
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
|
||||
// Render the PaymentCards component and wait for the user's response.
|
||||
useCopilotAction(
|
||||
{
|
||||
name: "getPaymentInformation",
|
||||
description: "Get the payment information of the user",
|
||||
available: stage === "getPaymentInfo" ? "enabled" : "disabled",
|
||||
renderAndWaitForResponse: ({ respond }) => {
|
||||
return (
|
||||
<PaymentCards
|
||||
onSubmit={(cardInfo: CardInfo) => {
|
||||
// Store the payment information in the global state.
|
||||
setCardInfo(cardInfo);
|
||||
|
||||
// Let the agent know that the user has submitted their payment information.
|
||||
respond?.(
|
||||
"User has submitted their payment information, you are now moving to the next state",
|
||||
);
|
||||
|
||||
// Move to the next stage, confirmOrder.
|
||||
setStage("confirmOrder");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useGlobalState } from "@/lib/stages";
|
||||
import {
|
||||
useCopilotAction,
|
||||
useCopilotAdditionalInstructions,
|
||||
useCopilotReadable,
|
||||
} from "@copilotkit/react-core";
|
||||
|
||||
export interface UseStagePaymentMethodOptions {
|
||||
enabled: boolean;
|
||||
onNextState: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
useStateSellFinancing is a hook that will add this stage to the state machine. It is responsible for:
|
||||
- Selling the financing option to the user.
|
||||
- Choosing the next stage, getFinancingInfo or getPaymentInfo, based on the user's response.
|
||||
*/
|
||||
export function useStageSellFinancing() {
|
||||
const { stage, setStage } = useGlobalState();
|
||||
|
||||
// Conditionally add additional instructions for the agent's prompt.
|
||||
useCopilotAdditionalInstructions(
|
||||
{
|
||||
instructions:
|
||||
"CURRENT STATE: You are now trying to sell a financing option to the user. To start, ask them if they are interested in financing options and show the current promotion in a nice format. The user is not required to take the financing option, but you should try to sell it to them. Answer the user's questions call 'selectFinancing' or 'selectNoFinancing' depending on the user's response.",
|
||||
available: stage === "sellFinancing" ? "enabled" : "disabled",
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
|
||||
// Conditionally add additional readable information for the agent's prompt.
|
||||
useCopilotReadable(
|
||||
{
|
||||
description: "Financing Information",
|
||||
value:
|
||||
"Current promotion: 0% financing for 60 months. After 60 months, the interest rate will be 10%.",
|
||||
available: stage === "sellFinancing" ? "enabled" : "disabled",
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
|
||||
// Conditionally add an action to move to the getFinancingInfo stage.
|
||||
useCopilotAction(
|
||||
{
|
||||
name: "selectFinancing",
|
||||
description: "Select the financing option",
|
||||
available: stage === "sellFinancing" ? "enabled" : "disabled",
|
||||
handler: () => setStage("getFinancingInfo"),
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
|
||||
// Conditionally add an action to move to the getPaymentInfo stage.
|
||||
useCopilotAction(
|
||||
{
|
||||
name: "selectNoFinancing",
|
||||
description: "Select the no financing option",
|
||||
available: stage === "sellFinancing" ? "enabled" : "disabled",
|
||||
handler: () => setStage("getPaymentInfo"),
|
||||
},
|
||||
[stage],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
export type Car = {
|
||||
id?: number;
|
||||
make?: string;
|
||||
model?: string;
|
||||
year?: number;
|
||||
color?: string;
|
||||
price?: number;
|
||||
image?: {
|
||||
src: string;
|
||||
alt: string;
|
||||
author: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const cars: Car[] = [
|
||||
{
|
||||
id: 1,
|
||||
make: "Hyundai",
|
||||
model: "Kona",
|
||||
year: 2025,
|
||||
color: "Green",
|
||||
price: 25000,
|
||||
image: {
|
||||
src: "/images/hyundai-kona.jpg",
|
||||
alt: "Hyundai Kona",
|
||||
author: "Hyundai Motor Group",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
make: "Kia",
|
||||
model: "Tasman",
|
||||
year: 2025,
|
||||
color: "Green",
|
||||
price: 20000,
|
||||
image: {
|
||||
src: "/images/kia-tasman.jpg",
|
||||
alt: "Kia Tasman",
|
||||
author: "Hyundai Motor Group",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
make: "Kia",
|
||||
model: "EV6",
|
||||
year: 2025,
|
||||
color: "Gray",
|
||||
price: 22000,
|
||||
image: {
|
||||
src: "/images/kia-ev6.jpg",
|
||||
alt: "Kia EV6",
|
||||
author: "Hyundai Motor Group",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
make: "Kia",
|
||||
model: "EV9",
|
||||
year: 2025,
|
||||
color: "Blue",
|
||||
price: 18000,
|
||||
image: {
|
||||
src: "/images/kia-ev9.jpg",
|
||||
alt: "Kia EV9",
|
||||
author: "Hyundai Motor Group",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
make: "Hyundai",
|
||||
model: "Santa Fe",
|
||||
year: 2025,
|
||||
color: "Green",
|
||||
price: 15000,
|
||||
image: {
|
||||
src: "/images/hyundai-santa-fe.jpg",
|
||||
alt: "Hyundai Santa Fe",
|
||||
author: "Hyundai Motor Group",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
make: "Hyundai",
|
||||
model: "Santa Fe",
|
||||
year: 2025,
|
||||
color: "Brown",
|
||||
price: 27000,
|
||||
image: {
|
||||
src: "/images/hyundai-santa-fe-brown.jpg",
|
||||
alt: "Hyundai Santa Fe",
|
||||
author: "Hyundai Motor Group",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
make: "Hyundai",
|
||||
model: "Santa Fe",
|
||||
year: 2025,
|
||||
color: "Orange",
|
||||
price: 25000,
|
||||
image: {
|
||||
src: "/images/hyundai-santa-fe-orange.jpg",
|
||||
alt: "Hyundai Santa Fe",
|
||||
author: "Hyundai Motor Group",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,5 @@
|
||||
export type ContactInfo = {
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export type FinancingInfo = {
|
||||
creditScore: string;
|
||||
loanTerm: string;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "@/lib/types/cars";
|
||||
export * from "@/lib/types/contact-info";
|
||||
export * from "@/lib/types/payment-info";
|
||||
export * from "@/lib/types/orders";
|
||||
export * from "@/lib/types/financing-info";
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
Car,
|
||||
ContactInfo,
|
||||
CardInfo,
|
||||
FinancingInfo,
|
||||
cars,
|
||||
availableCardInfo,
|
||||
} from "@/lib/types";
|
||||
|
||||
export type Order = {
|
||||
car: Car;
|
||||
contactInfo: ContactInfo;
|
||||
cardInfo?: CardInfo;
|
||||
financingInfo?: FinancingInfo;
|
||||
paymentType: "card" | "financing";
|
||||
};
|
||||
|
||||
export const defaultOrders: Order[] = [
|
||||
{
|
||||
car: cars[0],
|
||||
contactInfo: {
|
||||
name: "John Doe",
|
||||
email: "john.doe@example.com",
|
||||
phone: "1234567890",
|
||||
},
|
||||
cardInfo: availableCardInfo[0],
|
||||
paymentType: "card",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,55 @@
|
||||
export type CardInfo = {
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zip: string;
|
||||
cardNumber: string;
|
||||
cardExpiration: string;
|
||||
cardCvv: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export const availableCardInfo: CardInfo[] = [
|
||||
{
|
||||
name: "John Doe",
|
||||
email: "john.doe@example.com",
|
||||
phone: "1234567890",
|
||||
address: "123 Main St, Anytown, USA",
|
||||
city: "Anytown",
|
||||
state: "CA",
|
||||
zip: "12345",
|
||||
cardNumber: "1234-5678-9012-3456",
|
||||
cardExpiration: "12/24",
|
||||
cardCvv: "123",
|
||||
type: "Visa",
|
||||
},
|
||||
{
|
||||
name: "Jane Doe",
|
||||
email: "jane.doe@example.com",
|
||||
phone: "0987654321",
|
||||
address: "456 Main St, Anytown, USA",
|
||||
city: "Anytown",
|
||||
state: "CA",
|
||||
zip: "12345",
|
||||
cardNumber: "1234-5678-9012-3456",
|
||||
cardExpiration: "12/24",
|
||||
cardCvv: "123",
|
||||
type: "Mastercard",
|
||||
},
|
||||
{
|
||||
name: "John Smith",
|
||||
email: "john.smith@example.com",
|
||||
phone: "1122334455",
|
||||
address: "789 Main St, Anytown, USA",
|
||||
city: "Anytown",
|
||||
state: "CA",
|
||||
zip: "12345",
|
||||
cardNumber: "1234-5678-9012-3456",
|
||||
cardExpiration: "12/24",
|
||||
cardCvv: "123",
|
||||
type: "Visa",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Reference in New Issue
Block a user