feat(chat): enhance ChatViewport with conditional footer rendering and update ChatExamplePrompts for improved category selection

This commit is contained in:
Anish Sarkar 2026-06-02 22:52:48 +05:30
parent e15df9d949
commit 5fce4e1621
3 changed files with 87 additions and 53 deletions

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { ThreadPrimitive } from "@assistant-ui/react"; import { AuiIf, ThreadPrimitive } from "@assistant-ui/react";
import { ArrowDownIcon } from "lucide-react"; import { ArrowDownIcon } from "lucide-react";
import type { FC, ReactNode } from "react"; import type { FC, ReactNode } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -40,15 +40,17 @@ export const ChatViewport: FC<ChatViewportProps> = ({ children, footer }) => (
/> />
{children} {children}
{footer ? ( {footer ? (
<ThreadPrimitive.ViewportFooter <AuiIf condition={({ thread }) => !thread.isEmpty}>
className="aui-chat-composer-footer sticky bottom-0 z-20 -mx-4 mt-auto flex flex-col items-stretch bg-gradient-to-t from-main-panel from-60% to-transparent px-4 pt-6" <ThreadPrimitive.ViewportFooter
style={{ paddingBottom: "max(0.5rem, env(safe-area-inset-bottom))" }} className="aui-chat-composer-footer sticky bottom-0 z-20 -mx-4 mt-auto flex flex-col items-stretch bg-gradient-to-t from-main-panel from-60% to-transparent px-4 pt-6"
> style={{ paddingBottom: "max(0.5rem, env(safe-area-inset-bottom))" }}
<div className="aui-chat-composer-area relative mx-auto flex w-full max-w-(--thread-max-width) flex-col gap-3 overflow-visible"> >
<ChatScrollToBottom /> <div className="aui-chat-composer-area relative mx-auto flex w-full max-w-(--thread-max-width) flex-col gap-3 overflow-visible">
{footer} <ChatScrollToBottom />
</div> {footer}
</ThreadPrimitive.ViewportFooter> </div>
</ThreadPrimitive.ViewportFooter>
</AuiIf>
) : null} ) : null}
</ThreadPrimitive.Viewport> </ThreadPrimitive.Viewport>
); );

View file

