diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7de9bb6..f751c78 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { TRPCReactProvider } from "@/trpc/client"; import { Toaster } from "sonner"; +import { ThemeProvider } from "next-themes"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -26,12 +27,19 @@ export default function RootLayout({ }>) { return ( - + - - {children} + + + {children} + diff --git a/src/modules/projects/ui/components/message-card.tsx b/src/modules/projects/ui/components/message-card.tsx index 3c279d0..a0a4c12 100644 --- a/src/modules/projects/ui/components/message-card.tsx +++ b/src/modules/projects/ui/components/message-card.tsx @@ -42,9 +42,11 @@ const FragmentCard = ({ onFragmentClick }: FragmentCardProps) => { return ( - onFragmentClick(fragment)} + className={cn( + "flex items-start text-start gap-2 border rounded-lg bg-muted w-fit p-3 hover:bg-secondary transition-colors cursor-pointer", + isActiveFragment && "bg-primary text-primary-foreground border-primary hover:bg-primary" )}> @@ -94,7 +96,7 @@ const AssistantMessage = ({ { }} + onFragmentClick={onFragmentClick} /> )} @@ -122,7 +124,7 @@ export default function MessageCard({ fragment={fragment} createdAt={createdAt} isActiveFragment={isActiveFragment} - onFragmentClick={() => { }} + onFragmentClick={onFragmentClick} type={type} /> ) diff --git a/src/modules/projects/ui/components/message-loading.tsx b/src/modules/projects/ui/components/message-loading.tsx new file mode 100644 index 0000000..cd5a00f --- /dev/null +++ b/src/modules/projects/ui/components/message-loading.tsx @@ -0,0 +1,56 @@ +import Image from "next/image"; +import { useEffect, useState } from "react"; + +const ShimmerMessages = () => { + const messages = [ + "Thinking...", + "Loading...", + "Generating...", + "Analyzing your request...", + "Building your website", + "Crafting components...", + "Optimizing layout...", + "Adding final touches...", + "Almost ready..." + ]; + + const [currentMessageIndex, setCurrentMessageIndex] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setCurrentMessageIndex((prev) => (prev + 1) % messages.length); + }, 2000); + + return () => clearInterval(interval); + }, [messages.length]); + + return ( + + + {messages[currentMessageIndex]} + + + ); +} + +export default function MessageLoading() { + return ( + + + + + Neo + + + + + + + ); +} \ No newline at end of file diff --git a/src/modules/projects/ui/components/messages-container.tsx b/src/modules/projects/ui/components/messages-container.tsx index 5b50a70..5e171ee 100644 --- a/src/modules/projects/ui/components/messages-container.tsx +++ b/src/modules/projects/ui/components/messages-container.tsx @@ -5,23 +5,36 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import MessageCard from "./message-card"; import MessageForm from "./message-form"; import { useEffect, useRef } from "react"; +import { Fragment } from "@/generated/prisma"; +import MessageLoading from "./message-loading"; -export default function MessagesContainer({ projectId }: { projectId: string }) { +export default function MessagesContainer({ + projectId, + activeFragment, + setActiveFragment + }: { + projectId: string, + activeFragment: Fragment | null, + setActiveFragment: (fragment: Fragment | null) => void + }) { const bottomRef = useRef(null); const trpc = useTRPC(); const { data: messages } = useSuspenseQuery(trpc.messages.getMany.queryOptions({ projectId: projectId - })); + })); useEffect(() => { - const lastAssistantMessage = messages.findLast( - (message) => message.role === "ASSISTANT" + const lastAssistantMessageWithFragment = messages.findLast( + (message) => message.role === "ASSISTANT" && !!message.fragment ); - if (lastAssistantMessage) { - // TODO: SET ACTIVE FRAGMENT + if (lastAssistantMessageWithFragment) { + setActiveFragment(lastAssistantMessageWithFragment.fragment); } - }, [messages]); + }, [messages, setActiveFragment]); + + const lastMessage = messages[messages.length - 1 ]; + const isLastMessageUser = lastMessage?.role === "USER"; useEffect(() => { bottomRef.current?.scrollIntoView(); @@ -38,11 +51,12 @@ export default function MessagesContainer({ projectId }: { projectId: string }) role={message.role} fragment={message.fragment} createdAt={message.createdAt} - isActiveFragment={false} - onFragmentClick={() => {}} + isActiveFragment={activeFragment?.id === message.fragment?.id} + onFragmentClick={() => setActiveFragment(message.fragment)} type={message.type} /> ))} + { isLastMessageUser && } diff --git a/src/modules/projects/ui/components/project-header.tsx b/src/modules/projects/ui/components/project-header.tsx new file mode 100644 index 0000000..961b3ca --- /dev/null +++ b/src/modules/projects/ui/components/project-header.tsx @@ -0,0 +1,71 @@ +import { Button } from "@/components/ui/button"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { useTRPC } from "@/trpc/client"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { ChevronDownIcon, ChevronLeftIcon, SunMoonIcon } from "lucide-react"; +import { useTheme } from "next-themes"; +import Image from "next/image"; +import Link from "next/link"; + +export default function ProjectHeader({ projectId }: { projectId: string }) { + const trpc = useTRPC(); + const { data: project } = useSuspenseQuery( + trpc.projects.getOne.queryOptions({ id: projectId }) + ); + + const { setTheme, theme } = useTheme(); + + return ( + + + + + + {project.name} + + + + + + + + Go to dashboard + + + + + + + Appearance + + + + + + Light + + + Dark + + + System + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/modules/projects/ui/views/project-view.tsx b/src/modules/projects/ui/views/project-view.tsx index 186a4fc..590f398 100644 --- a/src/modules/projects/ui/views/project-view.tsx +++ b/src/modules/projects/ui/views/project-view.tsx @@ -8,13 +8,17 @@ import { ResizablePanelGroup, } from "@/components/ui/resizable" import MessagesContainer from "../components/messages-container"; -import { Suspense } from "react"; +import { Suspense, useState } from "react"; +import { Fragment } from "@/generated/prisma"; +import ProjectHeader from "../components/project-header"; interface Props { projectId: string } export default function ProjectView({ projectId }: Props) { + const [activeFragment, setActiveFragment] = useState(null); + return ( @@ -23,8 +27,15 @@ export default function ProjectView({ projectId }: Props) { minSize={20} className="flex flex-col min-h-0" > + Loading project...}> + + Loading messages}> - +