Skip to content
Merged
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
14 changes: 11 additions & 3 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -26,12 +27,19 @@ export default function RootLayout({
}>) {
return (
<TRPCReactProvider>
<html lang="en">
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Toaster />
{children}
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<Toaster />
{children}
</ThemeProvider>
</body>
</html>
</TRPCReactProvider>
Expand Down
12 changes: 7 additions & 5 deletions src/modules/projects/ui/components/message-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ const FragmentCard = ({
onFragmentClick
}: FragmentCardProps) => {
return (
<button 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"
<button
onClick={() => 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"
)}>
<Code2Icon className="size-4 mt-0.5" />
<div className="flex flex-col flex-1">
Expand Down Expand Up @@ -94,7 +96,7 @@ const AssistantMessage = ({
<FragmentCard
fragment={fragment}
isActiveFragment={isActiveFragment}
onFragmentClick={() => { }}
onFragmentClick={onFragmentClick}
/>
)}
</div>
Expand Down Expand Up @@ -122,7 +124,7 @@ export default function MessageCard({
fragment={fragment}
createdAt={createdAt}
isActiveFragment={isActiveFragment}
onFragmentClick={() => { }}
onFragmentClick={onFragmentClick}
type={type}
/>
)
Expand Down
56 changes: 56 additions & 0 deletions src/modules/projects/ui/components/message-loading.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex items-center gap-2">
<span className="text-base text-muted-foreground animate-pulse">
{messages[currentMessageIndex]}
</span>
</div>
);
}

export default function MessageLoading() {
return (
<div className="flex flex-col group px-2 pb-4">
<div className="flex items-center gap-2 pl-2 mb-2">
<Image
src="/logo.svg"
alt="Neo logo"
width={15}
height={15}
className="shrink-0"
/>
<span className="text-sm font-medium">
Neo
</span>
</div>
<div className="pl-8.5 flex flex-col gap-y-4">
<ShimmerMessages />
</div>
</div>
);
}
32 changes: 23 additions & 9 deletions src/modules/projects/ui/components/messages-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>(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();
Expand All @@ -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 && <MessageLoading /> }
<div ref={bottomRef} />
</div>
</div>
Expand Down
71 changes: 71 additions & 0 deletions src/modules/projects/ui/components/project-header.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<header className="p-2 flex justify-between items-center border-b">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="focus-visible:ring-0 hover:bg-transparent hover:opacity-75 transition-opacity pl-2!"
>
<Image
src="/logo.svg"
alt="Neo logo"
width={15}
height={15}
className="shrink-0"
/>
<span className="text-sm font-medium">{project.name}</span>
<ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem asChild>
<Link href="/">
<ChevronLeftIcon />
<span>Go to dashboard</span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger className="gap-2">
<SunMoonIcon className="size-4 text-muted-foreground" />
<span>Appearance</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
<DropdownMenuRadioItem value="light">
<span>Light</span>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark">
<span>Dark</span>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system">
<span>System</span>
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
</header>
);
}
15 changes: 13 additions & 2 deletions src/modules/projects/ui/views/project-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Fragment | null>(null);

return (
<div className="h-screen">
<ResizablePanelGroup direction="horizontal">
Expand All @@ -23,8 +27,15 @@ export default function ProjectView({ projectId }: Props) {
minSize={20}
className="flex flex-col min-h-0"
>
<Suspense fallback={<p>Loading project...</p>}>
<ProjectHeader projectId={projectId}/>
</Suspense>
<Suspense fallback={<p>Loading messages</p>}>
<MessagesContainer projectId={projectId} />
<MessagesContainer
projectId={projectId}
activeFragment={activeFragment}
setActiveFragment={setActiveFragment}
/>
</Suspense>
</ResizablePanel>
<ResizableHandle withHandle />
Expand Down