@ -1,10 +1,17 @@
"use client"; "use client";
import { CornerDownLeft, Lightbulb } from "lucide-react"; import {
import { memo, useCallback } from "react"; FilePlus2,
Search,
Settings2,
type LucideIcon,
WandSparkles,
Workflow,
X,
} from "lucide-react";
import { memo, useCallback, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { CHAT_EXAMPLE_CATEGORIES } from "@/lib/chat/example-prompts"; import { CHAT_EXAMPLE_CATEGORIES } from "@/lib/chat/example-prompts";
interface ChatExamplePromptsProps { interface ChatExamplePromptsProps {
@ -12,6 +19,13 @@ interface ChatExamplePromptsProps {
onSelect: (prompt: string) => void; onSelect: (prompt: string) => void;
} }
const CATEGORY_ICONS: Record<string, LucideIcon> = {
search: Search,
create: FilePlus2,
automate: Workflow,
tools: Settings2,
};
const ExamplePromptButton = memo(function ExamplePromptButton({ const ExamplePromptButton = memo(function ExamplePromptButton({
prompt, prompt,
onSelect, onSelect,
@ -26,50 +40,72 @@ const ExamplePromptButton = memo(function ExamplePromptButton({
type="button" type="button"
variant="ghost" variant="ghost"
onClick={handleClick} onClick={handleClick}
className="h-auto w-full items-start justify-start gap-2.5 whitespace-normal rounded-md border bg-background px-3 py-2 text-left font-normal text-muted-foreground hover:bg-accent hover:text-accent-foreground" className="h-auto w-full items-start justify-start whitespace-normal rounded-lg bg-transparent px-2.5 py-1.5 text-left font-normal text-muted-foreground shadow-none hover:bg-foreground/10 hover:text-foreground sm:rounded-xl sm:px-3 sm:py-2"
> >
<CornerDownLeft <span className="min-w-0 text-pretty text-xs sm:text-sm">{prompt}</span>
aria-hidden="true"
className="mt-0.5 size-3.5 shrink-0 text-muted-foreground/70"
/>
<span className="min-w-0 text-pretty text-sm">{prompt}</span>
</Button> </Button>
); );
}); });
export function ChatExamplePrompts({ onSelect }: ChatExamplePromptsProps) { export function ChatExamplePrompts({ onSelect }: ChatExamplePromptsProps) {
const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null);
const activeCategory = CHAT_EXAMPLE_CATEGORIES.find(
(category) => category.id === activeCategoryId
);
return ( return (
<div className="mt-3 w-full select-none rounded-xl border border-dashed bg-muted/30 p-3 sm:p-4"> <div className="mt-2 w-full select-none sm:mt-3">
<div className="mb-2 flex items-center gap-2 px-1"> {activeCategory ? null : (
<Lightbulb aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" /> <div className="pb-1">
<p className="text-sm font-medium text-foreground"> <div className="mx-auto flex max-w-full flex-wrap items-center justify-center gap-1.5 px-0.5 sm:gap-2">
Not sure where to start? Try one of these {CHAT_EXAMPLE_CATEGORIES.map((category) => {
</p> const Icon = CATEGORY_ICONS[category.id] ?? WandSparkles;
</div>
<Tabs defaultValue={CHAT_EXAMPLE_CATEGORIES[0].id} className="w-full"> return (
<div className="overflow-x-auto pb-1"> <Button
<TabsList className="h-9 w-max"> key={category.id}
{CHAT_EXAMPLE_CATEGORIES.map((category) => ( type="button"
<TabsTrigger key={category.id} value={category.id} className="text-xs"> variant="secondary"
{category.label} onClick={() => setActiveCategoryId(category.id)}
</TabsTrigger> className="h-8 rounded-lg bg-muted px-3 text-xs font-medium text-muted-foreground shadow-sm shadow-black/5 hover:bg-foreground/10 hover:text-foreground dark:shadow-black/10 sm:h-10 sm:rounded-xl sm:px-4 sm:text-sm"
))} >
</TabsList> <Icon aria-hidden="true" className="size-3.5 sm:size-4" />
{category.label}
</Button>
);
})}
</div>
</div> </div>
{CHAT_EXAMPLE_CATEGORIES.map((category) => ( )}
<TabsContent key={category.id} value={category.id} className="mt-3">
<ScrollArea className="max-h-48"> {activeCategory ? (
<ul className="flex flex-col gap-2 pr-2"> <div className="overflow-hidden rounded-lg border border-input bg-muted shadow-sm shadow-black/5 dark:shadow-black/10 sm:rounded-xl">
{category.prompts.map((prompt) => ( <div className="flex items-center justify-between gap-2 px-3 py-2 sm:gap-3 sm:px-4 sm:py-3">
<li key={prompt}> <div className="flex min-w-0 items-center gap-2 text-xs font-medium text-foreground sm:text-sm">
<ExamplePromptButton prompt={prompt} onSelect={onSelect} /> <span className="truncate">{activeCategory.label}</span>
</li> </div>
))} <Button
</ul> type="button"
</ScrollArea> variant="ghost"
</TabsContent> size="icon"
))} onClick={() => setActiveCategoryId(null)}
</Tabs> aria-label="Close example prompts"
className="size-7 shrink-0 rounded-full text-muted-foreground hover:bg-foreground/10 hover:text-foreground sm:size-8"
>
<X aria-hidden="true" className="size-3.5 sm:size-4" />
</Button>
</div>
<ScrollArea className="max-h-52 sm:max-h-64">
<ul className="divide-y px-2 pb-2 sm:px-3 sm:pb-3">
{activeCategory.prompts.map((prompt) => (
<li key={prompt} className="py-0.5 sm:py-1">
<ExamplePromptButton prompt={prompt} onSelect={onSelect} />
</li>
))}
</ul>
</ScrollArea>
</div>
) : null}
</div> </div>
); );
} }

View file

@ -26,7 +26,6 @@ export const CHAT_EXAMPLE_CATEGORIES: ChatExampleCategory[] = [
"Summarize the key points across all the documents in this space.", "Summarize the key points across all the documents in this space.",
"What do my files say about [topic]? Answer with citations.", "What do my files say about [topic]? Answer with citations.",
"Find every mention of [keyword] and list the sources.", "Find every mention of [keyword] and list the sources.",
"Give me a cited briefing on the documents I added this week.",
"Compare these two documents and highlight the differences.", "Compare these two documents and highlight the differences.",
], ],
}, },
@ -37,7 +36,6 @@ export const CHAT_EXAMPLE_CATEGORIES: ChatExampleCategory[] = [
"Write a cited research report on [topic] from my documents.", "Write a cited research report on [topic] from my documents.",
"Turn this folder into a two-host podcast I can listen to.", "Turn this folder into a two-host podcast I can listen to.",
"Create a slide deck and a narrated video overview from these sources.", "Create a slide deck and a narrated video overview from these sources.",
"Generate an image to illustrate [concept] for my report.",
"Tailor my resume to this job description so it gets past ATS and lands an interview.", "Tailor my resume to this job description so it gets past ATS and lands an interview.",
], ],
}, },
@ -49,7 +47,6 @@ export const CHAT_EXAMPLE_CATEGORIES: ChatExampleCategory[] = [
"When a PDF lands in my Research folder, generate a cited AI summary.", "When a PDF lands in my Research folder, generate a cited AI summary.",
"Generate a weekly status report from my Slack and Gmail every Friday.", "Generate a weekly status report from my Slack and Gmail every Friday.",
"Build an automation that turns new meeting notes into minutes with action items.", "Build an automation that turns new meeting notes into minutes with action items.",
"Run a monthly competitor analysis report and save it to my workspace.",
], ],
}, },
{ {
@ -60,7 +57,6 @@ export const CHAT_EXAMPLE_CATEGORIES: ChatExampleCategory[] = [
"Post this research summary to my Notion workspace.", "Post this research summary to my Notion workspace.",
"Send these meeting action items to our team Slack channel.", "Send these meeting action items to our team Slack channel.",
"Create a Jira ticket from this bug report.", "Create a Jira ticket from this bug report.",
"Open a Linear issue from this feature request.",
], ],
}, },
]; ];