Revamp new chat screen for chat UI

This commit is contained in:
Weves
2023-12-30 17:35:05 -08:00
committed by Chris Weaver
parent f883611e94
commit ae9b556876
9 changed files with 303 additions and 27 deletions

View File

@ -67,7 +67,7 @@ def get_user_chat_sessions(
@router.get("/get-chat-session/{session_id}")
def get_chat_session_messages(
def get_chat_session(
session_id: int,
user: User | None = Depends(current_user),
db_session: Session = Depends(get_session),
@ -88,6 +88,7 @@ def get_chat_session_messages(
return ChatSessionDetailResponse(
chat_session_id=session_id,
description=chat_session.description,
persona_id=chat_session.persona_id,
messages=[
translate_db_message_to_chat_message_detail(msg) for msg in session_messages
],

View File

@ -145,6 +145,7 @@ class ChatMessageDetail(BaseModel):
class ChatSessionDetailResponse(BaseModel):
chat_session_id: int
description: str
persona_id: int
messages: list[ChatMessageDetail]

View File

@ -39,6 +39,7 @@ import { SelectedDocuments } from "./modifiers/SelectedDocuments";
import { usePopup } from "@/components/admin/connectors/Popup";
import { ResizableSection } from "@/components/resizable/ResizableSection";
import { DanswerInitializingLoader } from "@/components/DanswerInitializingLoader";
import { ChatIntro } from "./ChatIntro";
const MAX_INPUT_HEIGHT = 200;
@ -48,6 +49,7 @@ export const Chat = ({
availableSources,
availableDocumentSets,
availablePersonas,
defaultSelectedPersonaId,
documentSidebarInitialWidth,
shouldhideBeforeScroll,
}: {
@ -56,6 +58,7 @@ export const Chat = ({
availableSources: ValidSources[];
availableDocumentSets: DocumentSet[];
availablePersonas: Persona[];
defaultSelectedPersonaId?: number; // what persona to default to
documentSidebarInitialWidth?: number;
shouldhideBeforeScroll?: boolean;
}) => {
@ -76,6 +79,15 @@ export const Chat = ({
async function initialSessionFetch() {
if (existingChatSessionId === null) {
setIsFetchingChatMessages(false);
if (defaultSelectedPersonaId !== undefined) {
setSelectedPersona(
availablePersonas.find(
(persona) => persona.id === defaultSelectedPersonaId
)
);
} else {
setSelectedPersona(undefined);
}
setMessageHistory([]);
return;
}
@ -85,6 +97,11 @@ export const Chat = ({
`/api/chat/get-chat-session/${existingChatSessionId}`
);
const chatSession = (await response.json()) as BackendChatSession;
setSelectedPersona(
availablePersonas.find(
(persona) => persona.id === chatSession.persona_id
)
);
const newMessageHistory = processRawChatHistory(chatSession.messages);
setMessageHistory(newMessageHistory);
@ -126,8 +143,23 @@ export const Chat = ({
? availablePersonas.find(
(persona) => persona.id === existingChatSessionPersonaId
)
: availablePersonas[0]
: defaultSelectedPersonaId !== undefined
? availablePersonas.find(
(persona) => persona.id === defaultSelectedPersonaId
)
: undefined
);
const livePersona = selectedPersona || availablePersonas[0];
useEffect(() => {
if (messageHistory.length === 0) {
setSelectedPersona(
availablePersonas.find(
(persona) => persona.id === defaultSelectedPersonaId
)
);
}
}, [defaultSelectedPersonaId]);
const filterManager = useFilters();
@ -212,7 +244,7 @@ export const Chat = ({
let currChatSessionId: number;
let isNewSession = chatSessionId === null;
if (isNewSession) {
currChatSessionId = await createChatSession(selectedPersona?.id || 0);
currChatSessionId = await createChatSession(livePersona?.id || 0);
} else {
currChatSessionId = chatSessionId as number;
}
@ -405,15 +437,16 @@ export const Chat = ({
className="w-full h-screen flex flex-col overflow-y-auto relative"
ref={scrollableDivRef}
>
{selectedPersona && (
{livePersona && (
<div className="sticky top-0 left-80 z-10 w-full bg-background/90">
<div className="ml-2 p-1 rounded mt-2 w-fit">
<ChatPersonaSelector
personas={availablePersonas}
selectedPersonaId={selectedPersona?.id}
selectedPersonaId={livePersona.id}
onPersonaChange={(persona) => {
if (persona) {
setSelectedPersona(persona);
router.push(`/chat?personaId=${persona.id}`);
}
}}
/>
@ -424,23 +457,15 @@ export const Chat = ({
{messageHistory.length === 0 &&
!isFetchingChatMessages &&
!isStreaming && (
<div className="flex justify-center items-center h-full">
<div className="px-8 w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
<div className="flex">
<div className="mx-auto h-[80px] w-[80px]">
<Image
src="/logo.png"
alt="Logo"
width="1419"
height="1520"
/>
</div>
</div>
<div className="mx-auto text-2xl font-bold text-strong p-4 w-fit">
What are you looking for today?
</div>
</div>
</div>
<ChatIntro
availableSources={availableSources}
availablePersonas={availablePersonas}
selectedPersona={selectedPersona}
handlePersonaSelect={(persona) => {
setSelectedPersona(persona);
router.push(`/chat?personaId=${persona.id}`);
}}
/>
)}
<div

View File

@ -0,0 +1,230 @@
import { listSourceMetadata } from "@/lib/sources";
import { ValidSources } from "@/lib/types";
import Image from "next/image";
import { Persona } from "../admin/personas/interfaces";
import { Divider } from "@tremor/react";
import { FiBookmark, FiCpu, FiInfo, FiX, FiZoomIn } from "react-icons/fi";
import { HoverPopup } from "@/components/HoverPopup";
import { Modal } from "@/components/Modal";
import { useState } from "react";
import { FaRobot } from "react-icons/fa";
const MAX_PERSONAS_TO_DISPLAY = 4;
function HelperItemDisplay({
title,
description,
}: {
title: string;
description: string;
}) {
return (
<div className="cursor-default hover:bg-hover-light border border-border rounded py-2 px-4">
<div className="text-emphasis font-bold text-lg flex">{title}</div>
<div className="text-sm">{description}</div>
</div>
);
}
function AllPersonaOptionDisplay({
availablePersonas,
handlePersonaSelect,
handleClose,
}: {
availablePersonas: Persona[];
handlePersonaSelect: (persona: Persona) => void;
handleClose: () => void;
}) {
return (
<Modal onOutsideClick={handleClose}>
<div className="px-8 py-12">
<div className="flex w-full border-b border-border mb-4 pb-4">
<h2 className="text-xl text-strong font-bold flex">
<div className="p-1 bg-ai rounded-lg h-fit my-auto mr-2">
<div className="text-inverted">
<FiCpu size={16} className="my-auto mx-auto" />
</div>
</div>
All Available Assistants
</h2>
<div
onClick={handleClose}
className="ml-auto p-1 rounded hover:bg-hover"
>
<FiX size={18} />
</div>
</div>
<div className="flex flex-col gap-y-4 max-h-96 overflow-y-auto pb-4 px-2">
{availablePersonas.map((persona) => (
<div
key={persona.id}
onClick={() => {
handleClose();
handlePersonaSelect(persona);
}}
>
<HelperItemDisplay
title={persona.name}
description={persona.description}
/>
</div>
))}
</div>
</div>
</Modal>
);
}
export function ChatIntro({
availableSources,
availablePersonas,
selectedPersona,
handlePersonaSelect,
}: {
availableSources: ValidSources[];
availablePersonas: Persona[];
selectedPersona?: Persona;
handlePersonaSelect: (persona: Persona) => void;
}) {
const [isAllPersonaOptionVisible, setIsAllPersonaOptionVisible] =
useState(false);
const allSources = listSourceMetadata();
const availableSourceMetadata = allSources.filter((source) =>
availableSources.includes(source.internalName)
);
return (
<>
{isAllPersonaOptionVisible && (
<AllPersonaOptionDisplay
handleClose={() => setIsAllPersonaOptionVisible(false)}
availablePersonas={availablePersonas}
handlePersonaSelect={handlePersonaSelect}
/>
)}
<div className="flex justify-center items-center h-full">
{selectedPersona ? (
<div className="w-message-xs 2xl:w-message-sm 3xl:w-message">
<div className="flex">
<div className="mx-auto">
<div className="m-auto h-[80px] w-[80px]">
<Image
src="/logo.png"
alt="Logo"
width="1419"
height="1520"
/>
</div>
<div className="m-auto text-3xl font-bold text-strong mt-4 w-fit">
{selectedPersona?.name || "How can I help you today?"}
</div>
{selectedPersona && (
<div className="mt-1">{selectedPersona.description}</div>
)}
</div>
</div>
<Divider />
<div>
{selectedPersona && selectedPersona.document_sets.length > 0 && (
<div className="mt-2">
<p className="font-bold mb-1 mt-4 text-emphasis">
Knowledge Sets:{" "}
</p>
{selectedPersona.document_sets.map((documentSet) => (
<div key={documentSet.id} className="w-fit">
<HoverPopup
mainContent={
<span className="flex w-fit p-1 rounded border border-border text-xs font-medium cursor-default">
<div className="mr-1 my-auto">
<FiBookmark />
</div>
{documentSet.name}
</span>
}
popupContent={
<div className="flex py-1 w-96">
<FiInfo className="my-auto mr-2" />
<div className="text-sm">
{documentSet.description}
</div>
</div>
}
direction="top"
/>
</div>
))}
</div>
)}
{availableSources.length > 0 && (
<div className="mt-2">
<p className="font-bold mb-1 mt-4 text-emphasis">
Connected Sources:{" "}
</p>
<div className="flex flex-wrap gap-x-2">
{availableSourceMetadata.map((sourceMetadata) => (
<span
key={sourceMetadata.internalName}
className="flex w-fit p-1 rounded border border-border text-xs font-medium cursor-default"
>
<div className="mr-1 my-auto">
{sourceMetadata.icon({})}
</div>
{sourceMetadata.displayName}
</span>
))}
</div>
</div>
)}
</div>
</div>
) : (
<div className="px-12 w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
<div className="mx-auto">
<div className="m-auto h-[80px] w-[80px]">
<Image src="/logo.png" alt="Logo" width="1419" height="1520" />
</div>
</div>
<div className="mt-2">
<p className="font-bold text-xl mb-1 mt-4 text-emphasis text-center">
Which assistant do you want to chat with today?{" "}
</p>
<p className="text-sm text-center">
Or ask a question immediately to use the{" "}
<b>{availablePersonas[0].name}</b> assistant.
</p>
<div className="flex flex-col gap-y-4 mt-8">
{availablePersonas
.slice(0, MAX_PERSONAS_TO_DISPLAY)
.map((persona) => (
<div
key={persona.id}
onClick={() => handlePersonaSelect(persona)}
>
<HelperItemDisplay
title={persona.name}
description={persona.description}
/>
</div>
))}
</div>
{availablePersonas.length > MAX_PERSONAS_TO_DISPLAY && (
<div className="mt-4 flex">
<div
onClick={() => setIsAllPersonaOptionVisible(true)}
className="text-sm flex mx-auto p-1 hover:bg-hover-light rounded cursor-default"
>
<FiZoomIn className="my-auto mr-1" /> See more
</div>
</div>
)}
</div>
</div>
)}
</div>
</>
);
}

View File

@ -13,6 +13,7 @@ export function ChatLayout({
availableSources,
availableDocumentSets,
availablePersonas,
defaultSelectedPersonaId,
documentSidebarInitialWidth,
}: {
user: User | null;
@ -20,12 +21,17 @@ export function ChatLayout({
availableSources: ValidSources[];
availableDocumentSets: DocumentSet[];
availablePersonas: Persona[];
defaultSelectedPersonaId?: number; // what persona to default to
documentSidebarInitialWidth?: number;
}) {
const searchParams = useSearchParams();
const chatIdRaw = searchParams.get("chatId");
const chatId = chatIdRaw ? parseInt(chatIdRaw) : null;
const selectedChatSession = chatSessions.find(
(chatSession) => chatSession.id === chatId
);
return (
<>
<div className="flex relative bg-background text-default h-screen overflow-x-hidden">
@ -37,10 +43,11 @@ export function ChatLayout({
<Chat
existingChatSessionId={chatId}
existingChatSessionPersonaId={0}
existingChatSessionPersonaId={selectedChatSession?.persona_id}
availableSources={availableSources}
availableDocumentSets={availableDocumentSets}
availablePersonas={availablePersonas}
defaultSelectedPersonaId={defaultSelectedPersonaId}
documentSidebarInitialWidth={documentSidebarInitialWidth}
/>
</div>

View File

@ -1,6 +1,5 @@
import { Persona } from "@/app/admin/personas/interfaces";
import { FiCheck, FiChevronDown } from "react-icons/fi";
import { FaRobot } from "react-icons/fa";
import { CustomDropdown } from "@/components/Dropdown";
function PersonaItem({

View File

@ -33,6 +33,9 @@ export interface Message {
}
export interface BackendChatSession {
chat_session_id: number;
description: string;
persona_id: number;
messages: BackendMessage[];
}

View File

@ -22,7 +22,11 @@ import { DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME } from "@/components/resizable/conta
import { personaComparator } from "../admin/personas/lib";
import { ChatLayout } from "./ChatPage";
export default async function Page() {
export default async function Page({
searchParams,
}: {
searchParams: { [key: string]: string };
}) {
noStore();
const tasks = [
@ -110,6 +114,11 @@ export default async function Page() {
// sort them in priority order
personas.sort(personaComparator);
const defaultPersonaIdRaw = searchParams["personaId"];
const defaultPersonaId = defaultPersonaIdRaw
? parseInt(defaultPersonaIdRaw)
: undefined;
const documentSidebarCookieInitialWidth = cookies().get(
DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME
);
@ -130,6 +139,7 @@ export default async function Page() {
availableSources={availableSources}
availableDocumentSets={documentSets}
availablePersonas={personas}
defaultSelectedPersonaId={defaultPersonaId}
documentSidebarInitialWidth={finalDocumentSidebarInitialWidth}
/>
</>

View File

@ -22,8 +22,8 @@ export function Modal({
>
<div
className={`
bg-background rounded-sm shadow-lg
shadow-lg relative w-1/2 text-sm
bg-background rounded shadow-lg
relative w-1/2 text-sm
${className}
`}
onClick={(event) => event.stopPropagation()}