mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
feat(chat): enhance ChatViewport with conditional footer rendering and update ChatExamplePrompts for improved category selection
This commit is contained in:
parent
e15df9d949
commit
5fce4e1621
3 changed files with 87 additions and 53 deletions
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue