From 5825d01d532e0a18318064a14e80073a27e54f95 Mon Sep 17 00:00:00 2001 From: pablodanswer Date: Mon, 5 Aug 2024 11:22:57 -0700 Subject: [PATCH] Better assistant interactions + UI (#2029) * add assistnat re-ordering, selections, etc. * squash * remove unnecessary comment * squash * adapt dragging for all IDs + smoother animation + consistency * fix minor typing issue * fix minor typing issue * remove logs --- backend/danswer/db/connector.py | 1 - .../[connector]/AddConnectorPage.tsx | 14 +- .../connectors/[connector]/pages/Create.tsx | 13 +- .../assistants/gallery/AssistantsGallery.tsx | 1 + .../app/assistants/mine/AssistantsList.tsx | 304 +++++++----------- web/src/app/assistants/mine/page.tsx | 7 - web/src/app/chat/ChatPage.tsx | 3 +- .../modal/configuration/AssistantsTab.tsx | 164 +++++----- .../chat/sessionSidebar/HistorySidebar.tsx | 27 +- .../components/assistants/AssistantCards.tsx | 112 +++++++ web/src/components/chat_search/Header.tsx | 29 +- web/src/components/search/SearchSection.tsx | 2 + web/src/components/table/DraggableTable.tsx | 2 - .../assistants/updateAssistantPreferences.ts | 2 +- web/src/lib/chat/fetchChatData.ts | 1 - 15 files changed, 374 insertions(+), 308 deletions(-) create mode 100644 web/src/components/assistants/AssistantCards.tsx diff --git a/backend/danswer/db/connector.py b/backend/danswer/db/connector.py index 59a57ff95..e63d6c048 100644 --- a/backend/danswer/db/connector.py +++ b/backend/danswer/db/connector.py @@ -248,7 +248,6 @@ def create_initial_default_connector(db_session: Session) -> None: default_connector_id = 0 default_connector = fetch_connector_by_id(default_connector_id, db_session) if default_connector is not None: - # Check if the existing connector has the correct values if ( default_connector.source != DocumentSource.INGESTION_API or default_connector.input_type != InputType.LOAD_STATE diff --git a/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx b/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx index eab945927..404d243e8 100644 --- a/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx +++ b/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx @@ -211,7 +211,7 @@ export default function AddConnector({ } // Without credential - if (isSuccess && response && credentialActivated) { + if (credentialActivated && isSuccess && response) { const credential = currentCredential || liveGDriveCredential || liveGmailCredential; const linkCredentialResponse = await linkCredential( @@ -228,7 +228,19 @@ export default function AddConnector({ setTimeout(() => { window.open("/admin/indexing/status", "_self"); }, 1000); + } else { + const errorData = await linkCredentialResponse.json(); + setPopup({ + message: errorData.message, + type: "error", + }); } + } else if (isSuccess) { + setPopup({ + message: + "Credential created succsfully! Redirecting to connector home page", + type: "success", + }); } else { setPopup({ message: message, type: "error" }); } diff --git a/web/src/app/admin/connectors/[connector]/pages/Create.tsx b/web/src/app/admin/connectors/[connector]/pages/Create.tsx index d601d416d..11170b4cb 100644 --- a/web/src/app/admin/connectors/[connector]/pages/Create.tsx +++ b/web/src/app/admin/connectors/[connector]/pages/Create.tsx @@ -250,10 +250,21 @@ const DynamicConnectionForm: React.FC = ({ {field.description} )} + + ) => + updateValue(setFieldValue)( + field.name, + e.target.value + ) + } as="select" + value={values[field.name]} name={field.name} - className="w-full p-2 border bg-input border-border-medium rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" + className="w-full p-2 border bg-input border-border-medium rounded-md bg-black + focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" > {field.options?.map((option) => ( diff --git a/web/src/app/assistants/gallery/AssistantsGallery.tsx b/web/src/app/assistants/gallery/AssistantsGallery.tsx index b4f46c1ec..386811c10 100644 --- a/web/src/app/assistants/gallery/AssistantsGallery.tsx +++ b/web/src/app/assistants/gallery/AssistantsGallery.tsx @@ -102,6 +102,7 @@ export function AssistantsGallery({ my-auto ml-2 text-strong + line-clamp-2 " > {assistant.name} diff --git a/web/src/app/assistants/mine/AssistantsList.tsx b/web/src/app/assistants/mine/AssistantsList.tsx index 29d67a7c7..e79a0bd2b 100644 --- a/web/src/app/assistants/mine/AssistantsList.tsx +++ b/web/src/app/assistants/mine/AssistantsList.tsx @@ -4,25 +4,10 @@ import { useState } from "react"; import { MinimalUserSnapshot, User } from "@/lib/types"; import { Persona } from "@/app/admin/assistants/interfaces"; import { Divider, Text } from "@tremor/react"; -import { - FiArrowDown, - FiArrowUp, - FiEdit2, - FiMoreHorizontal, - FiPlus, - FiSearch, - FiX, - FiShare2, - FiImage, -} from "react-icons/fi"; +import { FiEdit2, FiPlus, FiSearch, FiShare2 } from "react-icons/fi"; import Link from "next/link"; import { orderAssistantsForUser } from "@/lib/assistants/orderAssistants"; -import { - addAssistantToList, - moveAssistantDown, - moveAssistantUp, - removeAssistantFromList, -} from "@/lib/assistants/updateAssistantPreferences"; +import { updateUserAssistantList } from "@/lib/assistants/updateAssistantPreferences"; import { AssistantIcon } from "@/components/assistants/AssistantIcon"; import { DefaultPopover } from "@/components/popover/DefaultPopover"; import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup"; @@ -34,8 +19,56 @@ import { AssistantSharingModal } from "./AssistantSharingModal"; import { AssistantSharedStatusDisplay } from "../AssistantSharedStatus"; import useSWR from "swr"; import { errorHandlingFetcher } from "@/lib/fetcher"; -import { AssistantTools, ToolsDisplay } from "../ToolsDisplay"; -import { CustomTooltip } from "@/components/tooltip/CustomTooltip"; +import { AssistantTools } from "../ToolsDisplay"; + +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +import { DragHandle } from "@/components/table/DragHandle"; + +function DraggableAssistantListItem(props: any) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: props.assistant.id.toString() }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.9 : 1, + zIndex: isDragging ? 1000 : "auto", + }; + + return ( +
+
+ +
+
+ +
+
+ ); +} function AssistantListItem({ assistant, @@ -50,7 +83,7 @@ function AssistantListItem({ assistant: Persona; user: User | null; allUsers: MinimalUserSnapshot[]; - allAssistantIds: number[]; + allAssistantIds: string[]; isFirst: boolean; isLast: boolean; isVisible: boolean; @@ -59,7 +92,6 @@ function AssistantListItem({ const router = useRouter(); const [showSharingModal, setShowSharingModal] = useState(false); - const currentChosenAssistants = user?.preferences?.chosen_assistants; const isOwnedByUser = checkUserOwnsAssistant(user, assistant); return ( @@ -83,7 +115,6 @@ function AssistantListItem({ >
-

+

{assistant.name}

{assistant.description}
-
+
{assistant.tools.length != 0 && ( @@ -111,7 +142,10 @@ function AssistantListItem({ {!assistant.is_public && (
setShowSharingModal(true)} + onClick={(e) => { + e.stopPropagation(); + setShowSharingModal(true); + }} >
@@ -124,158 +158,31 @@ function AssistantListItem({
)} - - - -
- } - side="bottom" - align="start" - sideOffset={5} - > - {[ - ...(!isFirst - ? [ -
{ - const success = await moveAssistantUp( - assistant.id, - currentChosenAssistants || allAssistantIds - ); - if (success) { - setPopup({ - message: `"${assistant.name}" has been moved up.`, - type: "success", - }); - router.refresh(); - } else { - setPopup({ - message: `"${assistant.name}" could not be moved up.`, - type: "error", - }); - } - }} - > - Move Up -
, - ] - : []), - ...(!isLast - ? [ -
{ - const success = await moveAssistantDown( - assistant.id, - currentChosenAssistants || allAssistantIds - ); - if (success) { - setPopup({ - message: `"${assistant.name}" has been moved down.`, - type: "success", - }); - router.refresh(); - } else { - setPopup({ - message: `"${assistant.name}" could not be moved down.`, - type: "error", - }); - } - }} - > - Move Down -
, - ] - : []), - isVisible ? ( -
{ - if ( - currentChosenAssistants && - currentChosenAssistants.length === 1 - ) { - setPopup({ - message: `Cannot remove "${assistant.name}" - you must have at least one assistant.`, - type: "error", - }); - return; - } - - const success = await removeAssistantFromList( - assistant.id, - currentChosenAssistants || allAssistantIds - ); - if (success) { - setPopup({ - message: `"${assistant.name}" has been removed from your list.`, - type: "success", - }); - router.refresh(); - } else { - setPopup({ - message: `"${assistant.name}" could not be removed from your list.`, - type: "error", - }); - } - }} - > - {isOwnedByUser ? "Hide" : "Remove"} -
- ) : ( -
{ - const success = await addAssistantToList( - assistant.id, - currentChosenAssistants || allAssistantIds - ); - if (success) { - setPopup({ - message: `"${assistant.name}" has been added to your list.`, - type: "success", - }); - router.refresh(); - } else { - setPopup({ - message: `"${assistant.name}" could not be added to your list.`, - type: "error", - }); - } - }} - > - Add -
- ), - ]} -
); } - -interface AssistantsListProps { +export function AssistantsList({ + user, + assistants, +}: { user: User | null; assistants: Persona[]; -} +}) { + const [filteredAssistants, setFilteredAssistants] = useState( + orderAssistantsForUser(assistants, user) + ); -export function AssistantsList({ user, assistants }: AssistantsListProps) { - const filteredAssistants = orderAssistantsForUser(assistants, user); const ownedButHiddenAssistants = assistants.filter( (assistant) => checkUserOwnsAssistant(user, assistant) && user?.preferences?.chosen_assistants && !user?.preferences?.chosen_assistants?.includes(assistant.id) ); - const allAssistantIds = assistants.map((assistant) => assistant.id); + const allAssistantIds = assistants.map((assistant) => + assistant.id.toString() + ); const { popup, setPopup } = usePopup(); @@ -284,6 +191,32 @@ export function AssistantsList({ user, assistants }: AssistantsListProps) { errorHandlingFetcher ); + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + async function handleDragEnd(event: DragEndEvent) { + const { active, over } = event; + + if (over && active.id !== over.id) { + setFilteredAssistants((assistants) => { + const oldIndex = assistants.findIndex( + (a) => a.id.toString() === active.id + ); + const newIndex = assistants.findIndex( + (a) => a.id.toString() === over.id + ); + const newAssistants = arrayMove(assistants, oldIndex, newIndex); + + updateUserAssistantList(newAssistants.map((a) => a.id)); + return newAssistants; + }); + } + } + return ( <> {popup} @@ -323,24 +256,35 @@ export function AssistantsList({ user, assistants }: AssistantsListProps) { The order the assistants appear below will be the order they appear in the Assistants dropdown. The first assistant listed will be your - default assistant when you start a new chat. + default assistant when you start a new chat. Drag and drop to reorder. -
- {filteredAssistants.map((assistant, index) => ( - - ))} -
+ + a.id.toString())} + strategy={verticalListSortingStrategy} + > +
+ {filteredAssistants.map((assistant, index) => ( + + ))} +
+
+
{ownedButHiddenAssistants.length > 0 && ( <> @@ -362,7 +306,7 @@ export function AssistantsList({ user, assistants }: AssistantsListProps) { allAssistantIds={allAssistantIds} allUsers={users || []} isFirst={index === 0} - isLast={index === filteredAssistants.length - 1} + isLast={index === ownedButHiddenAssistants.length - 1} isVisible={false} setPopup={setPopup} /> diff --git a/web/src/app/assistants/mine/page.tsx b/web/src/app/assistants/mine/page.tsx index e167a10a8..f2d693c94 100644 --- a/web/src/app/assistants/mine/page.tsx +++ b/web/src/app/assistants/mine/page.tsx @@ -1,16 +1,9 @@ -import { HistorySidebar } from "@/app/chat/sessionSidebar/HistorySidebar"; import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh"; -import { UserDropdown } from "@/components/UserDropdown"; import { ChatProvider } from "@/components/context/ChatContext"; import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper"; -import { ApiKeyModal } from "@/components/llm/ApiKeyModal"; import { fetchChatData } from "@/lib/chat/fetchChatData"; import { unstable_noStore as noStore } from "next/cache"; import { redirect } from "next/navigation"; -import { AssistantsList } from "./AssistantsList"; -import { Logo } from "@/components/Logo"; -import FixedLogo from "@/app/chat/shared_chat_search/FixedLogo"; -import SidebarWrapper from "../SidebarWrapper"; import WrappedAssistantsMine from "./WrappedAssistantsMine"; export default async function GalleryPage({ diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index a7d94c387..73b754f2a 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -55,7 +55,6 @@ import { ShareChatSessionModal } from "./modal/ShareChatSessionModal"; import { FiArrowDown } from "react-icons/fi"; import { ChatIntro } from "./ChatIntro"; import { AIMessage, HumanMessage } from "./message/Messages"; -import { ThreeDots } from "react-loader-spinner"; import { StarterMessage } from "./StarterMessage"; import { AnswerPiecePacket, DanswerDocument } from "@/lib/search/interfaces"; import { buildFilters } from "@/lib/search/utils"; @@ -1169,6 +1168,7 @@ export function ChatPage({ >
setMessage("")} page="chat" ref={innerSidebarElementRef} toggleSidebar={toggleSidebar} @@ -1192,6 +1192,7 @@ export function ChatPage({
{liveAssistant && ( setMessage("")} page="chat" setSharingModalVisible={ chatSessionIdRef.current !== null diff --git a/web/src/app/chat/modal/configuration/AssistantsTab.tsx b/web/src/app/chat/modal/configuration/AssistantsTab.tsx index a95296ef3..d286f53bf 100644 --- a/web/src/app/chat/modal/configuration/AssistantsTab.tsx +++ b/web/src/app/chat/modal/configuration/AssistantsTab.tsx @@ -1,108 +1,92 @@ +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import { Persona } from "@/app/admin/assistants/interfaces"; import { LLMProviderDescriptor } from "@/app/admin/models/llm/interfaces"; -import { AssistantTools } from "@/app/assistants/ToolsDisplay"; -import { Bubble } from "@/components/Bubble"; -import { AssistantIcon } from "@/components/assistants/AssistantIcon"; -import { getDisplayNameForModel } from "@/lib/hooks"; import { getFinalLLM } from "@/lib/llm/utils"; import React, { useState } from "react"; -import { FiBookmark, FiPlus } from "react-icons/fi"; - -interface AssistantsTabProps { - selectedAssistant: Persona; - availableAssistants: Persona[]; - llmProviders: LLMProviderDescriptor[]; - onSelect: (assistant: Persona) => void; -} +import { updateUserAssistantList } from "@/lib/assistants/updateAssistantPreferences"; +import { DraggableAssistantCard } from "@/components/assistants/AssistantCards"; export function AssistantsTab({ selectedAssistant, availableAssistants, llmProviders, onSelect, -}: AssistantsTabProps) { +}: { + selectedAssistant: Persona; + availableAssistants: Persona[]; + llmProviders: LLMProviderDescriptor[]; + onSelect: (assistant: Persona) => void; +}) { const [_, llmName] = getFinalLLM(llmProviders, null, null); + const [assistants, setAssistants] = useState(availableAssistants); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + function handleDragEnd(event: DragEndEvent) { + const { active, over } = event; + + if (over && active.id !== over.id) { + setAssistants((items) => { + const oldIndex = items.findIndex( + (item) => item.id.toString() === active.id + ); + const newIndex = items.findIndex( + (item) => item.id.toString() === over.id + ); + const updatedAssistants = arrayMove(items, oldIndex, newIndex); + + updateUserAssistantList(updatedAssistants.map((a) => a.id)); + + return updatedAssistants; + }); + } + } return (

Change Assistant

-
- {availableAssistants.map((assistant) => ( - - ))} -
+ + a.id.toString())} + strategy={verticalListSortingStrategy} + > +
+ {assistants.map((assistant) => ( + + ))} +
+
+
); } - -const AssistantCard = ({ - assistant, - isSelected, - onSelect, - llmName, -}: { - assistant: Persona; - isSelected: boolean; - onSelect: (assistant: Persona) => void; - llmName: string; -}) => { - const [hovering, setHovering] = useState(false); - return ( -
onSelect(assistant)} - key={assistant.id} - className={` - p-4 - cursor-pointer - border - hover:bg-hover - shadow-md - rounded - rounded-lg - border-border - `} - onMouseEnter={() => setHovering(true)} - onMouseLeave={() => setHovering(false)} - > -
- -
- {assistant.name} -
-
- -
- {assistant.description} -
-
- {assistant.document_sets.length > 0 && ( -
-

Document Sets:

- {assistant.document_sets.map((set) => ( - -
- - {set.name} -
-
- ))} -
- )} - -
- Default model:{" "} - {getDisplayNameForModel( - assistant.llm_model_version_override || llmName - )} -
- -
-
- ); -}; diff --git a/web/src/app/chat/sessionSidebar/HistorySidebar.tsx b/web/src/app/chat/sessionSidebar/HistorySidebar.tsx index 5d6913d42..977398f87 100644 --- a/web/src/app/chat/sessionSidebar/HistorySidebar.tsx +++ b/web/src/app/chat/sessionSidebar/HistorySidebar.tsx @@ -53,11 +53,13 @@ interface HistorySidebarProps { toggleSidebar?: () => void; toggled?: boolean; removeToggle?: () => void; + reset?: () => void; } export const HistorySidebar = forwardRef( ( { + reset = () => null, toggled, page, existingChats, @@ -69,7 +71,6 @@ export const HistorySidebar = forwardRef( }, ref: ForwardedRef ) => { - const commandSymbol = KeyboardSymbol(); const router = useRouter(); const { popup, setPopup } = usePopup(); @@ -85,9 +86,19 @@ export const HistorySidebar = forwardRef( if (!combinedSettings) { return null; } - const settings = combinedSettings.settings; + const enterpriseSettings = combinedSettings.enterpriseSettings; + const handleNewChat = () => { + reset(); + const newChatUrl = + `/${page}` + + (NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA && currentChatSession + ? `?assistantId=${currentChatSession.persona_id}` + : ""); + router.push(newChatUrl); + }; + return ( <> {popup} @@ -144,19 +155,13 @@ export const HistorySidebar = forwardRef( {page == "chat" && (
-

New Chat

- + )}
diff --git a/web/src/components/search/SearchSection.tsx b/web/src/components/search/SearchSection.tsx index c2cba40b3..fab9218a6 100644 --- a/web/src/components/search/SearchSection.tsx +++ b/web/src/components/search/SearchSection.tsx @@ -541,6 +541,7 @@ export const SearchSection = ({ >
setQuery("")} page="search" ref={innerSidebarElementRef} toggleSidebar={toggleSidebar} @@ -552,6 +553,7 @@ export const SearchSection = ({
setQuery("")} toggleSidebar={toggleSidebar} page="search" user={user} diff --git a/web/src/components/table/DraggableTable.tsx b/web/src/components/table/DraggableTable.tsx index b5cf91399..b7c125436 100644 --- a/web/src/components/table/DraggableTable.tsx +++ b/web/src/components/table/DraggableTable.tsx @@ -4,9 +4,7 @@ import { TableRow, TableHeaderCell, TableBody, - TableCell, } from "@tremor/react"; -import { DraggableTableBody } from "./DraggableTableBody"; import React, { useMemo, useState } from "react"; import { closestCenter, diff --git a/web/src/lib/assistants/updateAssistantPreferences.ts b/web/src/lib/assistants/updateAssistantPreferences.ts index f902e561c..61a0128b2 100644 --- a/web/src/lib/assistants/updateAssistantPreferences.ts +++ b/web/src/lib/assistants/updateAssistantPreferences.ts @@ -1,4 +1,4 @@ -async function updateUserAssistantList( +export async function updateUserAssistantList( chosenAssistants: number[] ): Promise { const response = await fetch("/api/user/assistant-list", { diff --git a/web/src/lib/chat/fetchChatData.ts b/web/src/lib/chat/fetchChatData.ts index fba4848e3..4bd023945 100644 --- a/web/src/lib/chat/fetchChatData.ts +++ b/web/src/lib/chat/fetchChatData.ts @@ -213,7 +213,6 @@ export async function fetchChatData(searchParams: { ) ); } - // // TODO check for image capabilities and enable if so let folders: Folder[] = []; if (foldersResponse?.ok) {