mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-04-07 11:28:09 +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:
|
||||
error_msg = str(e)
|
||||
|
||||
logger.exception(f"Failed to process chat message: {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
|
||||
("/enterprise-settings", {"GET"}),
|
||||
("/enterprise-settings/logo", {"GET"}),
|
||||
("/enterprise-settings/logotype", {"GET"}),
|
||||
("/enterprise-settings/custom-analytics-script", {"GET"}),
|
||||
# oidc
|
||||
("/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 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")
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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),
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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} />
|
||||
|
@ -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 (
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
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 &&
|
||||
!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);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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 &&
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user