mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-06-19 12:30:55 +02:00
UX clarity + minor new features (#2136)
This commit is contained in:
parent
d9bcacfae7
commit
680388537b
@ -694,6 +694,7 @@ def stream_chat_message_objects(
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
|
|
||||||
logger.exception(f"Failed to process chat message: {error_msg}")
|
logger.exception(f"Failed to process chat message: {error_msg}")
|
||||||
|
|
||||||
if "Illegal header value b'Bearer '" in error_msg:
|
if "Illegal header value b'Bearer '" in error_msg:
|
||||||
|
@ -8,6 +8,7 @@ EE_PUBLIC_ENDPOINT_SPECS = PUBLIC_ENDPOINT_SPECS + [
|
|||||||
# needs to be accessible prior to user login
|
# needs to be accessible prior to user login
|
||||||
("/enterprise-settings", {"GET"}),
|
("/enterprise-settings", {"GET"}),
|
||||||
("/enterprise-settings/logo", {"GET"}),
|
("/enterprise-settings/logo", {"GET"}),
|
||||||
|
("/enterprise-settings/logotype", {"GET"}),
|
||||||
("/enterprise-settings/custom-analytics-script", {"GET"}),
|
("/enterprise-settings/custom-analytics-script", {"GET"}),
|
||||||
# oidc
|
# oidc
|
||||||
("/auth/oidc/authorize", {"GET"}),
|
("/auth/oidc/authorize", {"GET"}),
|
||||||
|
@ -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 AnalyticsScriptUpload
|
||||||
from ee.danswer.server.enterprise_settings.models import EnterpriseSettings
|
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 _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_analytics_script
|
||||||
from ee.danswer.server.enterprise_settings.store import load_settings
|
from ee.danswer.server.enterprise_settings.store import load_settings
|
||||||
from ee.danswer.server.enterprise_settings.store import store_analytics_script
|
from ee.danswer.server.enterprise_settings.store import store_analytics_script
|
||||||
@ -41,22 +42,38 @@ def fetch_settings() -> EnterpriseSettings:
|
|||||||
@admin_router.put("/logo")
|
@admin_router.put("/logo")
|
||||||
def put_logo(
|
def put_logo(
|
||||||
file: UploadFile,
|
file: UploadFile,
|
||||||
|
is_logotype: bool = False,
|
||||||
db_session: Session = Depends(get_session),
|
db_session: Session = Depends(get_session),
|
||||||
_: User | None = Depends(current_admin_user),
|
_: User | None = Depends(current_admin_user),
|
||||||
) -> None:
|
) -> 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_or_logotype(is_logotype: bool, db_session: Session) -> Response:
|
||||||
def fetch_logo(db_session: Session = Depends(get_session)) -> Response:
|
|
||||||
try:
|
try:
|
||||||
file_store = get_default_file_store(db_session)
|
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
|
# NOTE: specifying "image/jpeg" here, but it still works for pngs
|
||||||
# TODO: do this properly
|
# TODO: do this properly
|
||||||
return Response(content=file_io.read(), media_type="image/jpeg")
|
return Response(content=file_io.read(), media_type="image/jpeg")
|
||||||
except Exception:
|
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")
|
@admin_router.put("/custom-analytics-script")
|
||||||
|
@ -8,8 +8,10 @@ class EnterpriseSettings(BaseModel):
|
|||||||
|
|
||||||
application_name: str | None = None
|
application_name: str | None = None
|
||||||
use_custom_logo: bool = False
|
use_custom_logo: bool = False
|
||||||
|
use_custom_logotype: bool = False
|
||||||
|
|
||||||
# custom Chat components
|
# custom Chat components
|
||||||
|
custom_lower_disclaimer_content: str | None = None
|
||||||
custom_header_content: str | None = None
|
custom_header_content: str | None = None
|
||||||
custom_popup_header: str | None = None
|
custom_popup_header: str | None = None
|
||||||
custom_popup_content: str | None = None
|
custom_popup_content: str | None = None
|
||||||
|
@ -63,6 +63,7 @@ def store_analytics_script(analytics_script_upload: AnalyticsScriptUpload) -> No
|
|||||||
|
|
||||||
|
|
||||||
_LOGO_FILENAME = "__logo__"
|
_LOGO_FILENAME = "__logo__"
|
||||||
|
_LOGOTYPE_FILENAME = "__logotype__"
|
||||||
|
|
||||||
|
|
||||||
def is_valid_file_type(filename: str) -> bool:
|
def is_valid_file_type(filename: str) -> bool:
|
||||||
@ -79,8 +80,7 @@ def guess_file_type(filename: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def upload_logo(
|
def upload_logo(
|
||||||
db_session: Session,
|
db_session: Session, file: UploadFile | str, is_logotype: bool = False
|
||||||
file: UploadFile | str,
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
content: IO[Any]
|
content: IO[Any]
|
||||||
|
|
||||||
@ -111,7 +111,7 @@ def upload_logo(
|
|||||||
|
|
||||||
file_store = get_default_file_store(db_session)
|
file_store = get_default_file_store(db_session)
|
||||||
file_store.save_file(
|
file_store.save_file(
|
||||||
file_name=_LOGO_FILENAME,
|
file_name=_LOGOTYPE_FILENAME if is_logotype else _LOGO_FILENAME,
|
||||||
content=content,
|
content=content,
|
||||||
display_name=display_name,
|
display_name=display_name,
|
||||||
file_origin=FileOrigin.OTHER,
|
file_origin=FileOrigin.OTHER,
|
||||||
|
@ -8,8 +8,10 @@ export interface Settings {
|
|||||||
export interface EnterpriseSettings {
|
export interface EnterpriseSettings {
|
||||||
application_name: string | null;
|
application_name: string | null;
|
||||||
use_custom_logo: boolean;
|
use_custom_logo: boolean;
|
||||||
|
use_custom_logotype: boolean;
|
||||||
|
|
||||||
// custom Chat components
|
// custom Chat components
|
||||||
|
custom_lower_disclaimer_content: string | null;
|
||||||
custom_header_content: string | null;
|
custom_header_content: string | null;
|
||||||
custom_popup_header: string | null;
|
custom_popup_header: string | null;
|
||||||
custom_popup_content: string | null;
|
custom_popup_content: string | null;
|
||||||
|
@ -7,6 +7,7 @@ import remarkGfm from "remark-gfm";
|
|||||||
import { Popover } from "@/components/popover/Popover";
|
import { Popover } from "@/components/popover/Popover";
|
||||||
import { ChevronDownIcon } from "@/components/icons/icons";
|
import { ChevronDownIcon } from "@/components/icons/icons";
|
||||||
import { Divider } from "@tremor/react";
|
import { Divider } from "@tremor/react";
|
||||||
|
import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown";
|
||||||
|
|
||||||
export function ChatBanner() {
|
export function ChatBanner() {
|
||||||
const settings = useContext(SettingsContext);
|
const settings = useContext(SettingsContext);
|
||||||
@ -79,14 +80,19 @@ export function ChatBanner() {
|
|||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
className="line-clamp-2 text-center w-full overflow-hidden pr-8"
|
className="line-clamp-2 text-center w-full overflow-hidden pr-8"
|
||||||
>
|
>
|
||||||
{renderMarkdown("")}
|
<MinimalMarkdown
|
||||||
|
className=""
|
||||||
|
content={settings.enterpriseSettings.custom_header_content}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={fullContentRef}
|
ref={fullContentRef}
|
||||||
className="absolute top-0 left-0 invisible w-full"
|
className="absolute top-0 left-0 invisible w-full"
|
||||||
>
|
>
|
||||||
{renderMarkdown("")}
|
<MinimalMarkdown
|
||||||
|
className=""
|
||||||
|
content={settings.enterpriseSettings.custom_header_content}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-0 right-0 ">
|
<div className="absolute bottom-0 right-0 ">
|
||||||
{isOverflowing && (
|
{isOverflowing && (
|
||||||
@ -104,7 +110,12 @@ export function ChatBanner() {
|
|||||||
popover={
|
popover={
|
||||||
<div className="bg-background-100 p-4 rounded shadow-lg mobile:max-w-xs desktop:max-w-md">
|
<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>
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
import { redirect, useRouter, useSearchParams } from "next/navigation";
|
import { redirect, useRouter, useSearchParams } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
@ -82,6 +83,8 @@ import FixedLogo from "./shared_chat_search/FixedLogo";
|
|||||||
import { getSecondsUntilExpiration } from "@/lib/time";
|
import { getSecondsUntilExpiration } from "@/lib/time";
|
||||||
import { SetDefaultModelModal } from "./modal/SetDefaultModelModal";
|
import { SetDefaultModelModal } from "./modal/SetDefaultModelModal";
|
||||||
import { DeleteChatModal } from "./modal/DeleteChatModal";
|
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_USER_MESSAGE_ID = -1;
|
||||||
const TEMP_ASSISTANT_MESSAGE_ID = -2;
|
const TEMP_ASSISTANT_MESSAGE_ID = -2;
|
||||||
@ -289,7 +292,7 @@ export function ChatPage({
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
clearSelectedDocuments();
|
||||||
setIsFetchingChatMessages(true);
|
setIsFetchingChatMessages(true);
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/chat/get-chat-session/${existingChatSessionId}`
|
`/api/chat/get-chat-session/${existingChatSessionId}`
|
||||||
@ -1110,6 +1113,7 @@ export function ChatPage({
|
|||||||
// settings are passed in via Context and therefore aren't
|
// settings are passed in via Context and therefore aren't
|
||||||
// available in server-side components
|
// available in server-side components
|
||||||
const settings = useContext(SettingsContext);
|
const settings = useContext(SettingsContext);
|
||||||
|
const enterpriseSettings = settings?.enterpriseSettings;
|
||||||
if (settings?.settings?.chat_page_enabled === false) {
|
if (settings?.settings?.chat_page_enabled === false) {
|
||||||
router.push("/search");
|
router.push("/search");
|
||||||
}
|
}
|
||||||
@ -1694,7 +1698,7 @@ export function ChatPage({
|
|||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className="absolute bottom-0 z-10 w-full"
|
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 && (
|
{aboveHorizon && (
|
||||||
<div className="pointer-events-none w-full bg-transparent flex sticky justify-center">
|
<div className="pointer-events-none w-full bg-transparent flex sticky justify-center">
|
||||||
<button
|
<button
|
||||||
@ -1730,6 +1734,35 @@ export function ChatPage({
|
|||||||
textAreaRef={textAreaRef}
|
textAreaRef={textAreaRef}
|
||||||
chatSessionId={chatSessionIdRef.current!}
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1750,7 +1783,7 @@ export function ChatPage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FixedLogo />
|
<FixedLogo toggleSidebar={toggleSidebar} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DocumentSidebar
|
<DocumentSidebar
|
||||||
|
@ -22,21 +22,20 @@ import { usePopup } from "@/components/admin/connectors/Popup";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { CHAT_SESSION_ID_KEY } from "@/lib/drag/constants";
|
import { CHAT_SESSION_ID_KEY } from "@/lib/drag/constants";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
|
|
||||||
import { Tooltip } from "@/components/tooltip/Tooltip";
|
|
||||||
import { Popover } from "@/components/popover/Popover";
|
import { Popover } from "@/components/popover/Popover";
|
||||||
|
|
||||||
const FolderItem = ({
|
const FolderItem = ({
|
||||||
folder,
|
folder,
|
||||||
currentChatId,
|
currentChatId,
|
||||||
isInitiallyExpanded,
|
isInitiallyExpanded,
|
||||||
|
initiallySelected,
|
||||||
}: {
|
}: {
|
||||||
folder: Folder;
|
folder: Folder;
|
||||||
currentChatId?: number;
|
currentChatId?: number;
|
||||||
isInitiallyExpanded: boolean;
|
isInitiallyExpanded: boolean;
|
||||||
|
initiallySelected: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const [isExpanded, setIsExpanded] = useState<boolean>(isInitiallyExpanded);
|
const [isExpanded, setIsExpanded] = useState<boolean>(isInitiallyExpanded);
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
const [isEditing, setIsEditing] = useState<boolean>(initiallySelected);
|
||||||
const [editedFolderName, setEditedFolderName] = useState<string>(
|
const [editedFolderName, setEditedFolderName] = useState<string>(
|
||||||
folder.folder_name
|
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>) => {
|
const handleDrop = async (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setIsDragOver(false);
|
setIsDragOver(false);
|
||||||
@ -170,31 +177,6 @@ const FolderItem = ({
|
|||||||
isDragOver ? "bg-hover" : ""
|
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}>
|
<BasicSelectable fullWidth selected={false}>
|
||||||
<div
|
<div
|
||||||
onMouseEnter={() => setIsHovering(true)}
|
onMouseEnter={() => setIsHovering(true)}
|
||||||
@ -214,6 +196,7 @@ const FolderItem = ({
|
|||||||
</div>
|
</div>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<input
|
<input
|
||||||
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
value={editedFolderName}
|
value={editedFolderName}
|
||||||
onChange={handleFolderNameChange}
|
onChange={handleFolderNameChange}
|
||||||
@ -235,12 +218,43 @@ const FolderItem = ({
|
|||||||
<FiEdit2 size={16} />
|
<FiEdit2 size={16} />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
<Popover
|
||||||
|
open={showDeleteConfirm}
|
||||||
|
onOpenChange={setShowDeleteConfirm}
|
||||||
|
content={
|
||||||
<div
|
<div
|
||||||
onClick={handleDeleteClick}
|
onClick={handleDeleteClick}
|
||||||
className="hover:bg-black/10 p-1 -m-1 rounded ml-2"
|
className="hover:bg-black/10 p-1 -m-1 rounded ml-2"
|
||||||
>
|
>
|
||||||
<FiTrash size={16} />
|
<FiTrash size={16} />
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -285,22 +299,25 @@ export const FolderList = ({
|
|||||||
folders,
|
folders,
|
||||||
currentChatId,
|
currentChatId,
|
||||||
openedFolders,
|
openedFolders,
|
||||||
|
newFolderId,
|
||||||
}: {
|
}: {
|
||||||
folders: Folder[];
|
folders: Folder[];
|
||||||
currentChatId?: number;
|
currentChatId?: number;
|
||||||
openedFolders?: { [key: number]: boolean };
|
openedFolders?: { [key: number]: boolean };
|
||||||
|
newFolderId: number | null;
|
||||||
}) => {
|
}) => {
|
||||||
if (folders.length === 0) {
|
if (folders.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-1 mb-1 overflow-y-auto">
|
<div className="mt-1 mb-1 overflow-visible">
|
||||||
{folders.map((folder) => (
|
{folders.map((folder) => (
|
||||||
<FolderItem
|
<FolderItem
|
||||||
key={folder.folder_id}
|
key={folder.folder_id}
|
||||||
folder={folder}
|
folder={folder}
|
||||||
currentChatId={currentChatId}
|
currentChatId={currentChatId}
|
||||||
|
initiallySelected={newFolderId == folder.folder_id}
|
||||||
isInitiallyExpanded={
|
isInitiallyExpanded={
|
||||||
openedFolders ? openedFolders[folder.folder_id] || false : false
|
openedFolders ? openedFolders[folder.folder_id] || false : false
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ export async function createFolder(folderName: string): Promise<number> {
|
|||||||
throw new Error("Failed to create folder");
|
throw new Error("Failed to create folder");
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.folder_id;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to add a chat session to a folder
|
// Function to add a chat session to a folder
|
||||||
|
@ -268,14 +268,14 @@ export function ChatInputBar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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
|
<div
|
||||||
className="
|
className="
|
||||||
w-[90%]
|
w-[90%]
|
||||||
|
max-w-searchbar-max
|
||||||
shrink
|
shrink
|
||||||
relative
|
relative
|
||||||
desktop:px-4
|
desktop:px-4
|
||||||
max-w-searchbar-max
|
|
||||||
mx-auto
|
mx-auto
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@ -528,6 +528,7 @@ export function ChatInputBar({
|
|||||||
mobilePosition="top-right"
|
mobilePosition="top-right"
|
||||||
>
|
>
|
||||||
<ChatInputOption
|
<ChatInputOption
|
||||||
|
toggle
|
||||||
flexPriority="shrink"
|
flexPriority="shrink"
|
||||||
name={
|
name={
|
||||||
selectedAssistant ? selectedAssistant.name : "Assistants"
|
selectedAssistant ? selectedAssistant.name : "Assistants"
|
||||||
@ -559,6 +560,7 @@ export function ChatInputBar({
|
|||||||
>
|
>
|
||||||
<ChatInputOption
|
<ChatInputOption
|
||||||
flexPriority="second"
|
flexPriority="second"
|
||||||
|
toggle
|
||||||
name={
|
name={
|
||||||
settings?.isMobile
|
settings?.isMobile
|
||||||
? undefined
|
? undefined
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import React, { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import { IconType } from "react-icons";
|
import { ChevronRightIcon, IconProps } from "@/components/icons/icons";
|
||||||
import { DefaultDropdownElement } from "../../../components/Dropdown";
|
|
||||||
import { Popover } from "../../../components/popover/Popover";
|
|
||||||
import { IconProps } from "@/components/icons/icons";
|
|
||||||
|
|
||||||
interface ChatInputOptionProps {
|
interface ChatInputOptionProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -10,8 +7,8 @@ interface ChatInputOptionProps {
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
size?: number;
|
size?: number;
|
||||||
tooltipContent?: React.ReactNode;
|
tooltipContent?: React.ReactNode;
|
||||||
options?: { name: string; value: number; onClick?: () => void }[];
|
|
||||||
flexPriority?: "shrink" | "stiff" | "second";
|
flexPriority?: "shrink" | "stiff" | "second";
|
||||||
|
toggle?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatInputOption: React.FC<ChatInputOptionProps> = ({
|
export const ChatInputOption: React.FC<ChatInputOptionProps> = ({
|
||||||
@ -19,9 +16,9 @@ export const ChatInputOption: React.FC<ChatInputOptionProps> = ({
|
|||||||
Icon,
|
Icon,
|
||||||
// icon: Icon,
|
// icon: Icon,
|
||||||
size = 16,
|
size = 16,
|
||||||
options,
|
|
||||||
flexPriority,
|
flexPriority,
|
||||||
tooltipContent,
|
tooltipContent,
|
||||||
|
toggle,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const [isDropupVisible, setDropupVisible] = useState(false);
|
const [isDropupVisible, setDropupVisible] = useState(false);
|
||||||
@ -76,7 +73,11 @@ export const ChatInputOption: React.FC<ChatInputOptionProps> = ({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<Icon size={size} className="flex-none" />
|
<Icon size={size} className="flex-none" />
|
||||||
|
<div className="flex items-center gap-x-.5">
|
||||||
{name && <span className="text-sm break-all line-clamp-1">{name}</span>}
|
{name && <span className="text-sm break-all line-clamp-1">{name}</span>}
|
||||||
|
{toggle && <ChevronRightIcon className="flex-none" size={size} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
{isTooltipVisible && tooltipContent && (
|
{isTooltipVisible && tooltipContent && (
|
||||||
<div
|
<div
|
||||||
className="absolute z-10 p-2 text-sm text-white bg-black rounded shadow-lg"
|
className="absolute z-10 p-2 text-sm text-white bg-black rounded shadow-lg"
|
||||||
|
@ -27,6 +27,37 @@ import {
|
|||||||
import { Persona } from "../admin/assistants/interfaces";
|
import { Persona } from "../admin/assistants/interfaces";
|
||||||
import { ReadonlyURLSearchParams } from "next/navigation";
|
import { ReadonlyURLSearchParams } from "next/navigation";
|
||||||
import { SEARCH_PARAM_NAMES } from "./searchParams";
|
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(
|
export async function updateModelOverrideForChatSession(
|
||||||
chatSessionId: number,
|
chatSessionId: number,
|
||||||
@ -682,7 +713,7 @@ export async function useScrollonStream({
|
|||||||
// scroll on end of stream if within distance
|
// scroll on end of stream if within distance
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scrollableDivRef?.current && !isStreaming) {
|
if (scrollableDivRef?.current && !isStreaming) {
|
||||||
if (scrollDist.current < distance) {
|
if (scrollDist.current < distance - 50) {
|
||||||
scrollableDivRef?.current?.scrollBy({
|
scrollableDivRef?.current?.scrollBy({
|
||||||
left: 0,
|
left: 0,
|
||||||
top: Math.max(scrollDist.current + 600, 0),
|
top: Math.max(scrollDist.current + 600, 0),
|
||||||
|
@ -3,7 +3,11 @@
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { ChatSession } from "../interfaces";
|
import { ChatSession } from "../interfaces";
|
||||||
import { useState, useEffect, useContext } from "react";
|
import { useState, useEffect, useContext } from "react";
|
||||||
import { deleteChatSession, renameChatSession } from "../lib";
|
import {
|
||||||
|
deleteChatSession,
|
||||||
|
getChatRetentionInfo,
|
||||||
|
renameChatSession,
|
||||||
|
} from "../lib";
|
||||||
import { DeleteChatModal } from "../modal/DeleteChatModal";
|
import { DeleteChatModal } from "../modal/DeleteChatModal";
|
||||||
import { BasicSelectable } from "@/components/BasicClickable";
|
import { BasicSelectable } from "@/components/BasicClickable";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@ -20,6 +24,8 @@ import { Popover } from "@/components/popover/Popover";
|
|||||||
import { ShareChatSessionModal } from "../modal/ShareChatSessionModal";
|
import { ShareChatSessionModal } from "../modal/ShareChatSessionModal";
|
||||||
import { CHAT_SESSION_ID_KEY, FOLDER_ID_KEY } from "@/lib/drag/constants";
|
import { CHAT_SESSION_ID_KEY, FOLDER_ID_KEY } from "@/lib/drag/constants";
|
||||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||||
|
import { WarningCircle } from "@phosphor-icons/react";
|
||||||
|
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
|
||||||
|
|
||||||
export function ChatSessionDisplay({
|
export function ChatSessionDisplay({
|
||||||
chatSession,
|
chatSession,
|
||||||
@ -41,13 +47,13 @@ export function ChatSessionDisplay({
|
|||||||
showDeleteModal?: (chatSession: ChatSession) => void;
|
showDeleteModal?: (chatSession: ChatSession) => void;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isDeletionModalVisible, setIsDeletionModalVisible] = useState(false);
|
|
||||||
const [isRenamingChat, setIsRenamingChat] = useState(false);
|
const [isRenamingChat, setIsRenamingChat] = useState(false);
|
||||||
const [isMoreOptionsDropdownOpen, setIsMoreOptionsDropdownOpen] =
|
const [isMoreOptionsDropdownOpen, setIsMoreOptionsDropdownOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [isShareModalVisible, setIsShareModalVisible] = useState(false);
|
const [isShareModalVisible, setIsShareModalVisible] = useState(false);
|
||||||
const [chatName, setChatName] = useState(chatSession.name);
|
const [chatName, setChatName] = useState(chatSession.name);
|
||||||
const [delayedSkipGradient, setDelayedSkipGradient] = useState(skipGradient);
|
const [delayedSkipGradient, setDelayedSkipGradient] = useState(skipGradient);
|
||||||
|
const settings = useContext(SettingsContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (skipGradient) {
|
if (skipGradient) {
|
||||||
@ -69,7 +75,15 @@ export function ChatSessionDisplay({
|
|||||||
alert("Failed to rename chat session");
|
alert("Failed to rename chat session");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const settings = useContext(SettingsContext);
|
|
||||||
|
if (!settings) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { daysUntilExpiration, showRetentionWarning } = getChatRetentionInfo(
|
||||||
|
chatSession,
|
||||||
|
settings?.settings
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -120,7 +134,7 @@ export function ChatSessionDisplay({
|
|||||||
event.preventDefault();
|
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">
|
<p className="break-all overflow-hidden whitespace-nowrap w-full mr-3 relative">
|
||||||
@ -134,7 +148,7 @@ export function ChatSessionDisplay({
|
|||||||
|
|
||||||
{isSelected &&
|
{isSelected &&
|
||||||
(isRenamingChat ? (
|
(isRenamingChat ? (
|
||||||
<div className="ml-auto my-auto flex">
|
<div className="ml-auto my-auto items-center flex">
|
||||||
<div
|
<div
|
||||||
onClick={onRename}
|
onClick={onRename}
|
||||||
className={`hover:bg-black/10 p-1 -m-1 rounded`}
|
className={`hover:bg-black/10 p-1 -m-1 rounded`}
|
||||||
@ -152,7 +166,25 @@ export function ChatSessionDisplay({
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -160,7 +192,7 @@ export function ChatSessionDisplay({
|
|||||||
!isMoreOptionsDropdownOpen
|
!isMoreOptionsDropdownOpen
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className={"-m-1"}
|
className={"-my-1"}
|
||||||
>
|
>
|
||||||
<Popover
|
<Popover
|
||||||
open={isMoreOptionsDropdownOpen}
|
open={isMoreOptionsDropdownOpen}
|
||||||
@ -197,7 +229,7 @@ export function ChatSessionDisplay({
|
|||||||
{showDeleteModal && (
|
{showDeleteModal && (
|
||||||
<div
|
<div
|
||||||
onClick={() => showDeleteModal(chatSession)}
|
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} />
|
<FiTrash size={16} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FiEdit, FiFolderPlus } from "react-icons/fi";
|
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 Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { ChatSession } from "../interfaces";
|
import { ChatSession } from "../interfaces";
|
||||||
@ -56,6 +62,9 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { popup, setPopup } = usePopup();
|
const { popup, setPopup } = usePopup();
|
||||||
|
|
||||||
|
// For determining intial focus state
|
||||||
|
const [newFolderId, setNewFolderId] = useState<number | null>(null);
|
||||||
|
|
||||||
const currentChatId = currentChatSession?.id;
|
const currentChatId = currentChatSession?.id;
|
||||||
|
|
||||||
// prevent the NextJS Router cache from causing the chat sidebar to not
|
// prevent the NextJS Router cache from causing the chat sidebar to not
|
||||||
@ -69,8 +78,6 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const enterpriseSettings = combinedSettings.enterpriseSettings;
|
|
||||||
|
|
||||||
const handleNewChat = () => {
|
const handleNewChat = () => {
|
||||||
reset();
|
reset();
|
||||||
const newChatUrl =
|
const newChatUrl =
|
||||||
@ -133,8 +140,8 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
createFolder("New Folder")
|
createFolder("New Folder")
|
||||||
.then((folderId) => {
|
.then((folderId) => {
|
||||||
console.log(`Folder created with ID: ${folderId}`);
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
setNewFolderId(folderId);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Failed to create folder:", 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" />
|
<div className="border-b border-border pb-4 mx-3" />
|
||||||
<PagesTab
|
<PagesTab
|
||||||
|
newFolderId={newFolderId}
|
||||||
showDeleteModal={showDeleteModal}
|
showDeleteModal={showDeleteModal}
|
||||||
showShareModal={showShareModal}
|
showShareModal={showShareModal}
|
||||||
closeSidebar={removeToggle}
|
closeSidebar={removeToggle}
|
||||||
|
@ -7,7 +7,7 @@ import { Folder } from "../folders/interfaces";
|
|||||||
import { CHAT_SESSION_ID_KEY, FOLDER_ID_KEY } from "@/lib/drag/constants";
|
import { CHAT_SESSION_ID_KEY, FOLDER_ID_KEY } from "@/lib/drag/constants";
|
||||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { pageType } from "./types";
|
import { pageType } from "./types";
|
||||||
|
|
||||||
export function PagesTab({
|
export function PagesTab({
|
||||||
@ -17,6 +17,7 @@ export function PagesTab({
|
|||||||
folders,
|
folders,
|
||||||
openedFolders,
|
openedFolders,
|
||||||
closeSidebar,
|
closeSidebar,
|
||||||
|
newFolderId,
|
||||||
showShareModal,
|
showShareModal,
|
||||||
showDeleteModal,
|
showDeleteModal,
|
||||||
}: {
|
}: {
|
||||||
@ -26,6 +27,7 @@ export function PagesTab({
|
|||||||
folders?: Folder[];
|
folders?: Folder[];
|
||||||
openedFolders?: { [key: number]: boolean };
|
openedFolders?: { [key: number]: boolean };
|
||||||
closeSidebar?: () => void;
|
closeSidebar?: () => void;
|
||||||
|
newFolderId: number | null;
|
||||||
showShareModal?: (chatSession: ChatSession) => void;
|
showShareModal?: (chatSession: ChatSession) => void;
|
||||||
showDeleteModal?: (chatSession: ChatSession) => void;
|
showDeleteModal?: (chatSession: ChatSession) => void;
|
||||||
}) {
|
}) {
|
||||||
@ -69,8 +71,10 @@ export function PagesTab({
|
|||||||
<div className="py-2 border-b border-border">
|
<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">
|
<div className="text-xs text-subtle flex pb-0.5 mb-1.5 mt-2 font-bold">
|
||||||
Chat Folders
|
Chat Folders
|
||||||
|
{newFolderId ? newFolderId : "Hi"}
|
||||||
</div>
|
</div>
|
||||||
<FolderList
|
<FolderList
|
||||||
|
newFolderId={newFolderId}
|
||||||
folders={folders}
|
folders={folders}
|
||||||
currentChatId={currentChatId}
|
currentChatId={currentChatId}
|
||||||
openedFolders={openedFolders}
|
openedFolders={openedFolders}
|
||||||
|
@ -7,14 +7,21 @@ import { NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED } from "@/lib/constan
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { FiSidebar } from "react-icons/fi";
|
import { FiSidebar } from "react-icons/fi";
|
||||||
|
|
||||||
export default function FixedLogo() {
|
export default function FixedLogo({
|
||||||
|
toggleSidebar = () => null,
|
||||||
|
}: {
|
||||||
|
toggleSidebar?: () => void;
|
||||||
|
}) {
|
||||||
const combinedSettings = useContext(SettingsContext);
|
const combinedSettings = useContext(SettingsContext);
|
||||||
const settings = combinedSettings?.settings;
|
const settings = combinedSettings?.settings;
|
||||||
const enterpriseSettings = combinedSettings?.enterpriseSettings;
|
const enterpriseSettings = combinedSettings?.enterpriseSettings;
|
||||||
|
|
||||||
return (
|
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="max-w-[200px] mobile:hidden flex items-center gap-x-1 my-auto">
|
||||||
<div className="flex-none my-auto">
|
<div className="flex-none my-auto">
|
||||||
<Logo height={24} width={24} />
|
<Logo height={24} width={24} />
|
||||||
|
@ -80,7 +80,10 @@ export default function FunctionalWrapper({
|
|||||||
initiallyToggled,
|
initiallyToggled,
|
||||||
content,
|
content,
|
||||||
}: {
|
}: {
|
||||||
content: (toggledSidebar: boolean, toggle: () => void) => ReactNode;
|
content: (
|
||||||
|
toggledSidebar: boolean,
|
||||||
|
toggle: (toggled?: boolean) => void
|
||||||
|
) => ReactNode;
|
||||||
initiallyToggled: boolean;
|
initiallyToggled: boolean;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -123,11 +126,9 @@ export default function FunctionalWrapper({
|
|||||||
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
|
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
|
||||||
|
|
||||||
const toggle = (value?: boolean) => {
|
const toggle = (value?: boolean) => {
|
||||||
if (value !== undefined) {
|
setToggledSidebar((toggledSidebar) =>
|
||||||
setToggledSidebar(value);
|
value !== undefined ? value : !toggledSidebar
|
||||||
} else {
|
);
|
||||||
setToggledSidebar((prevState) => !prevState);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -16,7 +16,8 @@ import { ImageUpload } from "./ImageUpload";
|
|||||||
|
|
||||||
export function WhitelabelingForm() {
|
export function WhitelabelingForm() {
|
||||||
const router = useRouter();
|
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);
|
const settings = useContext(SettingsContext);
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
@ -49,27 +50,33 @@ export function WhitelabelingForm() {
|
|||||||
initialValues={{
|
initialValues={{
|
||||||
application_name: enterpriseSettings?.application_name || null,
|
application_name: enterpriseSettings?.application_name || null,
|
||||||
use_custom_logo: enterpriseSettings?.use_custom_logo || false,
|
use_custom_logo: enterpriseSettings?.use_custom_logo || false,
|
||||||
|
use_custom_logotype: enterpriseSettings?.use_custom_logotype || false,
|
||||||
|
|
||||||
custom_header_content:
|
custom_header_content:
|
||||||
enterpriseSettings?.custom_header_content || "",
|
enterpriseSettings?.custom_header_content || "",
|
||||||
custom_popup_header: enterpriseSettings?.custom_popup_header || "",
|
custom_popup_header: enterpriseSettings?.custom_popup_header || "",
|
||||||
custom_popup_content: enterpriseSettings?.custom_popup_content || "",
|
custom_popup_content: enterpriseSettings?.custom_popup_content || "",
|
||||||
|
custom_lower_disclaimer_content:
|
||||||
|
enterpriseSettings?.custom_lower_disclaimer_content || "",
|
||||||
}}
|
}}
|
||||||
validationSchema={Yup.object().shape({
|
validationSchema={Yup.object().shape({
|
||||||
application_name: Yup.string().nullable(),
|
application_name: Yup.string().nullable(),
|
||||||
use_custom_logo: Yup.boolean().required(),
|
use_custom_logo: Yup.boolean().required(),
|
||||||
|
use_custom_logotype: Yup.boolean().required(),
|
||||||
custom_header_content: Yup.string().nullable(),
|
custom_header_content: Yup.string().nullable(),
|
||||||
custom_popup_header: Yup.string().nullable(),
|
custom_popup_header: Yup.string().nullable(),
|
||||||
custom_popup_content: Yup.string().nullable(),
|
custom_popup_content: Yup.string().nullable(),
|
||||||
|
custom_lower_disclaimer_content: Yup.string().nullable(),
|
||||||
})}
|
})}
|
||||||
onSubmit={async (values, formikHelpers) => {
|
onSubmit={async (values, formikHelpers) => {
|
||||||
formikHelpers.setSubmitting(true);
|
formikHelpers.setSubmitting(true);
|
||||||
|
|
||||||
if (selectedFile) {
|
if (selectedLogo) {
|
||||||
values.use_custom_logo = true;
|
values.use_custom_logo = true;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", selectedFile);
|
formData.append("file", selectedLogo);
|
||||||
setSelectedFile(null);
|
setSelectedLogo(null);
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
"/api/admin/enterprise-settings/logo",
|
"/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);
|
formikHelpers.setValues(values);
|
||||||
await updateEnterpriseSettings(values);
|
await updateEnterpriseSettings(values);
|
||||||
}}
|
}}
|
||||||
@ -138,10 +166,53 @@ export function WhitelabelingForm() {
|
|||||||
Specify your own logo to replace the standard Danswer logo.
|
Specify your own logo to replace the standard Danswer logo.
|
||||||
</SubLabel>
|
</SubLabel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
selectedFile={selectedFile}
|
selectedFile={selectedLogo}
|
||||||
setSelectedFile={setSelectedFile}
|
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 />
|
<Divider />
|
||||||
@ -182,6 +253,17 @@ export function WhitelabelingForm() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<Button type="submit" className="mt-4">
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -30,9 +30,6 @@ export default function FunctionalHeader({
|
|||||||
setSharingModalVisible?: (value: SetStateAction<boolean>) => void;
|
setSharingModalVisible?: (value: SetStateAction<boolean>) => void;
|
||||||
toggleSidebar?: () => void;
|
toggleSidebar?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const combinedSettings = useContext(SettingsContext);
|
|
||||||
const enterpriseSettings = combinedSettings?.enterpriseSettings;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.metaKey || event.ctrlKey) {
|
if (event.metaKey || event.ctrlKey) {
|
||||||
@ -69,7 +66,7 @@ export default function FunctionalHeader({
|
|||||||
};
|
};
|
||||||
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-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
|
<LogoType
|
||||||
assistantId={currentChatSession?.persona_id}
|
assistantId={currentChatSession?.persona_id}
|
||||||
page={page}
|
page={page}
|
||||||
|
35
web/src/components/chat_search/MinimalMarkdown.tsx
Normal file
35
web/src/components/chat_search/MinimalMarkdown.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -52,7 +52,12 @@ export const useSidebarVisibility = ({
|
|||||||
!isWithinSidebar &&
|
!isWithinSidebar &&
|
||||||
!toggledSidebar
|
!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) {
|
} else if (currentXPosition < 100 && !showDocSidebar) {
|
||||||
if (!mobile) {
|
if (!mobile) {
|
||||||
setShowDocSidebar(true);
|
setShowDocSidebar(true);
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED,
|
NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED,
|
||||||
NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA,
|
NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA,
|
||||||
} from "@/lib/constants";
|
} from "@/lib/constants";
|
||||||
import { LefToLineIcon, NewChatIcon, RightToLineIcon } from "../icons/icons";
|
import { LeftToLineIcon, NewChatIcon, RightToLineIcon } from "../icons/icons";
|
||||||
import { Tooltip } from "../tooltip/Tooltip";
|
import { Tooltip } from "../tooltip/Tooltip";
|
||||||
import { pageType } from "@/app/chat/sessionSidebar/types";
|
import { pageType } from "@/app/chat/sessionSidebar/types";
|
||||||
import { Logo } from "../Logo";
|
import { Logo } from "../Logo";
|
||||||
@ -50,7 +50,7 @@ export default function LogoType({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<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]">
|
<div className="max-w-[175px]">
|
||||||
{enterpriseSettings && enterpriseSettings.application_name ? (
|
{enterpriseSettings && enterpriseSettings.application_name ? (
|
||||||
@ -100,7 +100,7 @@ export default function LogoType({
|
|||||||
{!toggled && !combinedSettings?.isMobile ? (
|
{!toggled && !combinedSettings?.isMobile ? (
|
||||||
<RightToLineIcon />
|
<RightToLineIcon />
|
||||||
) : (
|
) : (
|
||||||
<LefToLineIcon />
|
<LeftToLineIcon />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -71,6 +71,7 @@ import slackIcon from "../../../public/Slack.png";
|
|||||||
import s3Icon from "../../../public/S3.png";
|
import s3Icon from "../../../public/S3.png";
|
||||||
import r2Icon from "../../../public/r2.png";
|
import r2Icon from "../../../public/r2.png";
|
||||||
import salesforceIcon from "../../../public/Salesforce.png";
|
import salesforceIcon from "../../../public/Salesforce.png";
|
||||||
|
|
||||||
import sharepointIcon from "../../../public/Sharepoint.png";
|
import sharepointIcon from "../../../public/Sharepoint.png";
|
||||||
import teamsIcon from "../../../public/Teams.png";
|
import teamsIcon from "../../../public/Teams.png";
|
||||||
import mediawikiIcon from "../../../public/MediaWiki.svg";
|
import mediawikiIcon from "../../../public/MediaWiki.svg";
|
||||||
@ -82,8 +83,7 @@ import cohereIcon from "../../../public/Cohere.svg";
|
|||||||
import voyageIcon from "../../../public/Voyage.png";
|
import voyageIcon from "../../../public/Voyage.png";
|
||||||
import googleIcon from "../../../public/Google.webp";
|
import googleIcon from "../../../public/Google.webp";
|
||||||
|
|
||||||
import { FaRobot, FaSlack } from "react-icons/fa";
|
import { FaRobot } from "react-icons/fa";
|
||||||
import { IconType } from "react-icons";
|
|
||||||
|
|
||||||
export interface IconProps {
|
export interface IconProps {
|
||||||
size?: number;
|
size?: number;
|
||||||
@ -291,7 +291,7 @@ export const AnthropicIcon = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LefToLineIcon = ({
|
export const LeftToLineIcon = ({
|
||||||
size = 16,
|
size = 16,
|
||||||
className = defaultTailwindCSS,
|
className = defaultTailwindCSS,
|
||||||
}: IconProps) => {
|
}: IconProps) => {
|
||||||
@ -2489,3 +2489,95 @@ export const ClosedBookIcon = ({
|
|||||||
</svg>
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -644,7 +644,7 @@ export const SearchSection = ({
|
|||||||
|
|
||||||
{
|
{
|
||||||
<div
|
<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">
|
<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 &&
|
{!settings?.isMobile &&
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { DocumentSet, Tag, ValidSources } from "@/lib/types";
|
import { DocumentSet, Tag, ValidSources } from "@/lib/types";
|
||||||
import { SourceMetadata } from "@/lib/search/interfaces";
|
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 { HoverPopup } from "../../HoverPopup";
|
||||||
import {
|
import {
|
||||||
FiBook,
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`hidden ${
|
className={`hidden ${
|
||||||
@ -93,7 +113,17 @@ export function SourceSelector({
|
|||||||
|
|
||||||
{existingSources.length > 0 && (
|
{existingSources.length > 0 && (
|
||||||
<div className="mt-4">
|
<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">
|
<div className="px-1">
|
||||||
{listSourceMetadata()
|
{listSourceMetadata()
|
||||||
.filter((source) => existingSources.includes(source.internalName))
|
.filter((source) => existingSources.includes(source.internalName))
|
||||||
|
@ -6,6 +6,7 @@ import React, {
|
|||||||
createContext,
|
createContext,
|
||||||
useContext,
|
useContext,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
// Create a context for the tooltip group
|
// Create a context for the tooltip group
|
||||||
const TooltipGroupContext = createContext<{
|
const TooltipGroupContext = createContext<{
|
||||||
@ -52,13 +53,14 @@ export const CustomTooltip = ({
|
|||||||
light?: boolean;
|
light?: boolean;
|
||||||
showTick?: boolean;
|
showTick?: boolean;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
|
|
||||||
wrap?: boolean;
|
wrap?: boolean;
|
||||||
citation?: boolean;
|
citation?: boolean;
|
||||||
position?: "top" | "bottom";
|
position?: "top" | "bottom";
|
||||||
}) => {
|
}) => {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
|
||||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const triggerRef = useRef<HTMLSpanElement>(null);
|
||||||
const { groupHovered, setGroupHovered, hoverCountRef } =
|
const { groupHovered, setGroupHovered, hoverCountRef } =
|
||||||
useContext(TooltipGroupContext);
|
useContext(TooltipGroupContext);
|
||||||
|
|
||||||
@ -69,6 +71,7 @@ export const CustomTooltip = ({
|
|||||||
timeoutRef.current = setTimeout(() => {
|
timeoutRef.current = setTimeout(() => {
|
||||||
setIsVisible(true);
|
setIsVisible(true);
|
||||||
setGroupHovered(true);
|
setGroupHovered(true);
|
||||||
|
updateTooltipPosition();
|
||||||
}, showDelay);
|
}, showDelay);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -85,6 +88,16 @@ export const CustomTooltip = ({
|
|||||||
}, 100);
|
}, 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(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (timeoutRef.current) {
|
if (timeoutRef.current) {
|
||||||
@ -94,33 +107,45 @@ export const CustomTooltip = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="relative inline-block">
|
<>
|
||||||
<span
|
<span
|
||||||
className="h-full leading-none"
|
ref={triggerRef}
|
||||||
|
className="relative inline-block"
|
||||||
onMouseEnter={showTooltip}
|
onMouseEnter={showTooltip}
|
||||||
onMouseLeave={hideTooltip}
|
onMouseLeave={hideTooltip}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
{isVisible && (
|
{isVisible &&
|
||||||
|
createPortal(
|
||||||
<div
|
<div
|
||||||
className={`absolute z-[1000] ${citation ? "max-w-[350px]" : "w-40"} ${large ? "w-96" : line && "max-w-64 w-auto"}
|
className={`fixed z-[1000] ${citation ? "max-w-[350px]" : "w-40"} ${
|
||||||
left-1/2 transform -translate-x-1/2 ${position === "top" ? "bottom-full mb-2" : "mt-2"} text-sm
|
large ? "w-96" : line && "max-w-64 w-auto"
|
||||||
|
}
|
||||||
|
transform -translate-x-1/2 text-sm
|
||||||
${
|
${
|
||||||
light
|
light
|
||||||
? "text-gray-800 bg-background-200"
|
? "text-gray-800 bg-background-200"
|
||||||
: "text-white bg-background-800"
|
: "text-white bg-background-800"
|
||||||
}
|
}
|
||||||
rounded-lg shadow-lg`}
|
rounded-lg shadow-lg`}
|
||||||
|
style={{
|
||||||
|
top: `${tooltipPosition.top}px`,
|
||||||
|
left: `${tooltipPosition.left}px`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{showTick && (
|
{showTick && (
|
||||||
<div
|
<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
|
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"}`}
|
${light ? "bg-background-200" : "bg-background-800"}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`flex-wrap ${wrap && "w-full"} relative ${line ? "" : "flex"} p-2`}
|
className={`flex-wrap ${wrap && "w-full"} relative ${
|
||||||
|
line ? "" : "flex"
|
||||||
|
} p-2`}
|
||||||
style={
|
style={
|
||||||
line || wrap
|
line || wrap
|
||||||
? {
|
? {
|
||||||
@ -133,8 +158,9 @@ export const CustomTooltip = ({
|
|||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body
|
||||||
)}
|
)}
|
||||||
</span>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -90,6 +90,9 @@ module.exports = {
|
|||||||
"background-200": "#e5e5e5", // neutral-200
|
"background-200": "#e5e5e5", // neutral-200
|
||||||
"background-300": "#d4d4d4", // neutral-300
|
"background-300": "#d4d4d4", // neutral-300
|
||||||
"background-400": "#a3a3a3", // neutral-400
|
"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-800": "#262626", // neutral-800
|
||||||
"background-900": "#111827", // gray-900
|
"background-900": "#111827", // gray-900
|
||||||
"background-inverted": "#000000", // black
|
"background-inverted": "#000000", // black
|
||||||
|
Loading…
x
Reference in New Issue
Block a user