mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-24 21:38:09 +02:00
feat: implement dynamic greeting and thinking steps display in Thread component
This commit is contained in:
parent
1cbb1b5d66
commit
2f622891ae
1 changed files with 128 additions and 60 deletions
|
|
@ -21,7 +21,9 @@ import {
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
SquareIcon,
|
SquareIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
import {
|
import {
|
||||||
ComposerAddAttachment,
|
ComposerAddAttachment,
|
||||||
ComposerAttachments,
|
ComposerAttachments,
|
||||||
|
|
@ -32,6 +34,7 @@ import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
||||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||||
|
|
||||||
export const Thread: FC = () => {
|
export const Thread: FC = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -59,7 +62,11 @@ export const Thread: FC = () => {
|
||||||
|
|
||||||
<ThreadPrimitive.ViewportFooter className="aui-thread-viewport-footer sticky bottom-0 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4 md:pb-6">
|
<ThreadPrimitive.ViewportFooter className="aui-thread-viewport-footer sticky bottom-0 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4 md:pb-6">
|
||||||
<ThreadScrollToBottom />
|
<ThreadScrollToBottom />
|
||||||
<Composer />
|
<AssistantIf condition={({ thread }) => !thread.isEmpty}>
|
||||||
|
<div className="fade-in slide-in-from-bottom-4 animate-in duration-500 ease-out fill-mode-both">
|
||||||
|
<Composer />
|
||||||
|
</div>
|
||||||
|
</AssistantIf>
|
||||||
</ThreadPrimitive.ViewportFooter>
|
</ThreadPrimitive.ViewportFooter>
|
||||||
</ThreadPrimitive.Viewport>
|
</ThreadPrimitive.Viewport>
|
||||||
</ThreadPrimitive.Root>
|
</ThreadPrimitive.Root>
|
||||||
|
|
@ -80,62 +87,109 @@ const ThreadScrollToBottom: FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThreadWelcome: FC = () => {
|
const getTimeBasedGreeting = (userEmail?: string): string => {
|
||||||
return (
|
const hour = new Date().getHours();
|
||||||
<div className="aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col">
|
|
||||||
<div className="aui-thread-welcome-center flex w-full grow flex-col items-center justify-center">
|
// Extract first name from email if available
|
||||||
<div className="aui-thread-welcome-message flex size-full flex-col justify-center px-4">
|
const firstName = userEmail
|
||||||
<h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in font-semibold text-2xl duration-200">
|
? userEmail.split("@")[0].split(".")[0].charAt(0).toUpperCase() +
|
||||||
Hello there!
|
userEmail.split("@")[0].split(".")[0].slice(1)
|
||||||
</h1>
|
: null;
|
||||||
<p className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in text-muted-foreground text-xl delay-75 duration-200">
|
|
||||||
How can I help you today?
|
// Array of greeting variations for each time period
|
||||||
</p>
|
const morningGreetings = [
|
||||||
</div>
|
"Good morning",
|
||||||
</div>
|
"Rise and shine",
|
||||||
<ThreadSuggestions />
|
"Morning",
|
||||||
</div>
|
"Hey there",
|
||||||
);
|
"Welcome back",
|
||||||
|
];
|
||||||
|
|
||||||
|
const afternoonGreetings = [
|
||||||
|
"Good afternoon",
|
||||||
|
"Afternoon",
|
||||||
|
"Hey there",
|
||||||
|
"Welcome back",
|
||||||
|
"Hope you're having a great day",
|
||||||
|
];
|
||||||
|
|
||||||
|
const eveningGreetings = [
|
||||||
|
"Good evening",
|
||||||
|
"Evening",
|
||||||
|
"Hey there",
|
||||||
|
"Welcome back",
|
||||||
|
"Hope you had a great day",
|
||||||
|
];
|
||||||
|
|
||||||
|
const nightGreetings = [
|
||||||
|
"Late night",
|
||||||
|
"Still up",
|
||||||
|
"Hey there",
|
||||||
|
"Welcome back",
|
||||||
|
"Burning the midnight oil",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Select a random greeting based on time
|
||||||
|
let greeting: string;
|
||||||
|
if (hour < 12) {
|
||||||
|
greeting = morningGreetings[Math.floor(Math.random() * morningGreetings.length)];
|
||||||
|
} else if (hour < 17) {
|
||||||
|
greeting = afternoonGreetings[Math.floor(Math.random() * afternoonGreetings.length)];
|
||||||
|
} else if (hour < 21) {
|
||||||
|
greeting = eveningGreetings[Math.floor(Math.random() * eveningGreetings.length)];
|
||||||
|
} else {
|
||||||
|
greeting = nightGreetings[Math.floor(Math.random() * nightGreetings.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add personalization with first name if available
|
||||||
|
if (firstName) {
|
||||||
|
return `${greeting}, ${firstName}!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${greeting}!`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SUGGESTIONS = [
|
const ThreadWelcome: FC = () => {
|
||||||
{
|
const { data: user } = useAtomValue(currentUserAtom);
|
||||||
title: "What's the weather",
|
|
||||||
label: "in San Francisco?",
|
|
||||||
prompt: "What's the weather in San Francisco?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Explain React hooks",
|
|
||||||
label: "like useState and useEffect",
|
|
||||||
prompt: "Explain React hooks like useState and useEffect",
|
|
||||||
},
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const ThreadSuggestions: FC = () => {
|
|
||||||
return (
|
return (
|
||||||
<div className="aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4">
|
<div className="aui-thread-welcome-root mx-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center px-4 relative">
|
||||||
{SUGGESTIONS.map((suggestion, index) => (
|
{/* Greeting positioned near the composer */}
|
||||||
<div
|
<div className="aui-thread-welcome-message absolute top-1/2 left-0 right-0 flex flex-col items-center text-center z-10 -translate-y-[calc(50%+100px)]">
|
||||||
key={suggestion.prompt}
|
<h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in text-4xl delay-100 duration-500 ease-out fill-mode-both flex items-center gap-3">
|
||||||
className="aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-2 @md:nth-[n+3]:block nth-[n+3]:hidden animate-in fill-mode-both duration-200"
|
{/** biome-ignore lint/a11y/noStaticElementInteractions: wrong lint error, this is a workaround to fix the lint error */}
|
||||||
style={{ animationDelay: `${100 + index * 50}ms` }}
|
<div
|
||||||
>
|
className="relative cursor-pointer"
|
||||||
<ThreadPrimitive.Suggestion prompt={suggestion.prompt} autoSend asChild>
|
onMouseMove={(e) => {
|
||||||
<Button
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
variant="ghost"
|
const x = (e.clientX - rect.left - rect.width / 2) / 3;
|
||||||
className="aui-thread-welcome-suggestion h-auto w-full @md:flex-col flex-wrap items-start justify-start gap-1 rounded-2xl border px-4 py-3 text-left text-sm transition-colors hover:bg-muted"
|
const y = (e.clientY - rect.top - rect.height / 2) / 3;
|
||||||
aria-label={suggestion.prompt}
|
e.currentTarget.style.setProperty("--mag-x", `${x}px`);
|
||||||
>
|
e.currentTarget.style.setProperty("--mag-y", `${y}px`);
|
||||||
<span className="aui-thread-welcome-suggestion-text-1 font-medium">
|
}}
|
||||||
{suggestion.title}
|
onMouseLeave={(e) => {
|
||||||
</span>
|
e.currentTarget.style.setProperty("--mag-x", "0px");
|
||||||
<span className="aui-thread-welcome-suggestion-text-2 text-muted-foreground">
|
e.currentTarget.style.setProperty("--mag-y", "0px");
|
||||||
{suggestion.label}
|
}}
|
||||||
</span>
|
>
|
||||||
</Button>
|
<Image
|
||||||
</ThreadPrimitive.Suggestion>
|
src="/icon-128.png"
|
||||||
</div>
|
alt="SurfSense"
|
||||||
))}
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="rounded-full transition-transform duration-200 ease-out"
|
||||||
|
style={{
|
||||||
|
transform: "translate(var(--mag-x, 0), var(--mag-y, 0))",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{getTimeBasedGreeting(user?.email)}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
{/* Composer centered in the middle of the screen */}
|
||||||
|
<div className="fade-in slide-in-from-bottom-3 animate-in delay-200 duration-500 ease-out fill-mode-both w-full flex items-center justify-center absolute top-1/2 left-0 right-0 -translate-y-1/2">
|
||||||
|
<Composer />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -143,10 +197,10 @@ const ThreadSuggestions: FC = () => {
|
||||||
const Composer: FC = () => {
|
const Composer: FC = () => {
|
||||||
return (
|
return (
|
||||||
<ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col">
|
<ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col">
|
||||||
<ComposerPrimitive.AttachmentDropzone className="aui-composer-attachment-dropzone flex w-full flex-col rounded-2xl border border-input bg-background px-1 pt-2 outline-none transition-shadow has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-2 has-[textarea:focus-visible]:ring-ring/20 data-[dragging=true]:border-ring data-[dragging=true]:border-dashed data-[dragging=true]:bg-accent/50">
|
<ComposerPrimitive.AttachmentDropzone className="aui-composer-attachment-dropzone flex w-full flex-col rounded-2xl border-input bg-muted px-1 pt-2 outline-none transition-shadow has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-2 has-[textarea:focus-visible]:ring-ring/20 data-[dragging=true]:border-ring data-[dragging=true]:border-dashed data-[dragging=true]:bg-accent/50">
|
||||||
<ComposerAttachments />
|
<ComposerAttachments />
|
||||||
<ComposerPrimitive.Input
|
<ComposerPrimitive.Input
|
||||||
placeholder="Send a message..."
|
placeholder="Ask SurfSense"
|
||||||
className="aui-composer-input mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-0"
|
className="aui-composer-input mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-0"
|
||||||
rows={1}
|
rows={1}
|
||||||
autoFocus
|
autoFocus
|
||||||
|
|
@ -170,6 +224,14 @@ const ComposerAction: FC = () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check if composer text is empty
|
||||||
|
const isComposerEmpty = useAssistantState(({ composer }) => {
|
||||||
|
const text = composer.text?.trim() || "";
|
||||||
|
return text.length === 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSendDisabled = hasProcessingAttachments || isComposerEmpty;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="aui-composer-action-wrapper relative mx-2 mb-2 flex items-center justify-between">
|
<div className="aui-composer-action-wrapper relative mx-2 mb-2 flex items-center justify-between">
|
||||||
<ComposerAddAttachment />
|
<ComposerAddAttachment />
|
||||||
|
|
@ -183,19 +245,25 @@ const ComposerAction: FC = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AssistantIf condition={({ thread }) => !thread.isRunning}>
|
<AssistantIf condition={({ thread }) => !thread.isRunning}>
|
||||||
<ComposerPrimitive.Send asChild disabled={hasProcessingAttachments}>
|
<ComposerPrimitive.Send asChild disabled={isSendDisabled}>
|
||||||
<TooltipIconButton
|
<TooltipIconButton
|
||||||
tooltip={hasProcessingAttachments ? "Wait for attachments to process" : "Send message"}
|
tooltip={
|
||||||
|
hasProcessingAttachments
|
||||||
|
? "Wait for attachments to process"
|
||||||
|
: isComposerEmpty
|
||||||
|
? "Enter a message to send"
|
||||||
|
: "Send message"
|
||||||
|
}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="default"
|
variant="default"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={cn(
|
className={cn(
|
||||||
"aui-composer-send size-8 rounded-full",
|
"aui-composer-send size-8 rounded-full",
|
||||||
hasProcessingAttachments && "cursor-not-allowed opacity-50"
|
isSendDisabled && "cursor-not-allowed opacity-50"
|
||||||
)}
|
)}
|
||||||
aria-label="Send message"
|
aria-label="Send message"
|
||||||
disabled={hasProcessingAttachments}
|
disabled={isSendDisabled}
|
||||||
>
|
>
|
||||||
<ArrowUpIcon className="aui-composer-send-icon size-4" />
|
<ArrowUpIcon className="aui-composer-send-icon size-4" />
|
||||||
</TooltipIconButton>
|
</TooltipIconButton>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue