UX clarity + minor new features (#2136)

This commit is contained in:
pablodanswer 2024-08-14 15:23:36 -07:00 committed by GitHub
parent d9bcacfae7
commit 680388537b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 580 additions and 140 deletions

View File

@ -694,6 +694,7 @@ def stream_chat_message_objects(
except Exception as e:
error_msg = str(e)
logger.exception(f"Failed to process chat message: {error_msg}")
if "Illegal header value b'Bearer '" in error_msg:

View File

@ -8,6 +8,7 @@ EE_PUBLIC_ENDPOINT_SPECS = PUBLIC_ENDPOINT_SPECS + [
# needs to be accessible prior to user login
("/enterprise-settings", {"GET"}),
("/enterprise-settings/logo", {"GET"}),
("/enterprise-settings/logotype", {"GET"}),
("/enterprise-settings/custom-analytics-script", {"GET"}),
# oidc
("/auth/oidc/authorize", {"GET"}),

View File

@ -12,6 +12,7 @@ from danswer.file_store.file_store import get_default_file_store
from ee.danswer.server.enterprise_settings.models import AnalyticsScriptUpload
from ee.danswer.server.enterprise_settings.models import EnterpriseSettings
from ee.danswer.server.enterprise_settings.store import _LOGO_FILENAME
from ee.danswer.server.enterprise_settings.store import _LOGOTYPE_FILENAME
from ee.danswer.server.enterprise_settings.store import load_analytics_script
from ee.danswer.server.enterprise_settings.store import load_settings
from ee.danswer.server.enterprise_settings.store import store_analytics_script
@ -41,22 +42,38 @@ def fetch_settings() -> EnterpriseSettings:
@admin_router.put("/logo")
def put_logo(
file: UploadFile,
is_logotype: bool = False,
db_session: Session = Depends(get_session),
_: User | None = Depends(current_admin_user),
) -> None:
upload_logo(file=file, db_session=db_session)
upload_logo(file=file, db_session=db_session, is_logotype=is_logotype)
@basic_router.get("/logo")
def fetch_logo(db_session: Session = Depends(get_session)) -> Response:
def fetch_logo_or_logotype(is_logotype: bool, db_session: Session) -> Response:
try:
file_store = get_default_file_store(db_session)
file_io = file_store.read_file(_LOGO_FILENAME, mode="b")
filename = _LOGOTYPE_FILENAME if is_logotype else _LOGO_FILENAME
file_io = file_store.read_file(filename, mode="b")
# NOTE: specifying "image/jpeg" here, but it still works for pngs
# TODO: do this properly
return Response(content=file_io.read(), media_type="image/jpeg")
except Exception:
raise HTTPException(status_code=404, detail="No logo file found")
raise HTTPException(
status_code=404,
detail=f"No {'logotype' if is_logotype else 'logo'} file found",
)
@basic_router.get("/logotype")
def fetch_logotype(db_session: Session = Depends(get_session)) -> Response:
return fetch_logo_or_logotype(is_logotype=True, db_session=db_session)
@basic_router.get("/logo")
def fetch_logo(
is_logotype: bool = False, db_session: Session = Depends(get_session)
) -> Response:
return fetch_logo_or_logotype(is_logotype=is_logotype, db_session=db_session)
@admin_router.put("/custom-analytics-script")

View File

@ -8,8 +8,10 @@ class EnterpriseSettings(BaseModel):
application_name: str | None = None
use_custom_logo: bool = False
use_custom_logotype: bool = False
# custom Chat components
custom_lower_disclaimer_content: str | None = None
custom_header_content: str | None = None
custom_popup_header: str | None = None
custom_popup_content: str | None = None

View File

@ -63,6 +63,7 @@ def store_analytics_script(analytics_script_upload: AnalyticsScriptUpload) -> No
_LOGO_FILENAME = "__logo__"
_LOGOTYPE_FILENAME = "__logotype__"
def is_valid_file_type(filename: str) -> bool:
@ -79,8 +80,7 @@ def guess_file_type(filename: str) -> str:
def upload_logo(
db_session: Session,
file: UploadFile | str,
db_session: Session, file: UploadFile | str, is_logotype: bool = False
) -> bool:
content: IO[Any]
@ -111,7 +111,7 @@ def upload_logo(
file_store = get_default_file_store(db_session)
file_store.save_file(
file_name=_LOGO_FILENAME,
file_name=_LOGOTYPE_FILENAME if is_logotype else _LOGO_FILENAME,
content=content,
display_name=display_name,
file_origin=FileOrigin.OTHER,

View File

@ -8,8 +8,10 @@ export interface Settings {
export interface EnterpriseSettings {
application_name: string | null;
use_custom_logo: boolean;
use_custom_logotype: boolean;
// custom Chat components
custom_lower_disclaimer_content: string | null;
custom_header_content: string | null;
custom_popup_header: string | null;
custom_popup_content: string | null;

View File

@ -7,6 +7,7 @@ import remarkGfm from "remark-gfm";
import { Popover } from "@/components/popover/Popover";
import { ChevronDownIcon } from "@/components/icons/icons";
import { Divider } from "@tremor/react";
import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown";
export function ChatBanner() {
const settings = useContext(SettingsContext);
@ -79,14 +80,19 @@ export function ChatBanner() {
ref={contentRef}
className="line-clamp-2 text-center w-full overflow-hidden pr-8"
>
{renderMarkdown("")}
<MinimalMarkdown
className=""
content={settings.enterpriseSettings.custom_header_content}
/>
</div>
<div
ref={fullContentRef}
className="absolute top-0 left-0 invisible w-full"
>
{renderMarkdown("")}
<MinimalMarkdown
className=""
content={settings.enterpriseSettings.custom_header_content}
/>
</div>
<div className="absolute bottom-0 right-0 ">
{isOverflowing && (
@ -104,7 +110,12 @@ export function ChatBanner() {
popover={
<div className="bg-background-100 p-4 rounded shadow-lg mobile:max-w-xs desktop:max-w-md">
<p className="text-lg font-bold">Banner Content</p>
{renderMarkdown("max-h-96 overflow-y-auto")}
<MinimalMarkdown
className="max-h-96 overflow-y-auto"
content={
settings.enterpriseSettings.custom_header_content
}
/>
</div>
}
side="bottom"

View File

@ -1,4 +1,5 @@
"use client";
import ReactMarkdown from "react-markdown";
import { redirect, useRouter, useSearchParams } from "next/navigation";
import {
@ -82,6 +83,8 @@ import FixedLogo from "./shared_chat_search/FixedLogo";
import { getSecondsUntilExpiration } from "@/lib/time";
import { SetDefaultModelModal } from "./modal/SetDefaultModelModal";
import { DeleteChatModal } from "./modal/DeleteChatModal";
import remarkGfm from "remark-gfm";
import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown";
const TEMP_USER_MESSAGE_ID = -1;
const TEMP_ASSISTANT_MESSAGE_ID = -2;
@ -289,7 +292,7 @@ export function ChatPage({
}
return;
}
clearSelectedDocuments();
setIsFetchingChatMessages(true);
const response = await fetch(
`/api/chat/get-chat-session/${existingChatSessionId}`
@ -1110,6 +1113,7 @@ export function ChatPage({
// settings are passed in via Context and therefore aren't
// available in server-side components
const settings = useContext(SettingsContext);
const enterpriseSettings = settings?.enterpriseSettings;
if (settings?.settings?.chat_page_enabled === false) {
router.push("/search");
}
@ -1694,7 +1698,7 @@ export function ChatPage({
ref={inputRef}
className="absolute bottom-0 z-10 w-full"
>
<div className="w-full relative pb-4">
<div className="w-[95%] mx-auto relative mb-8">
{aboveHorizon && (
<div className="pointer-events-none w-full bg-transparent flex sticky justify-center">
<button
@ -1730,6 +1734,35 @@ export function ChatPage({
textAreaRef={textAreaRef}
chatSessionId={chatSessionIdRef.current!}
/>
{enterpriseSettings &&
enterpriseSettings.custom_lower_disclaimer_content && (
<div className="mobile:hidden mt-4 flex items-center justify-center relative w-[95%] mx-auto">
<div className="text-sm text-text-500 max-w-searchbar-max px-4 text-center">
<MinimalMarkdown
className=""
content={
enterpriseSettings.custom_lower_disclaimer_content
}
/>
</div>
</div>
)}
{enterpriseSettings &&
enterpriseSettings.use_custom_logotype && (
<div className="hidden lg:block absolute right-0 bottom-0">
<img
src={
"/api/enterprise-settings/logotype?u=" +
Date.now()
}
alt="logotype"
style={{ objectFit: "contain" }}
className="w-fit h-8"
/>
</div>
)}
</div>
</div>
</div>
@ -1750,7 +1783,7 @@ export function ChatPage({
)}
</div>
</div>
<FixedLogo />
<FixedLogo toggleSidebar={toggleSidebar} />
</div>
</div>
<DocumentSidebar

View File

@ -22,21 +22,20 @@ import { usePopup } from "@/components/admin/connectors/Popup";
import { useRouter } from "next/navigation";
import { CHAT_SESSION_ID_KEY } from "@/lib/drag/constants";
import Cookies from "js-cookie";
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
import { Tooltip } from "@/components/tooltip/Tooltip";
import { Popover } from "@/components/popover/Popover";
const FolderItem = ({
folder,
currentChatId,
isInitiallyExpanded,
initiallySelected,
}: {
folder: Folder;
currentChatId?: number;
isInitiallyExpanded: boolean;
initiallySelected: boolean;
}) => {
const [isExpanded, setIsExpanded] = useState<boolean>(isInitiallyExpanded);
const [isEditing, setIsEditing] = useState<boolean>(false);
const [isEditing, setIsEditing] = useState<boolean>(initiallySelected);
const [editedFolderName, setEditedFolderName] = useState<string>(
folder.folder_name
);
@ -135,6 +134,14 @@ const FolderItem = ({
};
}, []);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (initiallySelected && inputRef.current) {
inputRef.current.focus();
}
}, [initiallySelected]);
const handleDrop = async (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(false);
@ -170,31 +177,6 @@ const FolderItem = ({
isDragOver ? "bg-hover" : ""
}`}
>
{showDeleteConfirm && (
<div
ref={deleteConfirmRef}
className="absolute max-w-xs border z-[100] border-border-medium top-0 right-0 w-[225px] -bo-0 top-2 mt-4 p-2 bg-background-100 rounded shadow-lg z-10"
>
<p className="text-sm mb-2">
Are you sure you want to delete <i>{folder.folder_name}</i>? All the
content inside this folder will also be deleted
</p>
<div className="flex justify-end">
<button
onClick={confirmDelete}
className="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded text-xs mr-2"
>
Yes
</button>
<button
onClick={cancelDelete}
className="bg-gray-300 hover:bg-gray-200 px-2 py-1 rounded text-xs"
>
No
</button>
</div>
</div>
)}
<BasicSelectable fullWidth selected={false}>
<div
onMouseEnter={() => setIsHovering(true)}
@ -214,6 +196,7 @@ const FolderItem = ({
</div>
{isEditing ? (
<input
ref={inputRef}
type="text"
value={editedFolderName}
onChange={handleFolderNameChange}
@ -235,12 +218,43 @@ const FolderItem = ({
<FiEdit2 size={16} />
</div>
<div className="relative">
<div
onClick={handleDeleteClick}
className="hover:bg-black/10 p-1 -m-1 rounded ml-2"
>
<FiTrash size={16} />
</div>
<Popover
open={showDeleteConfirm}
onOpenChange={setShowDeleteConfirm}
content={
<div
onClick={handleDeleteClick}
className="hover:bg-black/10 p-1 -m-1 rounded ml-2"
>
<FiTrash size={16} />
</div>
}
popover={
<div className="p-2 w-[225px] bg-background-100 rounded shadow-lg">
<p className="text-sm mb-2">
Are you sure you want to delete{" "}
<i>{folder.folder_name}</i>? All the content inside
this folder will also be deleted.
</p>
<div className="flex justify-end">
<button
onClick={confirmDelete}
className="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded text-xs mr-2"
>
Yes
</button>
<button
onClick={cancelDelete}
className="bg-gray-300 hover:bg-gray-200 px-2 py-1 rounded text-xs"
>
No
</button>
</div>
</div>
}
side="top"
align="center"
/>
</div>
</div>
)}
@ -285,22 +299,25 @@ export const FolderList = ({
folders,
currentChatId,
openedFolders,
newFolderId,
}: {
folders: Folder[];
currentChatId?: number;
openedFolders?: { [key: number]: boolean };
newFolderId: number | null;
}) => {
if (folders.length === 0) {
return null;
}
return (
<div className="mt-1 mb-1 overflow-y-auto">
<div className="mt-1 mb-1 overflow-visible">
{folders.map((folder) => (
<FolderItem
key={folder.folder_id}
folder={folder}
currentChatId={currentChatId}
initiallySelected={newFolderId == folder.folder_id}
isInitiallyExpanded={
openedFolders ? openedFolders[folder.folder_id] || false : false
}

View File

@ -13,7 +13,7 @@ export async function createFolder(folderName: string): Promise<number> {
throw new Error("Failed to create folder");
}
const data = await response.json();
return data.folder_id;
return data;
}
// Function to add a chat session to a folder

View File

@ -268,14 +268,14 @@ export function ChatInputBar({
return (
<div>
<div className="flex justify-center pb-2 max-w-screen-lg mx-auto mb-2">
<div className="flex justify-center max-w-screen-lg mx-auto">
<div
className="
w-[90%]
max-w-searchbar-max
shrink
relative
desktop:px-4
max-w-searchbar-max
mx-auto
"
>
@ -528,6 +528,7 @@ export function ChatInputBar({
mobilePosition="top-right"
>
<ChatInputOption
toggle
flexPriority="shrink"
name={
selectedAssistant ? selectedAssistant.name : "Assistants"
@ -559,6 +560,7 @@ export function ChatInputBar({
>
<ChatInputOption
flexPriority="second"
toggle
name={
settings?.isMobile
? undefined

View File

@ -1,8 +1,5 @@
import React, { useState, useRef, useEffect } from "react";
import { IconType } from "react-icons";
import { DefaultDropdownElement } from "../../../components/Dropdown";
import { Popover } from "../../../components/popover/Popover";
import { IconProps } from "@/components/icons/icons";
import { ChevronRightIcon, IconProps } from "@/components/icons/icons";
interface ChatInputOptionProps {
name?: string;
@ -10,8 +7,8 @@ interface ChatInputOptionProps {
onClick?: () => void;
size?: number;
tooltipContent?: React.ReactNode;
options?: { name: string; value: number; onClick?: () => void }[];
flexPriority?: "shrink" | "stiff" | "second";
toggle?: boolean;
}
export const ChatInputOption: React.FC<ChatInputOptionProps> = ({
@ -19,9 +16,9 @@ export const ChatInputOption: React.FC<ChatInputOptionProps> = ({
Icon,
// icon: Icon,
size = 16,
options,
flexPriority,
tooltipContent,
toggle,
onClick,
}) => {
const [isDropupVisible, setDropupVisible] = useState(false);
@ -76,7 +73,11 @@ export const ChatInputOption: React.FC<ChatInputOptionProps> = ({
onClick={onClick}
>
<Icon size={size} className="flex-none" />
{name && <span className="text-sm break-all line-clamp-1">{name}</span>}
<div className="flex items-center gap-x-.5">
{name && <span className="text-sm break-all line-clamp-1">{name}</span>}
{toggle && <ChevronRightIcon className="flex-none" size={size} />}
</div>
{isTooltipVisible && tooltipContent && (
<div
className="absolute z-10 p-2 text-sm text-white bg-black rounded shadow-lg"

View File

@ -27,6 +27,37 @@ import {
import { Persona } from "../admin/assistants/interfaces";
import { ReadonlyURLSearchParams } from "next/navigation";
import { SEARCH_PARAM_NAMES } from "./searchParams";
import { Settings } from "../admin/settings/interfaces";
interface ChatRetentionInfo {
chatRetentionDays: number;
daysFromCreation: number;
daysUntilExpiration: number;
showRetentionWarning: boolean;
}
export function getChatRetentionInfo(
chatSession: ChatSession,
settings: Settings
): ChatRetentionInfo {
// If `maximum_chat_retention_days` isn't set- never display retention warning.
const chatRetentionDays = settings.maximum_chat_retention_days || 10000;
const createdDate = new Date(chatSession.time_created);
const today = new Date();
const daysFromCreation = Math.ceil(
(today.getTime() - createdDate.getTime()) / (1000 * 3600 * 24)
);
const daysUntilExpiration = chatRetentionDays - daysFromCreation;
const showRetentionWarning =
chatRetentionDays < 7 ? daysUntilExpiration < 2 : daysUntilExpiration < 7;
return {
chatRetentionDays,
daysFromCreation,
daysUntilExpiration,
showRetentionWarning,
};
}
export async function updateModelOverrideForChatSession(
chatSessionId: number,
@ -682,7 +713,7 @@ export async function useScrollonStream({
// scroll on end of stream if within distance
useEffect(() => {
if (scrollableDivRef?.current && !isStreaming) {
if (scrollDist.current < distance) {
if (scrollDist.current < distance - 50) {
scrollableDivRef?.current?.scrollBy({
left: 0,
top: Math.max(scrollDist.current + 600, 0),

View File

@ -3,7 +3,11 @@
import { useRouter } from "next/navigation";
import { ChatSession } from "../interfaces";
import { useState, useEffect, useContext } from "react";
import { deleteChatSession, renameChatSession } from "../lib";
import {
deleteChatSession,
getChatRetentionInfo,
renameChatSession,
} from "../lib";
import { DeleteChatModal } from "../modal/DeleteChatModal";
import { BasicSelectable } from "@/components/BasicClickable";
import Link from "next/link";
@ -20,6 +24,8 @@ import { Popover } from "@/components/popover/Popover";
import { ShareChatSessionModal } from "../modal/ShareChatSessionModal";
import { CHAT_SESSION_ID_KEY, FOLDER_ID_KEY } from "@/lib/drag/constants";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { WarningCircle } from "@phosphor-icons/react";
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
export function ChatSessionDisplay({
chatSession,
@ -41,13 +47,13 @@ export function ChatSessionDisplay({
showDeleteModal?: (chatSession: ChatSession) => void;
}) {
const router = useRouter();
const [isDeletionModalVisible, setIsDeletionModalVisible] = useState(false);
const [isRenamingChat, setIsRenamingChat] = useState(false);
const [isMoreOptionsDropdownOpen, setIsMoreOptionsDropdownOpen] =
useState(false);
const [isShareModalVisible, setIsShareModalVisible] = useState(false);
const [chatName, setChatName] = useState(chatSession.name);
const [delayedSkipGradient, setDelayedSkipGradient] = useState(skipGradient);
const settings = useContext(SettingsContext);
useEffect(() => {
if (skipGradient) {
@ -69,7 +75,15 @@ export function ChatSessionDisplay({
alert("Failed to rename chat session");
}
};
const settings = useContext(SettingsContext);
if (!settings) {
return <></>;
}
const { daysUntilExpiration, showRetentionWarning } = getChatRetentionInfo(
chatSession,
settings?.settings
);
return (
<>
@ -120,7 +134,7 @@ export function ChatSessionDisplay({
event.preventDefault();
}
}}
className="-my-px px-1 mr-2 w-full rounded"
className="-my-px px-1 mr-1 w-full rounded"
/>
) : (
<p className="break-all overflow-hidden whitespace-nowrap w-full mr-3 relative">
@ -134,10 +148,10 @@ export function ChatSessionDisplay({
{isSelected &&
(isRenamingChat ? (
<div className="ml-auto my-auto flex">
<div className="ml-auto my-auto items-center flex">
<div
onClick={onRename}
className={`hover:bg-black/10 p-1 -m-1 rounded`}
className={`hover:bg-black/10 p-1 -m-1 rounded`}
>
<FiCheck size={16} />
</div>
@ -152,7 +166,25 @@ export function ChatSessionDisplay({
</div>
</div>
) : (
<div className="ml-auto my-auto flex z-30">
<div className="ml-auto my-auto justify-end flex z-30">
{!showShareModal && showRetentionWarning && (
<CustomTooltip
line
content={
<p>
This chat will expire{" "}
{daysUntilExpiration < 1
? "today"
: `in ${daysUntilExpiration} day${daysUntilExpiration !== 1 ? "s" : ""}`}
</p>
}
>
<div className="mr-1 hover:bg-black/10 p-1 -m-1 rounded z-50">
<WarningCircle className="text-warning" />
</div>
</CustomTooltip>
)}
<div>
<div
onClick={() => {
@ -160,7 +192,7 @@ export function ChatSessionDisplay({
!isMoreOptionsDropdownOpen
);
}}
className={"-m-1"}
className={"-my-1"}
>
<Popover
open={isMoreOptionsDropdownOpen}
@ -197,7 +229,7 @@ export function ChatSessionDisplay({
{showDeleteModal && (
<div
onClick={() => showDeleteModal(chatSession)}
className={`hover:bg-black/10 p-1 -m-1 rounded ml-2`}
className={`hover:bg-black/10 p-1 -m-1 rounded ml-1`}
>
<FiTrash size={16} />
</div>

View File

@ -1,7 +1,13 @@
"use client";
import { FiEdit, FiFolderPlus } from "react-icons/fi";
import { ForwardedRef, forwardRef, useContext, useEffect } from "react";
import {
ForwardedRef,
forwardRef,
useContext,
useEffect,
useState,
} from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { ChatSession } from "../interfaces";
@ -56,6 +62,9 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
const router = useRouter();
const { popup, setPopup } = usePopup();
// For determining intial focus state
const [newFolderId, setNewFolderId] = useState<number | null>(null);
const currentChatId = currentChatSession?.id;
// prevent the NextJS Router cache from causing the chat sidebar to not
@ -69,8 +78,6 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
return null;
}
const enterpriseSettings = combinedSettings.enterpriseSettings;
const handleNewChat = () => {
reset();
const newChatUrl =
@ -133,8 +140,8 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
onClick={() =>
createFolder("New Folder")
.then((folderId) => {
console.log(`Folder created with ID: ${folderId}`);
router.refresh();
setNewFolderId(folderId);
})
.catch((error) => {
console.error("Failed to create folder:", error);
@ -172,6 +179,7 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
)}
<div className="border-b border-border pb-4 mx-3" />
<PagesTab
newFolderId={newFolderId}
showDeleteModal={showDeleteModal}
showShareModal={showShareModal}
closeSidebar={removeToggle}

View File

@ -7,7 +7,7 @@ import { Folder } from "../folders/interfaces";
import { CHAT_SESSION_ID_KEY, FOLDER_ID_KEY } from "@/lib/drag/constants";
import { usePopup } from "@/components/admin/connectors/Popup";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useEffect, useState } from "react";
import { pageType } from "./types";
export function PagesTab({
@ -17,6 +17,7 @@ export function PagesTab({
folders,
openedFolders,
closeSidebar,
newFolderId,
showShareModal,
showDeleteModal,
}: {
@ -26,6 +27,7 @@ export function PagesTab({
folders?: Folder[];
openedFolders?: { [key: number]: boolean };
closeSidebar?: () => void;
newFolderId: number | null;
showShareModal?: (chatSession: ChatSession) => void;
showDeleteModal?: (chatSession: ChatSession) => void;
}) {
@ -69,8 +71,10 @@ export function PagesTab({
<div className="py-2 border-b border-border">
<div className="text-xs text-subtle flex pb-0.5 mb-1.5 mt-2 font-bold">
Chat Folders
{newFolderId ? newFolderId : "Hi"}
</div>
<FolderList
newFolderId={newFolderId}
folders={folders}
currentChatId={currentChatId}
openedFolders={openedFolders}

View File

@ -7,14 +7,21 @@ import { NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED } from "@/lib/constan
import { useContext } from "react";
import { FiSidebar } from "react-icons/fi";
export default function FixedLogo() {
export default function FixedLogo({
toggleSidebar = () => null,
}: {
toggleSidebar?: () => void;
}) {
const combinedSettings = useContext(SettingsContext);
const settings = combinedSettings?.settings;
const enterpriseSettings = combinedSettings?.enterpriseSettings;
return (
<>
<div className="fixed pointer-events-none flex z-40 left-2.5 top-2">
<div
onClick={toggleSidebar}
className="fixed cursor-pointer flex z-40 left-2.5 top-2"
>
<div className="max-w-[200px] mobile:hidden flex items-center gap-x-1 my-auto">
<div className="flex-none my-auto">
<Logo height={24} width={24} />

View File

@ -80,7 +80,10 @@ export default function FunctionalWrapper({
initiallyToggled,
content,
}: {
content: (toggledSidebar: boolean, toggle: () => void) => ReactNode;
content: (
toggledSidebar: boolean,
toggle: (toggled?: boolean) => void
) => ReactNode;
initiallyToggled: boolean;
}) {
const router = useRouter();
@ -123,11 +126,9 @@ export default function FunctionalWrapper({
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
const toggle = (value?: boolean) => {
if (value !== undefined) {
setToggledSidebar(value);
} else {
setToggledSidebar((prevState) => !prevState);
}
setToggledSidebar((toggledSidebar) =>
value !== undefined ? value : !toggledSidebar
);
};
return (

View File

@ -16,7 +16,8 @@ import { ImageUpload } from "./ImageUpload";
export function WhitelabelingForm() {
const router = useRouter();
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [selectedLogo, setSelectedLogo] = useState<File | null>(null);
const [selectedLogotype, setSelectedLogotype] = useState<File | null>(null);
const settings = useContext(SettingsContext);
if (!settings) {
@ -49,27 +50,33 @@ export function WhitelabelingForm() {
initialValues={{
application_name: enterpriseSettings?.application_name || null,
use_custom_logo: enterpriseSettings?.use_custom_logo || false,
use_custom_logotype: enterpriseSettings?.use_custom_logotype || false,
custom_header_content:
enterpriseSettings?.custom_header_content || "",
custom_popup_header: enterpriseSettings?.custom_popup_header || "",
custom_popup_content: enterpriseSettings?.custom_popup_content || "",
custom_lower_disclaimer_content:
enterpriseSettings?.custom_lower_disclaimer_content || "",
}}
validationSchema={Yup.object().shape({
application_name: Yup.string().nullable(),
use_custom_logo: Yup.boolean().required(),
use_custom_logotype: Yup.boolean().required(),
custom_header_content: Yup.string().nullable(),
custom_popup_header: Yup.string().nullable(),
custom_popup_content: Yup.string().nullable(),
custom_lower_disclaimer_content: Yup.string().nullable(),
})}
onSubmit={async (values, formikHelpers) => {
formikHelpers.setSubmitting(true);
if (selectedFile) {
if (selectedLogo) {
values.use_custom_logo = true;
const formData = new FormData();
formData.append("file", selectedFile);
setSelectedFile(null);
formData.append("file", selectedLogo);
setSelectedLogo(null);
const response = await fetch(
"/api/admin/enterprise-settings/logo",
{
@ -85,6 +92,27 @@ export function WhitelabelingForm() {
}
}
if (selectedLogotype) {
values.use_custom_logotype = true;
const formData = new FormData();
formData.append("file", selectedLogotype);
setSelectedLogo(null);
const response = await fetch(
"/api/admin/enterprise-settings/logo?is_logotype=true",
{
method: "PUT",
body: formData,
}
);
if (!response.ok) {
const errorMsg = (await response.json()).detail;
alert(`Failed to upload logo. ${errorMsg}`);
formikHelpers.setSubmitting(false);
return;
}
}
formikHelpers.setValues(values);
await updateEnterpriseSettings(values);
}}
@ -138,10 +166,53 @@ export function WhitelabelingForm() {
Specify your own logo to replace the standard Danswer logo.
</SubLabel>
)}
<ImageUpload
selectedFile={selectedFile}
setSelectedFile={setSelectedFile}
selectedFile={selectedLogo}
setSelectedFile={setSelectedLogo}
/>
<br />
<Label>Custom Logotype</Label>
{values.use_custom_logotype ? (
<div className="mt-3">
<SubLabel>Current Custom Logotype: </SubLabel>
<img
src={"/api/enterprise-settings/logotype?u=" + Date.now()}
alt="logotype"
style={{ objectFit: "contain" }}
className="w-32 h-32 mb-10 mt-4"
/>
<Button
color="red"
size="xs"
type="button"
className="mb-8"
onClick={async () => {
const valuesWithoutLogotype = {
...values,
use_custom_logotype: false,
};
await updateEnterpriseSettings(valuesWithoutLogotype);
setValues(valuesWithoutLogotype);
}}
>
Delete
</Button>
<SubLabel>
Override the current custom logotype by uploading a new image
below and clicking the Update button. This logotype is the
text-based logo that will be rendered at the bottom right of
the chat screen.
</SubLabel>
</div>
) : (
<SubLabel>Specify your own logotype</SubLabel>
)}
<ImageUpload
selectedFile={selectedLogotype}
setSelectedFile={setSelectedLogotype}
/>
<Divider />
@ -182,6 +253,17 @@ export function WhitelabelingForm() {
/>
</div>
<div className="mt-4">
<TextFormField
label="Custom Footer"
name="custom_lower_disclaimer_content"
subtext={`Custom Markdown content that will be displayed at the bottom of the Chat page.`}
placeholder="Your disclaimer content..."
isTextArea
disabled={isSubmitting}
/>
</div>
<Button type="submit" className="mt-4">
Update
</Button>

View File

@ -30,9 +30,6 @@ export default function FunctionalHeader({
setSharingModalVisible?: (value: SetStateAction<boolean>) => void;
toggleSidebar?: () => void;
}) {
const combinedSettings = useContext(SettingsContext);
const enterpriseSettings = combinedSettings?.enterpriseSettings;
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.metaKey || event.ctrlKey) {
@ -69,7 +66,7 @@ export default function FunctionalHeader({
};
return (
<div className="pb-6 left-0 sticky top-0 z-20 w-full relative flex">
<div className="mt-2 mx-2.5 text-text-700 relative flex w-full">
<div className="mt-2 mx-2.5 cursor-pointer text-text-700 relative flex w-full">
<LogoType
assistantId={currentChatSession?.persona_id}
page={page}

View File

@ -0,0 +1,35 @@
import React from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
interface MinimalMarkdownProps {
content: string;
className?: string;
}
export const MinimalMarkdown: React.FC<MinimalMarkdownProps> = ({
content,
className = "",
}) => {
return (
<ReactMarkdown
className={`w-full text-wrap break-word ${className}`}
components={{
a: ({ node, ...props }) => (
<a
{...props}
className="text-sm text-link hover:text-link-hover"
target="_blank"
rel="noopener noreferrer"
/>
),
p: ({ node, ...props }) => (
<p {...props} className="text-wrap break-word text-sm m-0 w-full" />
),
}}
remarkPlugins={[remarkGfm]}
>
{content}
</ReactMarkdown>
);
};

View File

@ -52,7 +52,12 @@ export const useSidebarVisibility = ({
!isWithinSidebar &&
!toggledSidebar
) {
setShowDocSidebar(false);
setTimeout(() => {
setShowDocSidebar((showDocSidebar) => {
// Account for possition as point in time of
return !(xPosition.current > sidebarRect.right);
});
}, 200);
} else if (currentXPosition < 100 && !showDocSidebar) {
if (!mobile) {
setShowDocSidebar(true);

View File

@ -6,7 +6,7 @@ import {
NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED,
NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA,
} from "@/lib/constants";
import { LefToLineIcon, NewChatIcon, RightToLineIcon } from "../icons/icons";
import { LeftToLineIcon, NewChatIcon, RightToLineIcon } from "../icons/icons";
import { Tooltip } from "../tooltip/Tooltip";
import { pageType } from "@/app/chat/sessionSidebar/types";
import { Logo } from "../Logo";
@ -50,7 +50,7 @@ export default function LogoType({
</div>
)}
<div
className={` ${showArrow ? "desktop:invisible" : "invisible"} break-words inline-block w-fit ml-2 text-text-700 text-xl`}
className={`bg-black cursor-pointer ${showArrow ? "desktop:invisible" : "invisible"} break-words inline-block w-fit ml-2 text-text-700 text-xl`}
>
<div className="max-w-[175px]">
{enterpriseSettings && enterpriseSettings.application_name ? (
@ -100,7 +100,7 @@ export default function LogoType({
{!toggled && !combinedSettings?.isMobile ? (
<RightToLineIcon />
) : (
<LefToLineIcon />
<LeftToLineIcon />
)}
</button>
</Tooltip>

View File

@ -71,6 +71,7 @@ import slackIcon from "../../../public/Slack.png";
import s3Icon from "../../../public/S3.png";
import r2Icon from "../../../public/r2.png";
import salesforceIcon from "../../../public/Salesforce.png";
import sharepointIcon from "../../../public/Sharepoint.png";
import teamsIcon from "../../../public/Teams.png";
import mediawikiIcon from "../../../public/MediaWiki.svg";
@ -82,8 +83,7 @@ import cohereIcon from "../../../public/Cohere.svg";
import voyageIcon from "../../../public/Voyage.png";
import googleIcon from "../../../public/Google.webp";
import { FaRobot, FaSlack } from "react-icons/fa";
import { IconType } from "react-icons";
import { FaRobot } from "react-icons/fa";
export interface IconProps {
size?: number;
@ -291,7 +291,7 @@ export const AnthropicIcon = ({
);
};
export const LefToLineIcon = ({
export const LeftToLineIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
@ -2489,3 +2489,95 @@ export const ClosedBookIcon = ({
</svg>
);
};
export const PinIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
xmlns="http://www.w3.org/2000/svg"
width="200"
height="200"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="m17.942 6.076l2.442 2.442a1.22 1.22 0 0 1-.147 1.855l-1.757.232a1.697 1.697 0 0 0-.94.452c-.72.696-1.453 1.428-2.674 2.637c-.21.212-.358.478-.427.769l-.94 3.772a1.22 1.22 0 0 1-1.978.379l-3.04-3.052l-3.052-3.04a1.221 1.221 0 0 1 .379-1.978l3.747-.964a1.8 1.8 0 0 0 .77-.44c1.379-1.355 1.88-1.855 2.66-2.698c.233-.25.383-.565.428-.903l.232-1.783a1.221 1.221 0 0 1 1.856-.146zm-9.51 9.498L3.256 20.75"
/>
</svg>
);
};
export const TwoRightArrowIcons = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
xmlns="http://www.w3.org/2000/svg"
width="200"
height="200"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="m5.36 19l5.763-5.763a1.738 1.738 0 0 0 0-2.474L5.36 5m7 14l5.763-5.763a1.738 1.738 0 0 0 0-2.474L12.36 5"
/>
</svg>
);
};
export const PlusIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
xmlns="http://www.w3.org/2000/svg"
width="200"
height="200"
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5z"
/>
</svg>
);
};
export const MinusIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
xmlns="http://www.w3.org/2000/svg"
width="200"
height="200"
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5z"
/>
</svg>
);
};

View File

@ -644,7 +644,7 @@ export const SearchSection = ({
{
<div
className={` overflow-y-auto desktop:px-24 w-full ${chatBannerPresent && "mt-10"} pt-10 relative max-w-[2000px] xl:max-w-[1430px] mx-auto`}
className={`desktop:px-24 w-full ${chatBannerPresent && "mt-10"} pt-10 relative max-w-[2000px] xl:max-w-[1430px] mx-auto`}
>
<div className="absolute z-10 mobile:px-4 mobile:max-w-searchbar-max mobile:w-[90%] top-12 desktop:left-0 hidden 2xl:block mobile:left-1/2 mobile:transform mobile:-translate-x-1/2 desktop:w-52 3xl:w-64">
{!settings?.isMobile &&

View File

@ -1,7 +1,14 @@
import React, { useState } from "react";
import { DocumentSet, Tag, ValidSources } from "@/lib/types";
import { SourceMetadata } from "@/lib/search/interfaces";
import { InfoIcon, defaultTailwindCSS } from "../../icons/icons";
import {
GearIcon,
InfoIcon,
MinusIcon,
PlusCircleIcon,
PlusIcon,
defaultTailwindCSS,
} from "../../icons/icons";
import { HoverPopup } from "../../HoverPopup";
import {
FiBook,
@ -75,6 +82,19 @@ export function SourceSelector({
});
};
let allSourcesSelected = selectedSources.length > 0;
const toggleAllSources = () => {
if (allSourcesSelected) {
setSelectedSources([]);
} else {
const allSources = listSourceMetadata().filter((source) =>
existingSources.includes(source.internalName)
);
setSelectedSources(allSources);
}
};
return (
<div
className={`hidden ${
@ -93,7 +113,17 @@ export function SourceSelector({
{existingSources.length > 0 && (
<div className="mt-4">
<SectionTitle>Sources</SectionTitle>
<div className="flex w-full gap-x-2 items-center">
<div className="font-bold text-xs mt-2 flex items-center gap-x-2">
<p>Sources</p>
<input
type="checkbox"
checked={allSourcesSelected}
onChange={toggleAllSources}
className="my-auto form-checkbox h-3 w-3 text-primary border-background-900 rounded"
/>
</div>
</div>
<div className="px-1">
{listSourceMetadata()
.filter((source) => existingSources.includes(source.internalName))

View File

@ -6,6 +6,7 @@ import React, {
createContext,
useContext,
} from "react";
import { createPortal } from "react-dom";
// Create a context for the tooltip group
const TooltipGroupContext = createContext<{
@ -52,13 +53,14 @@ export const CustomTooltip = ({
light?: boolean;
showTick?: boolean;
delay?: number;
wrap?: boolean;
citation?: boolean;
position?: "top" | "bottom";
}) => {
const [isVisible, setIsVisible] = useState(false);
const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const triggerRef = useRef<HTMLSpanElement>(null);
const { groupHovered, setGroupHovered, hoverCountRef } =
useContext(TooltipGroupContext);
@ -69,6 +71,7 @@ export const CustomTooltip = ({
timeoutRef.current = setTimeout(() => {
setIsVisible(true);
setGroupHovered(true);
updateTooltipPosition();
}, showDelay);
};
@ -85,6 +88,16 @@ export const CustomTooltip = ({
}, 100);
};
const updateTooltipPosition = () => {
if (triggerRef.current) {
const rect = triggerRef.current.getBoundingClientRect();
setTooltipPosition({
top: position === "top" ? rect.top - 10 : rect.bottom + 10,
left: rect.left + rect.width / 2,
});
}
};
useEffect(() => {
return () => {
if (timeoutRef.current) {
@ -94,47 +107,60 @@ export const CustomTooltip = ({
}, []);
return (
<span className="relative inline-block">
<>
<span
className="h-full leading-none"
ref={triggerRef}
className="relative inline-block"
onMouseEnter={showTooltip}
onMouseLeave={hideTooltip}
>
{children}
</span>
{isVisible && (
<div
className={`absolute z-[1000] ${citation ? "max-w-[350px]" : "w-40"} ${large ? "w-96" : line && "max-w-64 w-auto"}
left-1/2 transform -translate-x-1/2 ${position === "top" ? "bottom-full mb-2" : "mt-2"} text-sm
${
light
? "text-gray-800 bg-background-200"
: "text-white bg-background-800"
}
rounded-lg shadow-lg`}
>
{showTick && (
<div
className={`absolute w-3 h-3 -top-1.5 ${position === "top" ? "bottom-1.5" : "-top-1.5"} left-1/2 transform -translate-x-1/2 rotate-45
${light ? "bg-background-200" : "bg-background-800"}`}
/>
)}
{isVisible &&
createPortal(
<div
className={`flex-wrap ${wrap && "w-full"} relative ${line ? "" : "flex"} p-2`}
style={
line || wrap
? {
whiteSpace: wrap ? "normal" : "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}
: {}
}
className={`fixed z-[1000] ${citation ? "max-w-[350px]" : "w-40"} ${
large ? "w-96" : line && "max-w-64 w-auto"
}
transform -translate-x-1/2 text-sm
${
light
? "text-gray-800 bg-background-200"
: "text-white bg-background-800"
}
rounded-lg shadow-lg`}
style={{
top: `${tooltipPosition.top}px`,
left: `${tooltipPosition.left}px`,
}}
>
{content}
</div>
</div>
)}
</span>
{showTick && (
<div
className={`absolute w-3 h-3 ${
position === "top" ? "bottom-1.5" : "-top-1.5"
} left-1/2 transform -translate-x-1/2 rotate-45
${light ? "bg-background-200" : "bg-background-800"}`}
/>
)}
<div
className={`flex-wrap ${wrap && "w-full"} relative ${
line ? "" : "flex"
} p-2`}
style={
line || wrap
? {
whiteSpace: wrap ? "normal" : "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}
: {}
}
>
{content}
</div>
</div>,
document.body
)}
</>
);
};

View File

@ -90,6 +90,9 @@ module.exports = {
"background-200": "#e5e5e5", // neutral-200
"background-300": "#d4d4d4", // neutral-300
"background-400": "#a3a3a3", // neutral-400
"background-500": "#737373", // neutral-400
"background-600": "#525252", // neutral-400
"background-700": "#404040", // neutral-400
"background-800": "#262626", // neutral-800
"background-900": "#111827", // gray-900
"background-inverted": "#000000", // black