Update the home page suggestion cards

- Rather than chunky generic cards, make the suggested actions more action oriented, around the problem a user might want to solve. Give them follow-up options. Design still in progress.
This commit is contained in:
sabaimran
2024-12-21 18:57:19 -08:00
parent 2c7c16d93e
commit 95826393e1
6 changed files with 387 additions and 845 deletions

View File

@@ -168,6 +168,9 @@ const iconMap: IconMap = {
Broadcast: (color: string, width: string, height: string) => ( Broadcast: (color: string, width: string, height: string) => (
<Broadcast className={`${width} ${height} ${color} mr-2`} /> <Broadcast className={`${width} ${height} ${color} mr-2`} />
), ),
Image: (color: string, width: string, height: string) => (
<Image className={`${width} ${height} ${color} mr-2`} />
),
}; };
export function getIconForSlashCommand(command: string, customClassName: string | null = null) { export function getIconForSlashCommand(command: string, customClassName: string | null = null) {

View File

@@ -65,6 +65,12 @@ export interface AttachedFileText {
size: number; size: number;
} }
export enum ChatInputFocus {
MESSAGE = "message",
FILE = "file",
RESEARCH = "research",
}
interface ChatInputProps { interface ChatInputProps {
sendMessage: (message: string) => void; sendMessage: (message: string) => void;
sendImage: (image: string) => void; sendImage: (image: string) => void;
@@ -77,11 +83,15 @@ interface ChatInputProps {
agentColor?: string; agentColor?: string;
isResearchModeEnabled?: boolean; isResearchModeEnabled?: boolean;
setTriggeredAbort: (value: boolean) => void; setTriggeredAbort: (value: boolean) => void;
prefillMessage?: string;
focus?: ChatInputFocus;
} }
export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((props, ref) => { export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((props, ref) => {
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const fileInputButtonRef = useRef<HTMLButtonElement>(null);
const researchModeRef = useRef<HTMLButtonElement>(null);
const [warning, setWarning] = useState<string | null>(null); const [warning, setWarning] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -125,6 +135,22 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
} }
}, [uploading]); }, [uploading]);
useEffect(() => {
if (props.prefillMessage) {
setMessage(props.prefillMessage);
}
}, [props.prefillMessage]);
useEffect(() => {
if (props.focus === ChatInputFocus.MESSAGE) {
chatInputRef?.current?.focus();
} else if (props.focus === ChatInputFocus.FILE) {
fileInputButtonRef.current?.focus();
} else if (props.focus === ChatInputFocus.RESEARCH) {
researchModeRef.current?.focus();
}
}, [props.focus]);
useEffect(() => { useEffect(() => {
async function fetchImageData() { async function fetchImageData() {
if (imagePaths.length > 0) { if (imagePaths.length > 0) {
@@ -630,6 +656,7 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
className="!bg-none p-0 m-2 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500" className="!bg-none p-0 m-2 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
disabled={props.sendDisabled || !props.isLoggedIn} disabled={props.sendDisabled || !props.isLoggedIn}
onClick={handleFileButtonClick} onClick={handleFileButtonClick}
ref={fileInputButtonRef}
> >
<Paperclip className="w-8 h-8" /> <Paperclip className="w-8 h-8" />
</Button> </Button>
@@ -732,6 +759,7 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
variant="ghost" variant="ghost"
className="float-right justify-center gap-1 flex items-center p-1.5 mr-2 h-fit" className="float-right justify-center gap-1 flex items-center p-1.5 mr-2 h-fit"
disabled={props.sendDisabled || !props.isLoggedIn} disabled={props.sendDisabled || !props.isLoggedIn}
ref={researchModeRef}
onClick={() => { onClick={() => {
setUseResearchMode(!useResearchMode); setUseResearchMode(!useResearchMode);
chatInputRef?.current?.focus(); chatInputRef?.current?.focus();

View File

@@ -1,25 +1,32 @@
"use client"; "use client";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription } from "@/components/ui/card";
import styles from "./suggestions.module.css"; import styles from "./suggestions.module.css";
import { converColorToBgGradient } from "@/app/common/colorUtils";
import { convertSuggestionTitleToIconClass } from "./suggestionsData"; import { convertSuggestionTitleToIconClass } from "./suggestionsData";
import { ArrowLeft, ArrowRight } from "@phosphor-icons/react";
interface SuggestionCardProps { interface StepOneSuggestionCardProps {
title: string; title: string;
body: string; body: string;
link: string;
color: string; color: string;
} }
export default function SuggestionCard(data: SuggestionCardProps) { interface StepOneSuggestionRevertCardProps extends StepOneSuggestionCardProps {
const cardClassName = `${styles.card} md:w-full md:h-fit sm:w-full h-fit md:w-[200px] md:h-[180px] cursor-pointer md:p-2`; onClick: () => void;
}
interface StepTwoSuggestionCardProps {
prompt: string;
}
export function StepOneSuggestionCard(data: StepOneSuggestionCardProps) {
const cardClassName = `${styles.card} md:w-full md:h-fit sm:w-full h-fit md:w-[200px] cursor-pointer md:p-2`;
const descriptionClassName = `${styles.text} dark:text-white`; const descriptionClassName = `${styles.text} dark:text-white`;
const cardContent = ( const cardContent = (
<Card className={cardClassName}> <Card className={cardClassName}>
<div className="flex w-full"> <div className="flex w-full">
<CardContent className="m-0 p-2 w-full"> <CardContent className="m-0 p-2 w-full flex flex-row">
{convertSuggestionTitleToIconClass(data.title, data.color.toLowerCase())} {convertSuggestionTitleToIconClass(data.title, data.color.toLowerCase())}
<CardDescription <CardDescription
className={`${descriptionClassName} sm:line-clamp-2 md:line-clamp-4 pt-1 break-words whitespace-pre-wrap max-w-full`} className={`${descriptionClassName} sm:line-clamp-2 md:line-clamp-4 pt-1 break-words whitespace-pre-wrap max-w-full`}
@@ -31,11 +38,45 @@ export default function SuggestionCard(data: SuggestionCardProps) {
</Card> </Card>
); );
return data.link ? ( return cardContent;
<a href={data.link} className="no-underline"> }
{cardContent}
</a> export function StepTwoSuggestionCard(data: StepTwoSuggestionCardProps) {
) : ( const cardClassName = `${styles.card} md:h-fit sm:w-full h-fit cursor-pointer md:p-2`;
cardContent
return (
<Card className={cardClassName}>
<div className="flex w-full items-center">
<CardContent className="m-0 p-2 w-full flex flex-row items-center">
<ArrowRight className="w-6 h-6 text-muted-foreground inline-flex mr-1" />
<CardDescription
className={`sm:line-clamp-2 md:line-clamp-4 break-words whitespace-pre-wrap max-w-full text-sm text-wrap text-black dark:text-white`}
>
{data.prompt}
</CardDescription>
</CardContent>
</div>
</Card>
);
}
export function StepOneSuggestionRevertCard(data: StepOneSuggestionRevertCardProps) {
const cardClassName = `${styles.card} md:w-full md:h-fit sm:w-full h-fit md:w-[200px] cursor-pointer md:p-2 animate-fade-in-up`;
const descriptionClassName = `${styles.text} dark:text-white`;
return (
<Card className={cardClassName} onClick={data.onClick}>
<div className="flex w-full">
<CardContent className="m-0 p-2 w-full flex flex-row">
<ArrowLeft className="w-4 h-4 text-muted-foreground inline-flex mr-1" />
{convertSuggestionTitleToIconClass(data.title, data.color.toLowerCase())}
<CardDescription
className={`${descriptionClassName} sm:line-clamp-2 md:line-clamp-4 pt-1 break-words whitespace-pre-wrap max-w-full`}
>
{data.body}
</CardDescription>
</CardContent>
</div>
</Card>
); );
} }

View File

@@ -8,14 +8,25 @@ import useSWR from "swr";
import { ArrowCounterClockwise } from "@phosphor-icons/react"; import { ArrowCounterClockwise } from "@phosphor-icons/react";
import { Card, CardTitle } from "@/components/ui/card"; import { Card, CardTitle } from "@/components/ui/card";
import SuggestionCard from "@/app/components/suggestions/suggestionCard"; import {
StepOneSuggestionCard,
StepOneSuggestionRevertCard,
StepTwoSuggestionCard,
} from "@/app/components/suggestions/suggestionCard";
import Loading from "@/app/components/loading/loading"; import Loading from "@/app/components/loading/loading";
import { import {
AttachedFileText, AttachedFileText,
ChatInputArea, ChatInputArea,
ChatInputFocus,
ChatOptions, ChatOptions,
} from "@/app/components/chatInputArea/chatInputArea"; } from "@/app/components/chatInputArea/chatInputArea";
import { Suggestion, suggestionsData } from "@/app/components/suggestions/suggestionsData"; import {
StepOneSuggestion,
stepOneSuggestions,
StepTwoSuggestion,
getStepTwoSuggestions,
SuggestionType,
} from "@/app/components/suggestions/suggestionsData";
import LoginPrompt from "@/app/components/loginPrompt/loginPrompt"; import LoginPrompt from "@/app/components/loginPrompt/loginPrompt";
import { import {
@@ -38,6 +49,7 @@ import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/s
import { AppSidebar } from "./components/appSidebar/appSidebar"; import { AppSidebar } from "./components/appSidebar/appSidebar";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { KhojLogoType } from "./components/logo/khojLogo"; import { KhojLogoType } from "./components/logo/khojLogo";
import { Button } from "@/components/ui/button";
interface ChatBodyDataProps { interface ChatBodyDataProps {
chatOptionsData: ChatOptions | null; chatOptionsData: ChatOptions | null;
@@ -59,10 +71,19 @@ function FisherYatesShuffle(array: any[]) {
function ChatBodyData(props: ChatBodyDataProps) { function ChatBodyData(props: ChatBodyDataProps) {
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const [prefilledMessage, setPrefilledMessage] = useState("");
const [chatInputFocus, setChatInputFocus] = useState<ChatInputFocus>(ChatInputFocus.MESSAGE);
const [images, setImages] = useState<string[]>([]); const [images, setImages] = useState<string[]>([]);
const [processingMessage, setProcessingMessage] = useState(false); const [processingMessage, setProcessingMessage] = useState(false);
const [greeting, setGreeting] = useState(""); const [greeting, setGreeting] = useState("");
const [shuffledOptions, setShuffledOptions] = useState<Suggestion[]>([]); const [stepOneSuggestionOptions, setStepOneSuggestionOptions] = useState<StepOneSuggestion[]>(
[],
);
const [stepTwoSuggestionOptions, setStepTwoSuggestionOptions] = useState<StepTwoSuggestion[]>(
[],
);
const [selectedStepOneSuggestion, setSelectedStepOneSuggestion] =
useState<StepOneSuggestion | null>(null);
const [hoveredAgent, setHoveredAgent] = useState<string | null>(null); const [hoveredAgent, setHoveredAgent] = useState<string | null>(null);
const debouncedHoveredAgent = useDebounce(hoveredAgent, 500); const debouncedHoveredAgent = useDebounce(hoveredAgent, 500);
const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isPopoverOpen, setIsPopoverOpen] = useState(false);
@@ -103,8 +124,8 @@ function ChatBodyData(props: ChatBodyDataProps) {
}; };
function shuffleAndSetOptions() { function shuffleAndSetOptions() {
const shuffled = FisherYatesShuffle(suggestionsData); const shuffled = FisherYatesShuffle(stepOneSuggestions);
setShuffledOptions(shuffled.slice(0, 3)); setStepOneSuggestionOptions(shuffled.slice(0, 3));
} }
useEffect(() => { useEffect(() => {
@@ -148,8 +169,8 @@ function ChatBodyData(props: ChatBodyDataProps) {
setAgentIcons(agentIcons); setAgentIcons(agentIcons);
}, [agentsData, props.isMobileWidth]); }, [agentsData, props.isMobileWidth]);
function shuffleSuggestionsCards() { function showAllSuggestionsCards() {
shuffleAndSetOptions(); setStepOneSuggestionOptions(stepOneSuggestions);
} }
useEffect(() => { useEffect(() => {
@@ -193,27 +214,22 @@ function ChatBodyData(props: ChatBodyDataProps) {
return () => scrollAreaEl?.removeEventListener("scroll", handleScroll); return () => scrollAreaEl?.removeEventListener("scroll", handleScroll);
}, []); }, []);
function fillArea(link: string, type: string, prompt: string) { function clickStepOneSuggestion(suggestion: StepOneSuggestion) {
if (!link) {
let message_str = ""; let message_str = "";
prompt = prompt.charAt(0).toLowerCase() + prompt.slice(1); let prompt =
suggestion.description.charAt(0).toLowerCase() + suggestion.description.slice(1);
if (type === "Online Search") { if (suggestion.type === "Paint") {
message_str = "/online " + prompt;
} else if (type === "Paint") {
message_str = "/image " + prompt; message_str = "/image " + prompt;
} else { } else {
message_str = prompt; message_str = prompt;
} }
// Get the textarea element
const message_area = document.getElementById("message") as HTMLTextAreaElement;
if (message_area) { setPrefilledMessage(message_str);
// Update the value directly const stepTwoSuggestions = getStepTwoSuggestions(suggestion.type);
message_area.value = message_str; setSelectedStepOneSuggestion(suggestion);
setMessage(message_str); setStepTwoSuggestionOptions(stepTwoSuggestions);
} setChatInputFocus(ChatInputFocus.FILE);
}
} }
return ( return (
@@ -306,13 +322,15 @@ function ChatBodyData(props: ChatBodyDataProps) {
</ScrollArea> </ScrollArea>
)} )}
</div> </div>
<div className={`mx-auto ${props.isMobileWidth ? "w-full" : "w-fit max-w-screen-md"}`}> <div className={`mx-auto ${props.isMobileWidth ? "w-full" : "w-full max-w-screen-md"}`}>
{!props.isMobileWidth && ( {!props.isMobileWidth && (
<div <div
className={`w-full ${styles.inputBox} shadow-lg bg-background align-middle items-center justify-center px-3 py-1 dark:bg-neutral-700 border-stone-100 dark:border-none dark:shadow-none rounded-2xl`} className={`w-full ${styles.inputBox} shadow-lg bg-background align-middle items-center justify-center px-3 py-1 dark:bg-neutral-700 border-stone-100 dark:border-none dark:shadow-none rounded-2xl`}
> >
<ChatInputArea <ChatInputArea
isLoggedIn={props.isLoggedIn} isLoggedIn={props.isLoggedIn}
prefillMessage={prefilledMessage}
focus={chatInputFocus}
sendMessage={(message) => setMessage(message)} sendMessage={(message) => setMessage(message)}
sendImage={(image) => setImages((prevImages) => [...prevImages, image])} sendImage={(image) => setImages((prevImages) => [...prevImages, image])}
sendDisabled={processingMessage} sendDisabled={processingMessage}
@@ -327,18 +345,15 @@ function ChatBodyData(props: ChatBodyDataProps) {
</div> </div>
)} )}
<div <div
className={`${styles.suggestions} w-full ${props.isMobileWidth ? "grid" : "flex flex-row"} justify-center items-center`} className={`${styles.suggestions} w-full ${props.isMobileWidth ? "grid" : "grid grid-cols-3"} justify-center items-center`}
> >
{shuffledOptions.map((suggestion, index) => ( {stepTwoSuggestionOptions.length == 0 &&
stepOneSuggestionOptions.map((suggestion, index) => (
<div <div
key={`${suggestion.type} ${suggestion.description}`} key={`${suggestion.type} ${suggestion.description}`}
onClick={(event) => { onClick={(event) => {
if (props.isLoggedIn) { if (props.isLoggedIn) {
fillArea( clickStepOneSuggestion(suggestion);
suggestion.link,
suggestion.type,
suggestion.description,
);
} else { } else {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@@ -346,24 +361,58 @@ function ChatBodyData(props: ChatBodyDataProps) {
} }
}} }}
> >
<SuggestionCard <StepOneSuggestionCard
key={suggestion.type + Math.random()} key={suggestion.type + Math.random()}
title={suggestion.type} title={suggestion.type}
body={suggestion.description} body={suggestion.description}
link={suggestion.link}
color={suggestion.color} color={suggestion.color}
/> />
</div> </div>
))} ))}
</div> </div>
{stepTwoSuggestionOptions.length == 0 &&
stepOneSuggestionOptions.length < stepOneSuggestions.length && (
<div className="flex items-center justify-center margin-auto"> <div className="flex items-center justify-center margin-auto">
<button <button
onClick={shuffleSuggestionsCards} onClick={showAllSuggestionsCards}
className="m-2 p-1.5 rounded-lg dark:hover:bg-[var(--background-color)] hover:bg-stone-100 border border-stone-100 text-sm text-stone-500 dark:text-stone-300 dark:border-neutral-700" className="m-2 p-1.5 rounded-lg dark:hover:bg-[var(--background-color)] hover:bg-stone-100 border border-stone-100 text-sm text-stone-500 dark:text-stone-300 dark:border-neutral-700"
> >
More Ideas <ArrowCounterClockwise className="h-4 w-4 inline" /> More Actions <ArrowCounterClockwise className="h-4 w-4 inline" />
</button> </button>
</div> </div>
)}
{selectedStepOneSuggestion && (
<StepOneSuggestionRevertCard
title={selectedStepOneSuggestion.type}
body={selectedStepOneSuggestion.description}
color={selectedStepOneSuggestion.color}
onClick={() => {
setSelectedStepOneSuggestion(null);
setStepTwoSuggestionOptions([]);
setChatInputFocus(ChatInputFocus.MESSAGE);
}}
/>
)}
{stepTwoSuggestionOptions.length > 0 && (
<div
className={`w-full ${props.isMobileWidth ? "grid" : "grid grid-cols-1"} justify-center items-center gap-2 pt-2`}
>
{stepTwoSuggestionOptions.map((suggestion, index) => (
<div
key={`${suggestion.prompt} ${index}`}
className={`w-full cursor-pointer animate-fade-in-up`}
onClick={(event) => {
setMessage(suggestion.prompt);
}}
>
<StepTwoSuggestionCard
key={suggestion.prompt}
prompt={suggestion.prompt}
/>
</div>
))}
</div>
)}
</div> </div>
{props.isMobileWidth && ( {props.isMobileWidth && (
<> <>

View File

@@ -133,11 +133,16 @@ const config = {
opacity: "0", opacity: "0",
}, },
}, },
fadeInUp: {
"0%": { opacity: "0", transform: "translateY(20px)" },
"100%": { opacity: "1", transform: "translateY(0)" },
},
}, },
animation: { animation: {
"accordion-down": "accordion-down 0.2s ease-out", "accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite", "caret-blink": "caret-blink 1.25s ease-out infinite",
"fade-in-up": "fadeInUp 0.3s ease-out",
}, },
}, },
}, },