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
This commit is contained in:
pablodanswer
2024-08-05 11:22:57 -07:00
committed by GitHub
parent cd22cca4e8
commit 5825d01d53
15 changed files with 374 additions and 308 deletions

View File

@@ -248,7 +248,6 @@ def create_initial_default_connector(db_session: Session) -> None:
default_connector_id = 0 default_connector_id = 0
default_connector = fetch_connector_by_id(default_connector_id, db_session) default_connector = fetch_connector_by_id(default_connector_id, db_session)
if default_connector is not None: if default_connector is not None:
# Check if the existing connector has the correct values
if ( if (
default_connector.source != DocumentSource.INGESTION_API default_connector.source != DocumentSource.INGESTION_API
or default_connector.input_type != InputType.LOAD_STATE or default_connector.input_type != InputType.LOAD_STATE

View File

@@ -211,7 +211,7 @@ export default function AddConnector({
} }
// Without credential // Without credential
if (isSuccess && response && credentialActivated) { if (credentialActivated && isSuccess && response) {
const credential = const credential =
currentCredential || liveGDriveCredential || liveGmailCredential; currentCredential || liveGDriveCredential || liveGmailCredential;
const linkCredentialResponse = await linkCredential( const linkCredentialResponse = await linkCredential(
@@ -228,7 +228,19 @@ export default function AddConnector({
setTimeout(() => { setTimeout(() => {
window.open("/admin/indexing/status", "_self"); window.open("/admin/indexing/status", "_self");
}, 1000); }, 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 { } else {
setPopup({ message: message, type: "error" }); setPopup({ message: message, type: "error" });
} }

View File

@@ -250,10 +250,21 @@ const DynamicConnectionForm: React.FC<DynamicConnectionFormProps> = ({
{field.description} {field.description}
</CredentialSubText> </CredentialSubText>
)} )}
<Field <Field
onChange={(
e: React.ChangeEvent<HTMLSelectElement>
) =>
updateValue(setFieldValue)(
field.name,
e.target.value
)
}
as="select" as="select"
value={values[field.name]}
name={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"
> >
<option value="">Select an option</option> <option value="">Select an option</option>
{field.options?.map((option) => ( {field.options?.map((option) => (

View File

@@ -102,6 +102,7 @@ export function AssistantsGallery({
my-auto my-auto
ml-2 ml-2
text-strong text-strong
line-clamp-2
" "
> >
{assistant.name} {assistant.name}

View File

@@ -4,25 +4,10 @@ import { useState } from "react";
import { MinimalUserSnapshot, User } from "@/lib/types"; import { MinimalUserSnapshot, User } from "@/lib/types";
import { Persona } from "@/app/admin/assistants/interfaces"; import { Persona } from "@/app/admin/assistants/interfaces";
import { Divider, Text } from "@tremor/react"; import { Divider, Text } from "@tremor/react";
import { import { FiEdit2, FiPlus, FiSearch, FiShare2 } from "react-icons/fi";
FiArrowDown,
FiArrowUp,
FiEdit2,
FiMoreHorizontal,
FiPlus,
FiSearch,
FiX,
FiShare2,
FiImage,
} from "react-icons/fi";
import Link from "next/link"; import Link from "next/link";
import { orderAssistantsForUser } from "@/lib/assistants/orderAssistants"; import { orderAssistantsForUser } from "@/lib/assistants/orderAssistants";
import { import { updateUserAssistantList } from "@/lib/assistants/updateAssistantPreferences";
addAssistantToList,
moveAssistantDown,
moveAssistantUp,
removeAssistantFromList,
} from "@/lib/assistants/updateAssistantPreferences";
import { AssistantIcon } from "@/components/assistants/AssistantIcon"; import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { DefaultPopover } from "@/components/popover/DefaultPopover"; import { DefaultPopover } from "@/components/popover/DefaultPopover";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup"; import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
@@ -34,8 +19,56 @@ import { AssistantSharingModal } from "./AssistantSharingModal";
import { AssistantSharedStatusDisplay } from "../AssistantSharedStatus"; import { AssistantSharedStatusDisplay } from "../AssistantSharedStatus";
import useSWR from "swr"; import useSWR from "swr";
import { errorHandlingFetcher } from "@/lib/fetcher"; import { errorHandlingFetcher } from "@/lib/fetcher";
import { AssistantTools, ToolsDisplay } from "../ToolsDisplay"; import { AssistantTools } from "../ToolsDisplay";
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
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 (
<div ref={setNodeRef} style={style} className="flex items-center">
<div {...attributes} {...listeners} className="mr-2 cursor-grab">
<DragHandle />
</div>
<div className="flex-grow">
<AssistantListItem {...props} />
</div>
</div>
);
}
function AssistantListItem({ function AssistantListItem({
assistant, assistant,
@@ -50,7 +83,7 @@ function AssistantListItem({
assistant: Persona; assistant: Persona;
user: User | null; user: User | null;
allUsers: MinimalUserSnapshot[]; allUsers: MinimalUserSnapshot[];
allAssistantIds: number[]; allAssistantIds: string[];
isFirst: boolean; isFirst: boolean;
isLast: boolean; isLast: boolean;
isVisible: boolean; isVisible: boolean;
@@ -59,7 +92,6 @@ function AssistantListItem({
const router = useRouter(); const router = useRouter();
const [showSharingModal, setShowSharingModal] = useState(false); const [showSharingModal, setShowSharingModal] = useState(false);
const currentChosenAssistants = user?.preferences?.chosen_assistants;
const isOwnedByUser = checkUserOwnsAssistant(user, assistant); const isOwnedByUser = checkUserOwnsAssistant(user, assistant);
return ( return (
@@ -83,7 +115,6 @@ function AssistantListItem({
> >
<div <div
className=" className="
flex flex
justify-between justify-between
items-center items-center
@@ -92,13 +123,13 @@ function AssistantListItem({
<div className="w-3/4"> <div className="w-3/4">
<div className="flex items-center"> <div className="flex items-center">
<AssistantIcon assistant={assistant} /> <AssistantIcon assistant={assistant} />
<h2 className="text-xl font-semibold my-auto ml-2"> <h2 className="text-xl line-clamp-2 font-semibold my-auto ml-2">
{assistant.name} {assistant.name}
</h2> </h2>
</div> </div>
<div className="text-sm mt-2">{assistant.description}</div> <div className="text-sm mt-2">{assistant.description}</div>
<div className="mt-2 flex items-center gap-x-3"> <div className="mt-2 flex items-start gap-y-2 flex-col gap-x-3">
<AssistantSharedStatusDisplay assistant={assistant} user={user} /> <AssistantSharedStatusDisplay assistant={assistant} user={user} />
{assistant.tools.length != 0 && ( {assistant.tools.length != 0 && (
<AssistantTools list assistant={assistant} /> <AssistantTools list assistant={assistant} />
@@ -111,7 +142,10 @@ function AssistantListItem({
{!assistant.is_public && ( {!assistant.is_public && (
<div <div
className="mr-4 rounded p-2 cursor-pointer hover:bg-hover" className="mr-4 rounded p-2 cursor-pointer hover:bg-hover"
onClick={() => setShowSharingModal(true)} onClick={(e) => {
e.stopPropagation();
setShowSharingModal(true);
}}
> >
<FiShare2 size={16} /> <FiShare2 size={16} />
</div> </div>
@@ -124,158 +158,31 @@ function AssistantListItem({
</Link> </Link>
</div> </div>
)} )}
<DefaultPopover
content={
<div className="hover:bg-hover rounded p-2 cursor-pointer">
<FiMoreHorizontal size={16} />
</div>
}
side="bottom"
align="start"
sideOffset={5}
>
{[
...(!isFirst
? [
<div
key="move-up"
className="flex items-center gap-x-2"
onClick={async () => {
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",
});
}
}}
>
<FiArrowUp /> Move Up
</div>,
]
: []),
...(!isLast
? [
<div
key="move-down"
className="flex items-center gap-x-2"
onClick={async () => {
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",
});
}
}}
>
<FiArrowDown /> Move Down
</div>,
]
: []),
isVisible ? (
<div
key="remove"
className="flex items-center gap-x-2"
onClick={async () => {
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",
});
}
}}
>
<FiX /> {isOwnedByUser ? "Hide" : "Remove"}
</div>
) : (
<div
key="add"
className="flex items-center gap-x-2"
onClick={async () => {
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",
});
}
}}
>
<FiPlus /> Add
</div>
),
]}
</DefaultPopover>
</div> </div>
</div> </div>
</> </>
); );
} }
export function AssistantsList({
interface AssistantsListProps { user,
assistants,
}: {
user: User | null; user: User | null;
assistants: Persona[]; assistants: Persona[];
} }) {
const [filteredAssistants, setFilteredAssistants] = useState(
orderAssistantsForUser(assistants, user)
);
export function AssistantsList({ user, assistants }: AssistantsListProps) {
const filteredAssistants = orderAssistantsForUser(assistants, user);
const ownedButHiddenAssistants = assistants.filter( const ownedButHiddenAssistants = assistants.filter(
(assistant) => (assistant) =>
checkUserOwnsAssistant(user, assistant) && checkUserOwnsAssistant(user, assistant) &&
user?.preferences?.chosen_assistants && user?.preferences?.chosen_assistants &&
!user?.preferences?.chosen_assistants?.includes(assistant.id) !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(); const { popup, setPopup } = usePopup();
@@ -284,6 +191,32 @@ export function AssistantsList({ user, assistants }: AssistantsListProps) {
errorHandlingFetcher 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 ( return (
<> <>
{popup} {popup}
@@ -323,24 +256,35 @@ export function AssistantsList({ user, assistants }: AssistantsListProps) {
<Text> <Text>
The order the assistants appear below will be the order they appear in The order the assistants appear below will be the order they appear in
the Assistants dropdown. The first assistant listed will be your 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.
</Text> </Text>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={filteredAssistants.map((a) => a.id.toString())}
strategy={verticalListSortingStrategy}
>
<div className="w-full p-4 mt-3"> <div className="w-full p-4 mt-3">
{filteredAssistants.map((assistant, index) => ( {filteredAssistants.map((assistant, index) => (
<AssistantListItem <DraggableAssistantListItem
key={assistant.id} key={assistant.id}
assistant={assistant} assistant={assistant}
user={user} user={user}
allAssistantIds={allAssistantIds} allAssistantIds={allAssistantIds}
allUsers={users || []} allUsers={users || []}
isFirst={index === 0} isFirst={false}
isLast={index === filteredAssistants.length - 1} isLast={index === filteredAssistants.length - 1}
isVisible isVisible
setPopup={setPopup} setPopup={setPopup}
/> />
))} ))}
</div> </div>
</SortableContext>
</DndContext>
{ownedButHiddenAssistants.length > 0 && ( {ownedButHiddenAssistants.length > 0 && (
<> <>
@@ -362,7 +306,7 @@ export function AssistantsList({ user, assistants }: AssistantsListProps) {
allAssistantIds={allAssistantIds} allAssistantIds={allAssistantIds}
allUsers={users || []} allUsers={users || []}
isFirst={index === 0} isFirst={index === 0}
isLast={index === filteredAssistants.length - 1} isLast={index === ownedButHiddenAssistants.length - 1}
isVisible={false} isVisible={false}
setPopup={setPopup} setPopup={setPopup}
/> />

View File

@@ -1,16 +1,9 @@
import { HistorySidebar } from "@/app/chat/sessionSidebar/HistorySidebar";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh"; import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { UserDropdown } from "@/components/UserDropdown";
import { ChatProvider } from "@/components/context/ChatContext"; import { ChatProvider } from "@/components/context/ChatContext";
import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper"; import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
import { ApiKeyModal } from "@/components/llm/ApiKeyModal";
import { fetchChatData } from "@/lib/chat/fetchChatData"; import { fetchChatData } from "@/lib/chat/fetchChatData";
import { unstable_noStore as noStore } from "next/cache"; import { unstable_noStore as noStore } from "next/cache";
import { redirect } from "next/navigation"; 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"; import WrappedAssistantsMine from "./WrappedAssistantsMine";
export default async function GalleryPage({ export default async function GalleryPage({

View File

@@ -55,7 +55,6 @@ import { ShareChatSessionModal } from "./modal/ShareChatSessionModal";
import { FiArrowDown } from "react-icons/fi"; import { FiArrowDown } from "react-icons/fi";
import { ChatIntro } from "./ChatIntro"; import { ChatIntro } from "./ChatIntro";
import { AIMessage, HumanMessage } from "./message/Messages"; import { AIMessage, HumanMessage } from "./message/Messages";
import { ThreeDots } from "react-loader-spinner";
import { StarterMessage } from "./StarterMessage"; import { StarterMessage } from "./StarterMessage";
import { AnswerPiecePacket, DanswerDocument } from "@/lib/search/interfaces"; import { AnswerPiecePacket, DanswerDocument } from "@/lib/search/interfaces";
import { buildFilters } from "@/lib/search/utils"; import { buildFilters } from "@/lib/search/utils";
@@ -1169,6 +1168,7 @@ export function ChatPage({
> >
<div className="w-full relative"> <div className="w-full relative">
<HistorySidebar <HistorySidebar
reset={() => setMessage("")}
page="chat" page="chat"
ref={innerSidebarElementRef} ref={innerSidebarElementRef}
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
@@ -1192,6 +1192,7 @@ export function ChatPage({
<div className="flex h-full flex-col w-full"> <div className="flex h-full flex-col w-full">
{liveAssistant && ( {liveAssistant && (
<FunctionalHeader <FunctionalHeader
reset={() => setMessage("")}
page="chat" page="chat"
setSharingModalVisible={ setSharingModalVisible={
chatSessionIdRef.current !== null chatSessionIdRef.current !== null

View File

@@ -1,35 +1,83 @@
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 { Persona } from "@/app/admin/assistants/interfaces";
import { LLMProviderDescriptor } from "@/app/admin/models/llm/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 { getFinalLLM } from "@/lib/llm/utils";
import React, { useState } from "react"; import React, { useState } from "react";
import { FiBookmark, FiPlus } from "react-icons/fi"; import { updateUserAssistantList } from "@/lib/assistants/updateAssistantPreferences";
import { DraggableAssistantCard } from "@/components/assistants/AssistantCards";
interface AssistantsTabProps {
selectedAssistant: Persona;
availableAssistants: Persona[];
llmProviders: LLMProviderDescriptor[];
onSelect: (assistant: Persona) => void;
}
export function AssistantsTab({ export function AssistantsTab({
selectedAssistant, selectedAssistant,
availableAssistants, availableAssistants,
llmProviders, llmProviders,
onSelect, onSelect,
}: AssistantsTabProps) { }: {
selectedAssistant: Persona;
availableAssistants: Persona[];
llmProviders: LLMProviderDescriptor[];
onSelect: (assistant: Persona) => void;
}) {
const [_, llmName] = getFinalLLM(llmProviders, null, null); 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 ( return (
<div className="py-4"> <div className="py-4">
<h3 className="px-4 text-lg font-semibold">Change Assistant</h3> <h3 className="px-4 text-lg font-semibold">Change Assistant</h3>
<div className="px-2 pb-2 mx-2 max-h-[500px] overflow-y-scroll my-3 grid grid-cols-1 gap-4"> <DndContext
{availableAssistants.map((assistant) => ( sensors={sensors}
<AssistantCard collisionDetection={closestCenter}
key={assistant.id} onDragEnd={handleDragEnd}
>
<SortableContext
items={assistants.map((a) => a.id.toString())}
strategy={verticalListSortingStrategy}
>
<div className="px-2 pb-2 mx-2 max-h-[500px] miniscroll overflow-y-scroll my-3 grid grid-cols-1 gap-4">
{assistants.map((assistant) => (
<DraggableAssistantCard
key={assistant.id.toString()}
assistant={assistant} assistant={assistant}
isSelected={selectedAssistant.id === assistant.id} isSelected={selectedAssistant.id === assistant.id}
onSelect={onSelect} onSelect={onSelect}
@@ -37,72 +85,8 @@ export function AssistantsTab({
/> />
))} ))}
</div> </div>
</SortableContext>
</DndContext>
</div> </div>
); );
} }
const AssistantCard = ({
assistant,
isSelected,
onSelect,
llmName,
}: {
assistant: Persona;
isSelected: boolean;
onSelect: (assistant: Persona) => void;
llmName: string;
}) => {
const [hovering, setHovering] = useState(false);
return (
<div
onClick={() => 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)}
>
<div className="flex items-center mb-2">
<AssistantIcon assistant={assistant} />
<div className="ml-2 line-clamp-1 ellipsis font-bold text-sm text-emphasis">
{assistant.name}
</div>
</div>
<div className="text-xs text-subtle mb-2 text-wrap mt-2 line-clamp-3 py-1">
{assistant.description}
</div>
<div className="mt-2 flex flex-col gap-y-1">
{assistant.document_sets.length > 0 && (
<div className="text-xs text-subtle flex flex-wrap gap-2">
<p className="my-auto font-medium">Document Sets:</p>
{assistant.document_sets.map((set) => (
<Bubble key={set.id} isSelected={false}>
<div className="flex flex-row gap-1">
<FiBookmark className="mr-1 my-auto" />
{set.name}
</div>
</Bubble>
))}
</div>
)}
<div className="text-xs text-subtle">
<span className="font-semibold">Default model:</span>{" "}
{getDisplayNameForModel(
assistant.llm_model_version_override || llmName
)}
</div>
<AssistantTools hovered={hovering} assistant={assistant} />
</div>
</div>
);
};

View File

@@ -53,11 +53,13 @@ interface HistorySidebarProps {
toggleSidebar?: () => void; toggleSidebar?: () => void;
toggled?: boolean; toggled?: boolean;
removeToggle?: () => void; removeToggle?: () => void;
reset?: () => void;
} }
export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>( export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
( (
{ {
reset = () => null,
toggled, toggled,
page, page,
existingChats, existingChats,
@@ -69,7 +71,6 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
}, },
ref: ForwardedRef<HTMLDivElement> ref: ForwardedRef<HTMLDivElement>
) => { ) => {
const commandSymbol = KeyboardSymbol();
const router = useRouter(); const router = useRouter();
const { popup, setPopup } = usePopup(); const { popup, setPopup } = usePopup();
@@ -85,9 +86,19 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
if (!combinedSettings) { if (!combinedSettings) {
return null; return null;
} }
const settings = combinedSettings.settings;
const enterpriseSettings = combinedSettings.enterpriseSettings; 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 ( return (
<> <>
{popup} {popup}
@@ -144,19 +155,13 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
{page == "chat" && ( {page == "chat" && (
<div className="mx-3 mt-4 gap-y-1 flex-col flex gap-x-1.5 items-center items-center"> <div className="mx-3 mt-4 gap-y-1 flex-col flex gap-x-1.5 items-center items-center">
<Link <button
href={ onClick={handleNewChat}
"/chat" +
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA &&
currentChatSession
? `?assistantId=${currentChatSession.persona_id}`
: "")
}
className="w-full p-2 bg-white border-border border rounded items-center hover:bg-background-200 cursor-pointer transition-all duration-150 flex gap-x-2" className="w-full p-2 bg-white border-border border rounded items-center hover:bg-background-200 cursor-pointer transition-all duration-150 flex gap-x-2"
> >
<FiEdit className="flex-none " /> <FiEdit className="flex-none " />
<p className="my-auto flex items-center text-sm">New Chat</p> <p className="my-auto flex items-center text-sm">New Chat</p>
</Link> </button>
<button <button
onClick={() => onClick={() =>

View File

@@ -0,0 +1,112 @@
import { CSS } from "@dnd-kit/utilities";
import { Persona } from "@/app/admin/assistants/interfaces";
import { AssistantTools } from "@/app/assistants/ToolsDisplay";
import { Bubble } from "@/components/Bubble";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { getDisplayNameForModel } from "@/lib/hooks";
import { useSortable } from "@dnd-kit/sortable";
import React, { useState } from "react";
import { FiBookmark } from "react-icons/fi";
import { MdDragIndicator } from "react-icons/md";
export const AssistantCard = ({
assistant,
isSelected,
onSelect,
llmName,
}: {
assistant: Persona;
isSelected: boolean;
onSelect: (assistant: Persona) => void;
llmName: string;
}) => {
const [hovering, setHovering] = useState(false);
return (
<div
onClick={() => onSelect(assistant)}
className={`
p-4
cursor-pointer
border
${isSelected ? "bg-hover" : "hover:bg-hover-light"}
shadow-md
rounded-lg
border-border
max-w-full
flex items-center
overflow-x-hidden
`}
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
>
<div className="flex-grow">
<div className="flex items-center mb-2">
<AssistantIcon assistant={assistant} />
<div className="ml-2 ellipsis font-bold text-sm text-emphasis">
{assistant.name}
</div>
</div>
<div className="text-xs text-wrap text-subtle mb-2 mt-2 line-clamp-3 py-1">
{assistant.description}
</div>
<div className="mt-2 flex flex-col gap-y-1">
{assistant.document_sets.length > 0 && (
<div className="text-xs text-subtle flex flex-wrap gap-2">
<p className="my-auto font-medium">Document Sets:</p>
{assistant.document_sets.map((set) => (
<Bubble key={set.id} isSelected={false}>
<div className="flex flex-row gap-1">
<FiBookmark className="mr-1 my-auto" />
{set.name}
</div>
</Bubble>
))}
</div>
)}
<div className="text-xs text-subtle">
<span className="font-semibold">Default model:</span>{" "}
{getDisplayNameForModel(
assistant.llm_model_version_override || llmName
)}
</div>
<AssistantTools hovered={hovering} assistant={assistant} />
</div>
</div>
</div>
);
};
export function DraggableAssistantCard(props: {
assistant: Persona;
isSelected: boolean;
onSelect: (assistant: Persona) => void;
llmName: string;
}) {
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,
};
return (
<div ref={setNodeRef} style={style} className="flex items-center">
<div {...attributes} {...listeners} className="mr-1 cursor-grab">
<MdDragIndicator className="h-3 w-3 flex-none" />
</div>
<div className="flex-grow">
<AssistantCard {...props} />
</div>
</div>
);
}

View File

@@ -16,6 +16,7 @@ import KeyboardSymbol from "@/lib/browserUtilities";
import Link from "next/link"; import Link from "next/link";
import { SettingsContext } from "../settings/SettingsProvider"; import { SettingsContext } from "../settings/SettingsProvider";
import { pageType } from "@/app/chat/sessionSidebar/types"; import { pageType } from "@/app/chat/sessionSidebar/types";
import { useRouter } from "next/navigation";
export default function FunctionalHeader({ export default function FunctionalHeader({
user, user,
@@ -23,7 +24,9 @@ export default function FunctionalHeader({
currentChatSession, currentChatSession,
setSharingModalVisible, setSharingModalVisible,
toggleSidebar, toggleSidebar,
reset = () => null,
}: { }: {
reset?: () => void;
page: pageType; page: pageType;
user: User | null; user: User | null;
currentChatSession?: ChatSession | null | undefined; currentChatSession?: ChatSession | null | undefined;
@@ -58,6 +61,17 @@ export default function FunctionalHeader({
window.removeEventListener("keydown", handleKeyDown); window.removeEventListener("keydown", handleKeyDown);
}; };
}, []); }, []);
const router = useRouter();
const handleNewChat = () => {
reset();
const newChatUrl =
`/${page}` +
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA && currentChatSession
? `?assistantId=${currentChatSession.persona_id}`
: "");
router.push(newChatUrl);
};
return ( return (
<div className="pb-6 left-0 sticky top-0 z-20 w-full relative flex"> <div className="pb-6 left-0 sticky top-0 z-20 w-full relative flex">
<div className="mt-2 mx-4 text-text-700 flex w-full"> <div className="mt-2 mx-4 text-text-700 flex w-full">
@@ -86,21 +100,12 @@ export default function FunctionalHeader({
</div> </div>
{page == "chat" && ( {page == "chat" && (
<Tooltip delayDuration={1000} content={`${commandSymbol}U`}> <Tooltip delayDuration={1000} content="New Chat">
<Link <button className="mobile:hidden my-auto" onClick={handleNewChat}>
className="mobile:hidden my-auto"
href={
`/${page}` +
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA &&
currentChatSession
? `?assistantId=${currentChatSession.persona_id}`
: "")
}
>
<div className="cursor-pointer ml-2 flex-none text-text-700 hover:text-text-600 transition-colors duration-300"> <div className="cursor-pointer ml-2 flex-none text-text-700 hover:text-text-600 transition-colors duration-300">
<NewChatIcon size={20} /> <NewChatIcon size={20} />
</div> </div>
</Link> </button>
</Tooltip> </Tooltip>
)} )}
</div> </div>

View File

@@ -541,6 +541,7 @@ export const SearchSection = ({
> >
<div className="w-full relative"> <div className="w-full relative">
<HistorySidebar <HistorySidebar
reset={() => setQuery("")}
page="search" page="search"
ref={innerSidebarElementRef} ref={innerSidebarElementRef}
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
@@ -552,6 +553,7 @@ export const SearchSection = ({
<div className="absolute left-0 w-full top-0"> <div className="absolute left-0 w-full top-0">
<FunctionalHeader <FunctionalHeader
reset={() => setQuery("")}
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
page="search" page="search"
user={user} user={user}

View File

@@ -4,9 +4,7 @@ import {
TableRow, TableRow,
TableHeaderCell, TableHeaderCell,
TableBody, TableBody,
TableCell,
} from "@tremor/react"; } from "@tremor/react";
import { DraggableTableBody } from "./DraggableTableBody";
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { import {
closestCenter, closestCenter,

View File

@@ -1,4 +1,4 @@
async function updateUserAssistantList( export async function updateUserAssistantList(
chosenAssistants: number[] chosenAssistants: number[]
): Promise<boolean> { ): Promise<boolean> {
const response = await fetch("/api/user/assistant-list", { const response = await fetch("/api/user/assistant-list", {

View File

@@ -213,7 +213,6 @@ export async function fetchChatData(searchParams: {
) )
); );
} }
// // TODO check for image capabilities and enable if so
let folders: Folder[] = []; let folders: Folder[] = [];
if (foldersResponse?.ok) { if (foldersResponse?.ok) {