Skip to content
Open
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
27 changes: 27 additions & 0 deletions app/(chat)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,33 @@ export default function Layout({ children }: { children: React.ReactNode }) {
}

async function SidebarShell({ children }: { children: React.ReactNode }) {
const isDbConfigured = Boolean(process.env.POSTGRES_URL);

if (!isDbConfigured) {
return (
<div className="flex min-h-dvh w-full flex-col items-center justify-center bg-zinc-950 text-zinc-100 p-6 font-sans selection:bg-zinc-800">
<div className="w-full max-w-md rounded-xl border border-zinc-800 bg-zinc-900 p-6 shadow-xl">
<div className="flex items-center gap-2.5">
<span className="flex size-7 items-center justify-center rounded-lg bg-zinc-800 border border-zinc-700 text-xs font-mono text-zinc-400">⚠️</span>
<h1 className="text-sm font-semibold tracking-tight text-zinc-100">
Database Connection Required
</h1>
</div>
<p className="mt-3 text-xs text-zinc-400 leading-relaxed">
This chatbot requires a database connection to persist conversation history and manage active sessions.
</p>
<div className="mt-4 rounded-lg bg-zinc-950 border border-zinc-800 p-4 font-mono text-[11px] text-zinc-400">
<p className="font-semibold text-zinc-200 mb-1.5">To fix this, create a <span className="text-zinc-100 font-mono">.env.local</span> file in the root:</p>
<pre className="overflow-x-auto whitespace-pre-wrap select-all bg-zinc-900/50 p-2.5 rounded border border-zinc-800/80 leading-relaxed">POSTGRES_URL="postgres://username:password@host:port/database"</pre>
</div>
<p className="mt-4 text-[10px] text-zinc-500 leading-relaxed">
Once you've configured your <span className="font-mono">.env.local</span> file, restart the development server. You can provision a free Postgres database using Neon, Vercel Postgres, or run a local instance via Docker.
</p>
</div>
</div>
);
}

const [session, cookieStore] = await Promise.all([auth(), cookies()]);
const isCollapsed = cookieStore.get("sidebar_state")?.value !== "true";

Expand Down
34 changes: 23 additions & 11 deletions components/chat/greeting.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import { motion } from "framer-motion";
import { SparklesIcon } from "lucide-react";

export const Greeting = () => {
return (
<div className="flex flex-col items-center px-4" key="overview">
<div className="flex flex-col items-center px-6 text-center select-none" key="overview">
<motion.div
animate={{ opacity: 1, scale: 1 }}
className="mb-4 rounded-xl border border-border/50 bg-card/45 p-2.5 text-muted-foreground/80 shadow-[0_1px_2px_rgba(0,0,0,0.02)]"
initial={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
>
<SparklesIcon className="size-4.5" />
</motion.div>

<motion.h1
animate={{ opacity: 1, y: 0 }}
className="text-center font-semibold text-2xl tracking-tight text-foreground md:text-3xl"
initial={{ opacity: 0, y: 10 }}
transition={{ delay: 0.35, duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
className="text-2xl font-semibold tracking-tight text-foreground md:text-3xl"
initial={{ opacity: 0, y: 8 }}
transition={{ delay: 0.1, duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
>
What can I help with?
</motion.div>
<motion.div
</motion.h1>

<motion.p
animate={{ opacity: 1, y: 0 }}
className="mt-3 text-center text-muted-foreground/80 text-sm"
initial={{ opacity: 0, y: 10 }}
transition={{ delay: 0.5, duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
className="mt-2.5 max-w-[280px] text-muted-foreground/65 text-[13px] leading-relaxed md:max-w-sm"
initial={{ opacity: 0, y: 8 }}
transition={{ delay: 0.2, duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
>
Ask a question, write code, or explore ideas.
</motion.div>
Ask a question, write and analyze code, or brainstorm ideas.
</motion.p>
</div>
);
};

66 changes: 53 additions & 13 deletions components/chat/suggested-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import type { UseChatHelpers } from "@ai-sdk/react";
import { motion } from "framer-motion";
import { memo } from "react";
import { suggestions } from "@/lib/constants";
import {
GlobeIcon,
Code2Icon,
BookOpenIcon,
CloudSunIcon
} from "lucide-react";
import type { ChatMessage } from "@/lib/types";
import { Suggestion } from "../ai-elements/suggestion";
import type { VisibilityType } from "./visibility-selector";
Expand All @@ -14,9 +19,34 @@ type SuggestedActionsProps = {
selectedVisibilityType: VisibilityType;
};

function PureSuggestedActions({ chatId, sendMessage }: SuggestedActionsProps) {
const suggestedActions = suggestions;
const curatedActions = [
{
title: "Explain Next.js",
description: "Learn about Server Components & the App Router.",
prompt: "What are the advantages of using Next.js?",
icon: <GlobeIcon className="size-3.5" />,
},
{
title: "Dijkstra's Algorithm",
description: "Generate a clean implementation in Python.",
prompt: "Write code to demonstrate Dijkstra's algorithm",
icon: <Code2Icon className="size-3.5" />,
},
{
title: "Silicon Valley Essay",
description: "Outline a historical analysis of technology hubs.",
prompt: "Help me write an essay about Silicon Valley",
icon: <BookOpenIcon className="size-3.5" />,
},
{
title: "Weather in San Francisco",
description: "Check current meteorological conditions.",
prompt: "What is the weather in San Francisco?",
icon: <CloudSunIcon className="size-3.5" />,
},
];

function PureSuggestedActions({ chatId, sendMessage }: SuggestedActionsProps) {
return (
<div
className="flex w-full gap-2.5 overflow-x-auto pb-1 sm:grid sm:grid-cols-2 sm:overflow-visible"
Expand All @@ -27,21 +57,21 @@ function PureSuggestedActions({ chatId, sendMessage }: SuggestedActionsProps) {
msOverflowStyle: "none",
}}
>
{suggestedActions.map((suggestedAction, index) => (
{curatedActions.map((action, index) => (
<motion.div
animate={{ opacity: 1, y: 0 }}
className="min-w-[200px] shrink-0 sm:min-w-0 sm:shrink"
exit={{ opacity: 0, y: 16 }}
initial={{ opacity: 0, y: 16 }}
key={suggestedAction}
className="min-w-[220px] shrink-0 sm:min-w-0 sm:shrink"
exit={{ opacity: 0, y: 12 }}
initial={{ opacity: 0, y: 12 }}
key={action.title}
transition={{
delay: 0.06 * index,
duration: 0.4,
delay: 0.05 * index,
duration: 0.45,
ease: [0.22, 1, 0.36, 1],
}}
>
<Suggestion
className="h-auto w-full whitespace-nowrap rounded-xl border border-border/50 bg-card/30 px-4 py-3 text-left text-[12px] leading-relaxed text-muted-foreground transition-all duration-200 sm:whitespace-normal sm:p-4 sm:text-[13px] hover:-translate-y-0.5 hover:bg-card/60 hover:text-foreground hover:shadow-[var(--shadow-card)]"
className="h-auto w-full flex flex-row items-start gap-3 rounded-xl border border-border/50 bg-card/35 px-3.5 py-3 text-left transition-all duration-200 hover:-translate-y-0.5 hover:bg-card/65 hover:border-border/80 hover:shadow-[0_1.5px_6px_rgba(0,0,0,0.02)] dark:hover:shadow-[0_1.5px_6px_rgba(0,0,0,0.15)] group"
onClick={(suggestion) => {
window.history.pushState(
{},
Expand All @@ -53,9 +83,19 @@ function PureSuggestedActions({ chatId, sendMessage }: SuggestedActionsProps) {
parts: [{ type: "text", text: suggestion }],
});
}}
suggestion={suggestedAction}
suggestion={action.prompt}
>
{suggestedAction}
<div className="flex size-7 shrink-0 items-center justify-center rounded-lg border border-border/50 bg-background/50 text-muted-foreground/75 transition-colors group-hover:text-foreground/90 group-hover:border-border/80">
{action.icon}
</div>
<div className="flex flex-col gap-0.5 min-w-0 pr-1">
<span className="font-medium text-[13px] text-foreground/90 tracking-tight leading-normal">
{action.title}
</span>
<span className="text-[11px] text-muted-foreground/60 leading-normal truncate sm:whitespace-normal">
{action.description}
</span>
</div>
</Suggestion>
</motion.div>
))}
Expand Down