mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-25 11:16:43 +02:00
Add custom-styling ability via themes
This commit is contained in:
@@ -208,6 +208,7 @@ services:
|
|||||||
- NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
- NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
||||||
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
||||||
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
||||||
|
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
|
||||||
- NEXT_PUBLIC_EE_ENABLED=${NEXT_PUBLIC_EE_ENABLED:-true}
|
- NEXT_PUBLIC_EE_ENABLED=${NEXT_PUBLIC_EE_ENABLED:-true}
|
||||||
depends_on:
|
depends_on:
|
||||||
- api_server
|
- api_server
|
||||||
@@ -215,6 +216,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- INTERNAL_URL=http://api_server:8080
|
- INTERNAL_URL=http://api_server:8080
|
||||||
- WEB_DOMAIN=${WEB_DOMAIN:-}
|
- WEB_DOMAIN=${WEB_DOMAIN:-}
|
||||||
|
- THEME_IS_DARK=${THEME_IS_DARK:-}
|
||||||
|
|
||||||
|
|
||||||
inference_model_server:
|
inference_model_server:
|
||||||
@@ -271,7 +273,6 @@ services:
|
|||||||
max-size: "50m"
|
max-size: "50m"
|
||||||
max-file: "6"
|
max-file: "6"
|
||||||
|
|
||||||
|
|
||||||
relational_db:
|
relational_db:
|
||||||
image: postgres:15.2-alpine
|
image: postgres:15.2-alpine
|
||||||
restart: always
|
restart: always
|
||||||
|
@@ -70,6 +70,7 @@ services:
|
|||||||
- NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
- NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
||||||
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
||||||
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
||||||
|
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
|
||||||
- NEXT_PUBLIC_EE_ENABLED=${NEXT_PUBLIC_EE_ENABLED:-true}
|
- NEXT_PUBLIC_EE_ENABLED=${NEXT_PUBLIC_EE_ENABLED:-true}
|
||||||
depends_on:
|
depends_on:
|
||||||
- api_server
|
- api_server
|
||||||
|
@@ -70,6 +70,7 @@ services:
|
|||||||
- NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
- NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
||||||
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
||||||
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
||||||
|
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
|
||||||
- NEXT_PUBLIC_EE_ENABLED=${NEXT_PUBLIC_EE_ENABLED:-true}
|
- NEXT_PUBLIC_EE_ENABLED=${NEXT_PUBLIC_EE_ENABLED:-true}
|
||||||
depends_on:
|
depends_on:
|
||||||
- api_server
|
- api_server
|
||||||
|
@@ -53,6 +53,9 @@ ENV NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PRED
|
|||||||
|
|
||||||
ARG NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS
|
ARG NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS
|
||||||
ENV NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS}
|
ENV NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS}
|
||||||
|
ARG NEXT_PUBLIC_THEME
|
||||||
|
ENV NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME}
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_EE_ENABLED
|
ARG NEXT_PUBLIC_EE_ENABLED
|
||||||
ENV NEXT_PUBLIC_EE_ENABLED=${NEXT_PUBLIC_EE_ENABLED}
|
ENV NEXT_PUBLIC_EE_ENABLED=${NEXT_PUBLIC_EE_ENABLED}
|
||||||
|
|
||||||
@@ -103,6 +106,9 @@ ENV NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PRED
|
|||||||
|
|
||||||
ARG NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS
|
ARG NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS
|
||||||
ENV NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS}
|
ENV NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS}
|
||||||
|
ARG NEXT_PUBLIC_THEME
|
||||||
|
ENV NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME}
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_EE_ENABLED
|
ARG NEXT_PUBLIC_EE_ENABLED
|
||||||
ENV NEXT_PUBLIC_EE_ENABLED=${NEXT_PUBLIC_EE_ENABLED}
|
ENV NEXT_PUBLIC_EE_ENABLED=${NEXT_PUBLIC_EE_ENABLED}
|
||||||
|
|
||||||
|
@@ -160,7 +160,7 @@ export function Explorer({
|
|||||||
<div>
|
<div>
|
||||||
{popup}
|
{popup}
|
||||||
<div className="justify-center py-2">
|
<div className="justify-center py-2">
|
||||||
<div className="flex items-center w-full border-2 border-border rounded-lg px-4 py-2 focus-within:border-accent">
|
<div className="flex items-center w-full border-2 border-border rounded-lg px-4 py-2 focus-within:border-accent bg-background-search">
|
||||||
<MagnifyingGlass />
|
<MagnifyingGlass />
|
||||||
<textarea
|
<textarea
|
||||||
autoFocus
|
autoFocus
|
||||||
|
@@ -4,6 +4,10 @@ export interface Settings {
|
|||||||
default_page: "search" | "chat";
|
default_page: "search" | "chat";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ColorConfig {
|
||||||
|
primary: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EnterpriseSettings {
|
export interface EnterpriseSettings {
|
||||||
application_name: string | null;
|
application_name: string | null;
|
||||||
use_custom_logo: boolean;
|
use_custom_logo: boolean;
|
||||||
@@ -12,6 +16,7 @@ export interface EnterpriseSettings {
|
|||||||
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;
|
||||||
|
color_config: ColorConfig | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CombinedSettings {
|
export interface CombinedSettings {
|
||||||
|
968
web/src/app/chat/Chat.tsx
Normal file
968
web/src/app/chat/Chat.tsx
Normal file
@@ -0,0 +1,968 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { FiSend, FiShare2, FiStopCircle } from "react-icons/fi";
|
||||||
|
import { AIMessage, HumanMessage } from "./message/Messages";
|
||||||
|
import { AnswerPiecePacket, DanswerDocument } from "@/lib/search/interfaces";
|
||||||
|
import {
|
||||||
|
BackendChatSession,
|
||||||
|
BackendMessage,
|
||||||
|
ChatSessionSharedStatus,
|
||||||
|
DocumentsResponse,
|
||||||
|
Message,
|
||||||
|
RetrievalType,
|
||||||
|
StreamingError,
|
||||||
|
} from "./interfaces";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { FeedbackType } from "./types";
|
||||||
|
import {
|
||||||
|
buildChatUrl,
|
||||||
|
createChatSession,
|
||||||
|
getCitedDocumentsFromMessage,
|
||||||
|
getHumanAndAIMessageFromMessageNumber,
|
||||||
|
getLastSuccessfulMessageId,
|
||||||
|
handleAutoScroll,
|
||||||
|
handleChatFeedback,
|
||||||
|
nameChatSession,
|
||||||
|
personaIncludesRetrieval,
|
||||||
|
processRawChatHistory,
|
||||||
|
sendMessage,
|
||||||
|
} from "./lib";
|
||||||
|
import { ThreeDots } from "react-loader-spinner";
|
||||||
|
import { FeedbackModal } from "./modal/FeedbackModal";
|
||||||
|
import { DocumentSidebar } from "./documentSidebar/DocumentSidebar";
|
||||||
|
import { ChatPersonaSelector } from "./ChatPersonaSelector";
|
||||||
|
import { useFilters } from "@/lib/hooks";
|
||||||
|
import { DocumentSet, Tag, ValidSources } from "@/lib/types";
|
||||||
|
import { ChatFilters } from "./modifiers/ChatFilters";
|
||||||
|
import { buildFilters } from "@/lib/search/utils";
|
||||||
|
import { SelectedDocuments } from "./modifiers/SelectedDocuments";
|
||||||
|
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||||
|
import { ResizableSection } from "@/components/resizable/ResizableSection";
|
||||||
|
import { DanswerInitializingLoader } from "@/components/DanswerInitializingLoader";
|
||||||
|
import { ChatIntro } from "./ChatIntro";
|
||||||
|
import { HEADER_PADDING } from "@/lib/constants";
|
||||||
|
import { computeAvailableFilters } from "@/lib/filters";
|
||||||
|
import { useDocumentSelection } from "./useDocumentSelection";
|
||||||
|
import { StarterMessage } from "./StarterMessage";
|
||||||
|
import { ShareChatSessionModal } from "./modal/ShareChatSessionModal";
|
||||||
|
import { SEARCH_PARAM_NAMES, shouldSubmitOnLoad } from "./searchParams";
|
||||||
|
import { Persona } from "../admin/assistants/interfaces";
|
||||||
|
|
||||||
|
const MAX_INPUT_HEIGHT = 200;
|
||||||
|
|
||||||
|
export const Chat = ({
|
||||||
|
existingChatSessionId,
|
||||||
|
existingChatSessionPersonaId,
|
||||||
|
availableSources,
|
||||||
|
availableDocumentSets,
|
||||||
|
availablePersonas,
|
||||||
|
availableTags,
|
||||||
|
defaultSelectedPersonaId,
|
||||||
|
documentSidebarInitialWidth,
|
||||||
|
shouldhideBeforeScroll,
|
||||||
|
}: {
|
||||||
|
existingChatSessionId: number | null;
|
||||||
|
existingChatSessionPersonaId: number | undefined;
|
||||||
|
availableSources: ValidSources[];
|
||||||
|
availableDocumentSets: DocumentSet[];
|
||||||
|
availablePersonas: Persona[];
|
||||||
|
availableTags: Tag[];
|
||||||
|
defaultSelectedPersonaId?: number; // what persona to default to
|
||||||
|
documentSidebarInitialWidth?: number;
|
||||||
|
shouldhideBeforeScroll?: boolean;
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
// used to track whether or not the initial "submit on load" has been performed
|
||||||
|
// this only applies if `?submit-on-load=true` or `?submit-on-load=1` is in the URL
|
||||||
|
// NOTE: this is required due to React strict mode, where all `useEffect` hooks
|
||||||
|
// are run twice on initial load during development
|
||||||
|
const submitOnLoadPerformed = useRef<boolean>(false);
|
||||||
|
|
||||||
|
const { popup, setPopup } = usePopup();
|
||||||
|
|
||||||
|
// fetch messages for the chat session
|
||||||
|
const [isFetchingChatMessages, setIsFetchingChatMessages] = useState(
|
||||||
|
existingChatSessionId !== null
|
||||||
|
);
|
||||||
|
|
||||||
|
// needed so closures (e.g. onSubmit) can access the current value
|
||||||
|
const urlChatSessionId = useRef<number | null>();
|
||||||
|
// this is triggered every time the user switches which chat
|
||||||
|
// session they are using
|
||||||
|
useEffect(() => {
|
||||||
|
urlChatSessionId.current = existingChatSessionId;
|
||||||
|
|
||||||
|
textareaRef.current?.focus();
|
||||||
|
|
||||||
|
// only clear things if we're going from one chat session to another
|
||||||
|
if (chatSessionId !== null && existingChatSessionId !== chatSessionId) {
|
||||||
|
// de-select documents
|
||||||
|
clearSelectedDocuments();
|
||||||
|
// reset all filters
|
||||||
|
filterManager.setSelectedDocumentSets([]);
|
||||||
|
filterManager.setSelectedSources([]);
|
||||||
|
filterManager.setSelectedTags([]);
|
||||||
|
filterManager.setTimeRange(null);
|
||||||
|
if (isStreaming) {
|
||||||
|
setIsCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setChatSessionId(existingChatSessionId);
|
||||||
|
|
||||||
|
async function initialSessionFetch() {
|
||||||
|
if (existingChatSessionId === null) {
|
||||||
|
setIsFetchingChatMessages(false);
|
||||||
|
if (defaultSelectedPersonaId !== undefined) {
|
||||||
|
setSelectedPersona(
|
||||||
|
availablePersonas.find(
|
||||||
|
(persona) => persona.id === defaultSelectedPersonaId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedPersona(undefined);
|
||||||
|
}
|
||||||
|
setMessageHistory([]);
|
||||||
|
setChatSessionSharedStatus(ChatSessionSharedStatus.Private);
|
||||||
|
|
||||||
|
// if we're supposed to submit on initial load, then do that here
|
||||||
|
if (
|
||||||
|
shouldSubmitOnLoad(searchParams) &&
|
||||||
|
!submitOnLoadPerformed.current
|
||||||
|
) {
|
||||||
|
submitOnLoadPerformed.current = true;
|
||||||
|
await onSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFetchingChatMessages(true);
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/chat/get-chat-session/${existingChatSessionId}`
|
||||||
|
);
|
||||||
|
const chatSession = (await response.json()) as BackendChatSession;
|
||||||
|
setSelectedPersona(
|
||||||
|
availablePersonas.find(
|
||||||
|
(persona) => persona.id === chatSession.persona_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const newMessageHistory = processRawChatHistory(chatSession.messages);
|
||||||
|
setMessageHistory(newMessageHistory);
|
||||||
|
|
||||||
|
const latestMessageId =
|
||||||
|
newMessageHistory[newMessageHistory.length - 1]?.messageId;
|
||||||
|
setSelectedMessageForDocDisplay(
|
||||||
|
latestMessageId !== undefined ? latestMessageId : null
|
||||||
|
);
|
||||||
|
|
||||||
|
setChatSessionSharedStatus(chatSession.shared_status);
|
||||||
|
|
||||||
|
setIsFetchingChatMessages(false);
|
||||||
|
|
||||||
|
// if this is a seeded chat, then kick off the AI message generation
|
||||||
|
if (newMessageHistory.length === 1 && !submitOnLoadPerformed.current) {
|
||||||
|
submitOnLoadPerformed.current = true;
|
||||||
|
const seededMessage = newMessageHistory[0].message;
|
||||||
|
await onSubmit({
|
||||||
|
isSeededChat: true,
|
||||||
|
messageOverride: seededMessage,
|
||||||
|
});
|
||||||
|
// force re-name if the chat session doesn't have one
|
||||||
|
if (!chatSession.description) {
|
||||||
|
await nameChatSession(existingChatSessionId, seededMessage);
|
||||||
|
router.refresh(); // need to refresh to update name on sidebar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initialSessionFetch();
|
||||||
|
}, [existingChatSessionId]);
|
||||||
|
|
||||||
|
const [chatSessionId, setChatSessionId] = useState<number | null>(
|
||||||
|
existingChatSessionId
|
||||||
|
);
|
||||||
|
const [message, setMessage] = useState(
|
||||||
|
searchParams.get(SEARCH_PARAM_NAMES.USER_MESSAGE) || ""
|
||||||
|
);
|
||||||
|
const [messageHistory, setMessageHistory] = useState<Message[]>([]);
|
||||||
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
|
|
||||||
|
// for document display
|
||||||
|
// NOTE: -1 is a special designation that means the latest AI message
|
||||||
|
const [selectedMessageForDocDisplay, setSelectedMessageForDocDisplay] =
|
||||||
|
useState<number | null>(null);
|
||||||
|
const { aiMessage } = selectedMessageForDocDisplay
|
||||||
|
? getHumanAndAIMessageFromMessageNumber(
|
||||||
|
messageHistory,
|
||||||
|
selectedMessageForDocDisplay
|
||||||
|
)
|
||||||
|
: { aiMessage: null };
|
||||||
|
|
||||||
|
const [selectedPersona, setSelectedPersona] = useState<Persona | undefined>(
|
||||||
|
existingChatSessionPersonaId !== undefined
|
||||||
|
? availablePersonas.find(
|
||||||
|
(persona) => persona.id === existingChatSessionPersonaId
|
||||||
|
)
|
||||||
|
: defaultSelectedPersonaId !== undefined
|
||||||
|
? availablePersonas.find(
|
||||||
|
(persona) => persona.id === defaultSelectedPersonaId
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
const livePersona = selectedPersona || availablePersonas[0];
|
||||||
|
|
||||||
|
const [chatSessionSharedStatus, setChatSessionSharedStatus] =
|
||||||
|
useState<ChatSessionSharedStatus>(ChatSessionSharedStatus.Private);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messageHistory.length === 0 && chatSessionId === null) {
|
||||||
|
setSelectedPersona(
|
||||||
|
availablePersonas.find(
|
||||||
|
(persona) => persona.id === defaultSelectedPersonaId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [defaultSelectedPersonaId]);
|
||||||
|
|
||||||
|
const [
|
||||||
|
selectedDocuments,
|
||||||
|
toggleDocumentSelection,
|
||||||
|
clearSelectedDocuments,
|
||||||
|
selectedDocumentTokens,
|
||||||
|
] = useDocumentSelection();
|
||||||
|
// just choose a conservative default, this will be updated in the
|
||||||
|
// background on initial load / on persona change
|
||||||
|
const [maxTokens, setMaxTokens] = useState<number>(4096);
|
||||||
|
// fetch # of allowed document tokens for the selected Persona
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchMaxTokens() {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/chat/max-selected-document-tokens?persona_id=${livePersona.id}`
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
const maxTokens = (await response.json()).max_tokens as number;
|
||||||
|
setMaxTokens(maxTokens);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchMaxTokens();
|
||||||
|
}, [livePersona]);
|
||||||
|
|
||||||
|
const filterManager = useFilters();
|
||||||
|
const [finalAvailableSources, finalAvailableDocumentSets] =
|
||||||
|
computeAvailableFilters({
|
||||||
|
selectedPersona,
|
||||||
|
availableSources,
|
||||||
|
availableDocumentSets,
|
||||||
|
});
|
||||||
|
|
||||||
|
// state for cancelling streaming
|
||||||
|
const [isCancelled, setIsCancelled] = useState(false);
|
||||||
|
const isCancelledRef = useRef(isCancelled);
|
||||||
|
useEffect(() => {
|
||||||
|
isCancelledRef.current = isCancelled;
|
||||||
|
}, [isCancelled]);
|
||||||
|
|
||||||
|
const [currentFeedback, setCurrentFeedback] = useState<
|
||||||
|
[FeedbackType, number] | null
|
||||||
|
>(null);
|
||||||
|
const [sharingModalVisible, setSharingModalVisible] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
|
// auto scroll as message comes out
|
||||||
|
const scrollableDivRef = useRef<HTMLDivElement>(null);
|
||||||
|
const endDivRef = useRef<HTMLDivElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (isStreaming || !message) {
|
||||||
|
handleAutoScroll(endDivRef, scrollableDivRef);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// scroll to bottom initially
|
||||||
|
const [hasPerformedInitialScroll, setHasPerformedInitialScroll] = useState(
|
||||||
|
shouldhideBeforeScroll !== true
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
endDivRef.current?.scrollIntoView();
|
||||||
|
setHasPerformedInitialScroll(true);
|
||||||
|
}, [isFetchingChatMessages]);
|
||||||
|
|
||||||
|
// handle re-sizing of the text area
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
if (textarea) {
|
||||||
|
textarea.style.height = "0px";
|
||||||
|
textarea.style.height = `${Math.min(
|
||||||
|
textarea.scrollHeight,
|
||||||
|
MAX_INPUT_HEIGHT
|
||||||
|
)}px`;
|
||||||
|
}
|
||||||
|
}, [message]);
|
||||||
|
|
||||||
|
// used for resizing of the document sidebar
|
||||||
|
const masterFlexboxRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [maxDocumentSidebarWidth, setMaxDocumentSidebarWidth] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
|
const adjustDocumentSidebarWidth = () => {
|
||||||
|
if (masterFlexboxRef.current && document.documentElement.clientWidth) {
|
||||||
|
// numbers below are based on the actual width the center section for different
|
||||||
|
// screen sizes. `1700` corresponds to the custom "3xl" tailwind breakpoint
|
||||||
|
// NOTE: some buffer is needed to account for scroll bars
|
||||||
|
if (document.documentElement.clientWidth > 1700) {
|
||||||
|
setMaxDocumentSidebarWidth(masterFlexboxRef.current.clientWidth - 950);
|
||||||
|
} else if (document.documentElement.clientWidth > 1420) {
|
||||||
|
setMaxDocumentSidebarWidth(masterFlexboxRef.current.clientWidth - 760);
|
||||||
|
} else {
|
||||||
|
setMaxDocumentSidebarWidth(masterFlexboxRef.current.clientWidth - 660);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
adjustDocumentSidebarWidth(); // Adjust the width on initial render
|
||||||
|
window.addEventListener("resize", adjustDocumentSidebarWidth); // Add resize event listener
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", adjustDocumentSidebarWidth); // Cleanup the event listener
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!documentSidebarInitialWidth && maxDocumentSidebarWidth) {
|
||||||
|
documentSidebarInitialWidth = Math.min(700, maxDocumentSidebarWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async ({
|
||||||
|
messageIdToResend,
|
||||||
|
messageOverride,
|
||||||
|
queryOverride,
|
||||||
|
forceSearch,
|
||||||
|
isSeededChat,
|
||||||
|
}: {
|
||||||
|
messageIdToResend?: number;
|
||||||
|
messageOverride?: string;
|
||||||
|
queryOverride?: string;
|
||||||
|
forceSearch?: boolean;
|
||||||
|
isSeededChat?: boolean;
|
||||||
|
} = {}) => {
|
||||||
|
let currChatSessionId: number;
|
||||||
|
let isNewSession = chatSessionId === null;
|
||||||
|
const searchParamBasedChatSessionName =
|
||||||
|
searchParams.get(SEARCH_PARAM_NAMES.TITLE) || null;
|
||||||
|
|
||||||
|
if (isNewSession) {
|
||||||
|
currChatSessionId = await createChatSession(
|
||||||
|
livePersona?.id || 0,
|
||||||
|
searchParamBasedChatSessionName
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
currChatSessionId = chatSessionId as number;
|
||||||
|
}
|
||||||
|
setChatSessionId(currChatSessionId);
|
||||||
|
|
||||||
|
const messageToResend = messageHistory.find(
|
||||||
|
(message) => message.messageId === messageIdToResend
|
||||||
|
);
|
||||||
|
const messageToResendIndex = messageToResend
|
||||||
|
? messageHistory.indexOf(messageToResend)
|
||||||
|
: null;
|
||||||
|
if (!messageToResend && messageIdToResend !== undefined) {
|
||||||
|
setPopup({
|
||||||
|
message:
|
||||||
|
"Failed to re-send message - please refresh the page and try again.",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currMessage = messageToResend ? messageToResend.message : message;
|
||||||
|
if (messageOverride) {
|
||||||
|
currMessage = messageOverride;
|
||||||
|
}
|
||||||
|
const currMessageHistory =
|
||||||
|
messageToResendIndex !== null
|
||||||
|
? messageHistory.slice(0, messageToResendIndex)
|
||||||
|
: messageHistory;
|
||||||
|
setMessageHistory([
|
||||||
|
...currMessageHistory,
|
||||||
|
{
|
||||||
|
messageId: 0,
|
||||||
|
message: currMessage,
|
||||||
|
type: "user",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setMessage("");
|
||||||
|
|
||||||
|
setIsStreaming(true);
|
||||||
|
let answer = "";
|
||||||
|
let query: string | null = null;
|
||||||
|
let retrievalType: RetrievalType =
|
||||||
|
selectedDocuments.length > 0
|
||||||
|
? RetrievalType.SelectedDocs
|
||||||
|
: RetrievalType.None;
|
||||||
|
let documents: DanswerDocument[] = selectedDocuments;
|
||||||
|
let error: string | null = null;
|
||||||
|
let finalMessage: BackendMessage | null = null;
|
||||||
|
try {
|
||||||
|
const lastSuccessfulMessageId =
|
||||||
|
getLastSuccessfulMessageId(currMessageHistory);
|
||||||
|
for await (const packetBunch of sendMessage({
|
||||||
|
message: currMessage,
|
||||||
|
parentMessageId: lastSuccessfulMessageId,
|
||||||
|
chatSessionId: currChatSessionId,
|
||||||
|
promptId: livePersona?.prompts[0]?.id || 0,
|
||||||
|
filters: buildFilters(
|
||||||
|
filterManager.selectedSources,
|
||||||
|
filterManager.selectedDocumentSets,
|
||||||
|
filterManager.timeRange,
|
||||||
|
filterManager.selectedTags
|
||||||
|
),
|
||||||
|
selectedDocumentIds: selectedDocuments
|
||||||
|
.filter(
|
||||||
|
(document) =>
|
||||||
|
document.db_doc_id !== undefined && document.db_doc_id !== null
|
||||||
|
)
|
||||||
|
.map((document) => document.db_doc_id as number),
|
||||||
|
queryOverride,
|
||||||
|
forceSearch,
|
||||||
|
modelVersion:
|
||||||
|
searchParams.get(SEARCH_PARAM_NAMES.MODEL_VERSION) || undefined,
|
||||||
|
temperature:
|
||||||
|
parseFloat(searchParams.get(SEARCH_PARAM_NAMES.TEMPERATURE) || "") ||
|
||||||
|
undefined,
|
||||||
|
systemPromptOverride:
|
||||||
|
searchParams.get(SEARCH_PARAM_NAMES.SYSTEM_PROMPT) || undefined,
|
||||||
|
useExistingUserMessage: isSeededChat,
|
||||||
|
})) {
|
||||||
|
for (const packet of packetBunch) {
|
||||||
|
if (Object.hasOwn(packet, "answer_piece")) {
|
||||||
|
answer += (packet as AnswerPiecePacket).answer_piece;
|
||||||
|
} else if (Object.hasOwn(packet, "top_documents")) {
|
||||||
|
documents = (packet as DocumentsResponse).top_documents;
|
||||||
|
query = (packet as DocumentsResponse).rephrased_query;
|
||||||
|
retrievalType = RetrievalType.Search;
|
||||||
|
if (documents && documents.length > 0) {
|
||||||
|
// point to the latest message (we don't know the messageId yet, which is why
|
||||||
|
// we have to use -1)
|
||||||
|
setSelectedMessageForDocDisplay(-1);
|
||||||
|
}
|
||||||
|
} else if (Object.hasOwn(packet, "error")) {
|
||||||
|
error = (packet as StreamingError).error;
|
||||||
|
} else if (Object.hasOwn(packet, "message_id")) {
|
||||||
|
finalMessage = packet as BackendMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setMessageHistory([
|
||||||
|
...currMessageHistory,
|
||||||
|
{
|
||||||
|
messageId: finalMessage?.parent_message || null,
|
||||||
|
message: currMessage,
|
||||||
|
type: "user",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
messageId: finalMessage?.message_id || null,
|
||||||
|
message: error || answer,
|
||||||
|
type: error ? "error" : "assistant",
|
||||||
|
retrievalType,
|
||||||
|
query: finalMessage?.rephrased_query || query,
|
||||||
|
documents: finalMessage?.context_docs?.top_documents || documents,
|
||||||
|
citations: finalMessage?.citations || {},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
if (isCancelledRef.current) {
|
||||||
|
setIsCancelled(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
const errorMsg = e.message;
|
||||||
|
setMessageHistory([
|
||||||
|
...currMessageHistory,
|
||||||
|
{
|
||||||
|
messageId: null,
|
||||||
|
message: currMessage,
|
||||||
|
type: "user",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
messageId: null,
|
||||||
|
message: errorMsg,
|
||||||
|
type: "error",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
setIsStreaming(false);
|
||||||
|
if (isNewSession) {
|
||||||
|
if (finalMessage) {
|
||||||
|
setSelectedMessageForDocDisplay(finalMessage.message_id);
|
||||||
|
}
|
||||||
|
if (!searchParamBasedChatSessionName) {
|
||||||
|
await nameChatSession(currChatSessionId, currMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: don't switch pages if the user has navigated away from the chat
|
||||||
|
if (
|
||||||
|
currChatSessionId === urlChatSessionId.current ||
|
||||||
|
urlChatSessionId.current === null
|
||||||
|
) {
|
||||||
|
router.push(buildChatUrl(searchParams, currChatSessionId, null), {
|
||||||
|
scroll: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
finalMessage?.context_docs &&
|
||||||
|
finalMessage.context_docs.top_documents.length > 0 &&
|
||||||
|
retrievalType === RetrievalType.Search
|
||||||
|
) {
|
||||||
|
setSelectedMessageForDocDisplay(finalMessage.message_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFeedback = async (
|
||||||
|
messageId: number,
|
||||||
|
feedbackType: FeedbackType,
|
||||||
|
feedbackDetails: string
|
||||||
|
) => {
|
||||||
|
if (chatSessionId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await handleChatFeedback(
|
||||||
|
messageId,
|
||||||
|
feedbackType,
|
||||||
|
feedbackDetails
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setPopup({
|
||||||
|
message: "Thanks for your feedback!",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const responseJson = await response.json();
|
||||||
|
const errorMsg = responseJson.detail || responseJson.message;
|
||||||
|
setPopup({
|
||||||
|
message: `Failed to submit feedback - ${errorMsg}`,
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const retrievalDisabled = !personaIncludesRetrieval(livePersona);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full overflow-x-hidden" ref={masterFlexboxRef}>
|
||||||
|
{popup}
|
||||||
|
{currentFeedback && (
|
||||||
|
<FeedbackModal
|
||||||
|
feedbackType={currentFeedback[0]}
|
||||||
|
onClose={() => setCurrentFeedback(null)}
|
||||||
|
onSubmit={(feedbackDetails) => {
|
||||||
|
onFeedback(currentFeedback[1], currentFeedback[0], feedbackDetails);
|
||||||
|
setCurrentFeedback(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sharingModalVisible && chatSessionId !== null && (
|
||||||
|
<ShareChatSessionModal
|
||||||
|
chatSessionId={chatSessionId}
|
||||||
|
existingSharedStatus={chatSessionSharedStatus}
|
||||||
|
onClose={() => setSharingModalVisible(false)}
|
||||||
|
onShare={(shared) =>
|
||||||
|
setChatSessionSharedStatus(
|
||||||
|
shared
|
||||||
|
? ChatSessionSharedStatus.Public
|
||||||
|
: ChatSessionSharedStatus.Private
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{documentSidebarInitialWidth !== undefined ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`w-full sm:relative h-screen ${
|
||||||
|
retrievalDisabled ? "pb-[111px]" : "pb-[140px]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-full h-full ${HEADER_PADDING} flex flex-col overflow-y-auto overflow-x-hidden relative`}
|
||||||
|
ref={scrollableDivRef}
|
||||||
|
>
|
||||||
|
{livePersona && (
|
||||||
|
<div className="sticky top-0 left-80 z-10 w-full bg-background/90 flex">
|
||||||
|
<div className="ml-2 p-1 rounded mt-2 w-fit">
|
||||||
|
<ChatPersonaSelector
|
||||||
|
personas={availablePersonas}
|
||||||
|
selectedPersonaId={livePersona.id}
|
||||||
|
onPersonaChange={(persona) => {
|
||||||
|
if (persona) {
|
||||||
|
setSelectedPersona(persona);
|
||||||
|
textareaRef.current?.focus();
|
||||||
|
router.push(
|
||||||
|
buildChatUrl(searchParams, null, persona.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{chatSessionId !== null && (
|
||||||
|
<div
|
||||||
|
onClick={() => setSharingModalVisible(true)}
|
||||||
|
className="ml-auto mr-6 my-auto border-border border p-2 rounded cursor-pointer hover:bg-hover-light"
|
||||||
|
>
|
||||||
|
<FiShare2 />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{messageHistory.length === 0 &&
|
||||||
|
!isFetchingChatMessages &&
|
||||||
|
!isStreaming && (
|
||||||
|
<ChatIntro
|
||||||
|
availableSources={finalAvailableSources}
|
||||||
|
availablePersonas={availablePersonas}
|
||||||
|
selectedPersona={selectedPersona}
|
||||||
|
handlePersonaSelect={(persona) => {
|
||||||
|
setSelectedPersona(persona);
|
||||||
|
textareaRef.current?.focus();
|
||||||
|
router.push(buildChatUrl(searchParams, null, persona.id));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"mt-4 pt-12 sm:pt-0 mx-8" +
|
||||||
|
(hasPerformedInitialScroll ? "" : " invisible")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{messageHistory.map((message, i) => {
|
||||||
|
if (message.type === "user") {
|
||||||
|
return (
|
||||||
|
<div key={i}>
|
||||||
|
<HumanMessage content={message.message} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (message.type === "assistant") {
|
||||||
|
const isShowingRetrieved =
|
||||||
|
(selectedMessageForDocDisplay !== null &&
|
||||||
|
selectedMessageForDocDisplay === message.messageId) ||
|
||||||
|
(selectedMessageForDocDisplay === -1 &&
|
||||||
|
i === messageHistory.length - 1);
|
||||||
|
const previousMessage =
|
||||||
|
i !== 0 ? messageHistory[i - 1] : null;
|
||||||
|
return (
|
||||||
|
<div key={i}>
|
||||||
|
<AIMessage
|
||||||
|
messageId={message.messageId}
|
||||||
|
content={message.message}
|
||||||
|
query={messageHistory[i]?.query || undefined}
|
||||||
|
personaName={livePersona.name}
|
||||||
|
citedDocuments={getCitedDocumentsFromMessage(message)}
|
||||||
|
isComplete={
|
||||||
|
i !== messageHistory.length - 1 || !isStreaming
|
||||||
|
}
|
||||||
|
hasDocs={
|
||||||
|
(message.documents &&
|
||||||
|
message.documents.length > 0) === true
|
||||||
|
}
|
||||||
|
handleFeedback={
|
||||||
|
i === messageHistory.length - 1 && isStreaming
|
||||||
|
? undefined
|
||||||
|
: (feedbackType) =>
|
||||||
|
setCurrentFeedback([
|
||||||
|
feedbackType,
|
||||||
|
message.messageId as number,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
handleSearchQueryEdit={
|
||||||
|
i === messageHistory.length - 1 && !isStreaming
|
||||||
|
? (newQuery) => {
|
||||||
|
if (!previousMessage) {
|
||||||
|
setPopup({
|
||||||
|
type: "error",
|
||||||
|
message:
|
||||||
|
"Cannot edit query of first message - please refresh the page and try again.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousMessage.messageId === null) {
|
||||||
|
setPopup({
|
||||||
|
type: "error",
|
||||||
|
message:
|
||||||
|
"Cannot edit query of a pending message - please wait a few seconds and try again.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSubmit({
|
||||||
|
messageIdToResend:
|
||||||
|
previousMessage.messageId,
|
||||||
|
queryOverride: newQuery,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
isCurrentlyShowingRetrieved={isShowingRetrieved}
|
||||||
|
handleShowRetrieved={(messageNumber) => {
|
||||||
|
if (isShowingRetrieved) {
|
||||||
|
setSelectedMessageForDocDisplay(null);
|
||||||
|
} else {
|
||||||
|
if (messageNumber !== null) {
|
||||||
|
setSelectedMessageForDocDisplay(messageNumber);
|
||||||
|
} else {
|
||||||
|
setSelectedMessageForDocDisplay(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
handleForceSearch={() => {
|
||||||
|
if (previousMessage && previousMessage.messageId) {
|
||||||
|
onSubmit({
|
||||||
|
messageIdToResend: previousMessage.messageId,
|
||||||
|
forceSearch: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setPopup({
|
||||||
|
type: "error",
|
||||||
|
message:
|
||||||
|
"Failed to force search - please refresh the page and try again.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
retrievalDisabled={retrievalDisabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div key={i}>
|
||||||
|
<AIMessage
|
||||||
|
messageId={message.messageId}
|
||||||
|
personaName={livePersona.name}
|
||||||
|
content={
|
||||||
|
<p className="text-red-700 text-sm my-auto">
|
||||||
|
{message.message}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
|
||||||
|
{isStreaming &&
|
||||||
|
messageHistory.length &&
|
||||||
|
messageHistory[messageHistory.length - 1].type === "user" && (
|
||||||
|
<div key={messageHistory.length}>
|
||||||
|
<AIMessage
|
||||||
|
messageId={null}
|
||||||
|
personaName={livePersona.name}
|
||||||
|
content={
|
||||||
|
<div className="text-sm my-auto">
|
||||||
|
<ThreeDots
|
||||||
|
height="30"
|
||||||
|
width="50"
|
||||||
|
color="#3b82f6"
|
||||||
|
ariaLabel="grid-loading"
|
||||||
|
radius="12.5"
|
||||||
|
wrapperStyle={{}}
|
||||||
|
wrapperClass=""
|
||||||
|
visible={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Some padding at the bottom so the search bar has space at the bottom to not cover the last message*/}
|
||||||
|
<div className={`min-h-[30px] w-full`}></div>
|
||||||
|
|
||||||
|
{livePersona &&
|
||||||
|
livePersona.starter_messages &&
|
||||||
|
livePersona.starter_messages.length > 0 &&
|
||||||
|
selectedPersona &&
|
||||||
|
messageHistory.length === 0 &&
|
||||||
|
!isFetchingChatMessages && (
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
mx-auto
|
||||||
|
px-4
|
||||||
|
w-searchbar-xs
|
||||||
|
2xl:w-searchbar-sm
|
||||||
|
3xl:w-searchbar
|
||||||
|
grid
|
||||||
|
gap-4
|
||||||
|
grid-cols-1
|
||||||
|
grid-rows-1
|
||||||
|
mt-4
|
||||||
|
md:grid-cols-2
|
||||||
|
mb-6`}
|
||||||
|
>
|
||||||
|
{livePersona.starter_messages.map((starterMessage, i) => (
|
||||||
|
<div key={i} className="w-full">
|
||||||
|
<StarterMessage
|
||||||
|
starterMessage={starterMessage}
|
||||||
|
onClick={() =>
|
||||||
|
onSubmit({
|
||||||
|
messageOverride: starterMessage.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={endDivRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-0 z-10 w-full bg-background border-t border-border">
|
||||||
|
<div className="w-full pb-4 pt-2">
|
||||||
|
{!retrievalDisabled && (
|
||||||
|
<div className="flex">
|
||||||
|
<div className="w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar mx-auto px-4 pt-1 flex">
|
||||||
|
{selectedDocuments.length > 0 ? (
|
||||||
|
<SelectedDocuments
|
||||||
|
selectedDocuments={selectedDocuments}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChatFilters
|
||||||
|
{...filterManager}
|
||||||
|
existingSources={finalAvailableSources}
|
||||||
|
availableDocumentSets={finalAvailableDocumentSets}
|
||||||
|
availableTags={availableTags}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-center py-2 max-w-screen-lg mx-auto mb-2">
|
||||||
|
<div className="w-full shrink relative px-4 w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar mx-auto">
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
autoFocus
|
||||||
|
className={`
|
||||||
|
opacity-100
|
||||||
|
w-full
|
||||||
|
shrink
|
||||||
|
border
|
||||||
|
border-border
|
||||||
|
bg-background-search
|
||||||
|
rounded-lg
|
||||||
|
outline-none
|
||||||
|
placeholder-subtle
|
||||||
|
bg-background-emphasis
|
||||||
|
pl-4
|
||||||
|
pr-12
|
||||||
|
py-4
|
||||||
|
overflow-hidden
|
||||||
|
h-14
|
||||||
|
${
|
||||||
|
(textareaRef?.current?.scrollHeight || 0) >
|
||||||
|
MAX_INPUT_HEIGHT
|
||||||
|
? "overflow-y-auto"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
whitespace-normal
|
||||||
|
break-word
|
||||||
|
overscroll-contain
|
||||||
|
resize-none
|
||||||
|
`}
|
||||||
|
style={{ scrollbarWidth: "thin" }}
|
||||||
|
role="textarea"
|
||||||
|
aria-multiline
|
||||||
|
placeholder="Ask me anything..."
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (
|
||||||
|
event.key === "Enter" &&
|
||||||
|
!event.shiftKey &&
|
||||||
|
message &&
|
||||||
|
!isStreaming
|
||||||
|
) {
|
||||||
|
onSubmit();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
suppressContentEditableWarning={true}
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-4 right-10">
|
||||||
|
<div
|
||||||
|
className={"cursor-pointer"}
|
||||||
|
onClick={() => {
|
||||||
|
if (!isStreaming) {
|
||||||
|
if (message) {
|
||||||
|
onSubmit();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsCancelled(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isStreaming ? (
|
||||||
|
<FiStopCircle
|
||||||
|
size={18}
|
||||||
|
className={
|
||||||
|
"text-emphasis w-9 h-9 p-2 rounded-lg hover:bg-hover"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FiSend
|
||||||
|
size={18}
|
||||||
|
className={
|
||||||
|
"text-emphasis w-9 h-9 p-2 rounded-lg " +
|
||||||
|
(message ? "bg-blue-200" : "")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!retrievalDisabled ? (
|
||||||
|
<ResizableSection
|
||||||
|
intialWidth={documentSidebarInitialWidth}
|
||||||
|
minWidth={400}
|
||||||
|
maxWidth={maxDocumentSidebarWidth || undefined}
|
||||||
|
>
|
||||||
|
<DocumentSidebar
|
||||||
|
selectedMessage={aiMessage}
|
||||||
|
selectedDocuments={selectedDocuments}
|
||||||
|
toggleDocumentSelection={toggleDocumentSelection}
|
||||||
|
clearSelectedDocuments={clearSelectedDocuments}
|
||||||
|
selectedDocumentTokens={selectedDocumentTokens}
|
||||||
|
maxTokens={maxTokens}
|
||||||
|
isLoading={isFetchingChatMessages}
|
||||||
|
/>
|
||||||
|
</ResizableSection>
|
||||||
|
) : // Another option is to use a div with the width set to the initial width, so that the
|
||||||
|
// chat section appears in the same place as before
|
||||||
|
// <div style={documentSidebarInitialWidth ? {width: documentSidebarInitialWidth} : {}}></div>
|
||||||
|
null}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="mx-auto h-full flex flex-col">
|
||||||
|
<div className="my-auto">
|
||||||
|
<DanswerInitializingLoader />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@@ -286,6 +286,9 @@ export const AIMessage = ({
|
|||||||
code: (props) => (
|
code: (props) => (
|
||||||
<CodeBlock {...props} content={content as string} />
|
<CodeBlock {...props} content={content as string} />
|
||||||
),
|
),
|
||||||
|
p: ({ node, ...props }) => (
|
||||||
|
<p {...props} className="text-default" />
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
rehypePlugins={[[rehypePrism, { ignoreMissing: true }]]}
|
rehypePlugins={[[rehypePrism, { ignoreMissing: true }]]}
|
||||||
|
@@ -13,7 +13,6 @@ export function ImageUpload({
|
|||||||
}) {
|
}) {
|
||||||
const [tmpImageUrl, setTmpImageUrl] = useState<string>("");
|
const [tmpImageUrl, setTmpImageUrl] = useState<string>("");
|
||||||
|
|
||||||
console.log(selectedFile);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
setTmpImageUrl(URL.createObjectURL(selectedFile));
|
setTmpImageUrl(URL.createObjectURL(selectedFile));
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { EnterpriseSettings, Settings } from "@/app/admin/settings/interfaces";
|
import { EnterpriseSettings } from "@/app/admin/settings/interfaces";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { SettingsContext } from "@/components/settings/SettingsProviderClientSideHelper";
|
import { SettingsContext } from "@/components/settings/SettingsProviderClientSideHelper";
|
||||||
import { Form, Formik } from "formik";
|
import { Form, Formik } from "formik";
|
||||||
@@ -42,6 +42,7 @@ export function WhitelabelingForm() {
|
|||||||
alert(`Failed to update settings. ${errorMsg}`);
|
alert(`Failed to update settings. ${errorMsg}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log(enterpriseSettings);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -49,10 +50,14 @@ 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,
|
||||||
|
color_config: enterpriseSettings?.color_config || null,
|
||||||
}}
|
}}
|
||||||
validationSchema={Yup.object().shape({
|
validationSchema={Yup.object().shape({
|
||||||
application_name: Yup.string(),
|
application_name: Yup.string(),
|
||||||
use_custom_logo: Yup.boolean().required(),
|
use_custom_logo: Yup.boolean().required(),
|
||||||
|
color_config: Yup.object().shape({
|
||||||
|
primary: Yup.string(),
|
||||||
|
}),
|
||||||
})}
|
})}
|
||||||
onSubmit={async (values, formikHelpers) => {
|
onSubmit={async (values, formikHelpers) => {
|
||||||
formikHelpers.setSubmitting(true);
|
formikHelpers.setSubmitting(true);
|
||||||
|
@@ -16,27 +16,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #f9fafb; /* Track background color */
|
background: theme("colors.scrollbar.track"); /* Track background color */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style the scrollbar handle */
|
/* Style the scrollbar handle */
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #e5e7eb; /* Handle color */
|
background: theme("colors.scrollbar.thumb"); /* Handle color */
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle on hover */
|
/* Handle on hover */
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #d1d5db; /* Handle color on hover */
|
background: theme("colors.scrollbar.thumb-hover"); /* Handle color on hover */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-scrollbar::-webkit-scrollbar-thumb {
|
.dark-scrollbar::-webkit-scrollbar-thumb {
|
||||||
background: #c7cdd2; /* Handle color */
|
background: theme("colors.scrollbar.dark.thumb"); /* Handle color */
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-scrollbar::-webkit-scrollbar-thumb:hover {
|
.dark-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
background: #989a9c; /* Handle color on hover */
|
background: theme(
|
||||||
|
"colors.scrollbar.dark.thumb-hover"
|
||||||
|
); /* Handle color on hover */
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
@@ -27,7 +27,7 @@ export const SearchBar = ({ query, setQuery, onSearch }: SearchBarProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div className="flex items-center w-full opacity-100 border-2 border-border rounded-lg px-4 py-2 focus-within:border-accent bg-white">
|
<div className="flex items-center w-full opacity-100 border-2 border-border rounded-lg px-4 py-2 focus-within:border-accent bg-background-search">
|
||||||
<MagnifyingGlass className="text-emphasis" />
|
<MagnifyingGlass className="text-emphasis" />
|
||||||
<textarea
|
<textarea
|
||||||
autoFocus
|
autoFocus
|
||||||
|
4
web/tailwind-themes/custom/.gitignore
vendored
Normal file
4
web/tailwind-themes/custom/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Ignore everything in this directory
|
||||||
|
*
|
||||||
|
# Except this file
|
||||||
|
!.gitignore
|
214
web/tailwind-themes/tailwind.config.js
Normal file
214
web/tailwind-themes/tailwind.config.js
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: "class",
|
||||||
|
content: [
|
||||||
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
|
||||||
|
// Or if using `src` directory:
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
|
||||||
|
// tremor
|
||||||
|
"./node_modules/@tremor/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
transparent: "transparent",
|
||||||
|
current: "currentColor",
|
||||||
|
extend: {
|
||||||
|
screens: {
|
||||||
|
"2xl": "1420px",
|
||||||
|
"3xl": "1700px",
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["var(--font-inter)"],
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
"message-xs": "450px",
|
||||||
|
"message-sm": "550px",
|
||||||
|
"message-default": "740px",
|
||||||
|
"searchbar-xs": "560px",
|
||||||
|
"searchbar-sm": "660px",
|
||||||
|
searchbar: "850px",
|
||||||
|
"document-sidebar": "800px",
|
||||||
|
"document-sidebar-large": "1000px",
|
||||||
|
},
|
||||||
|
maxWidth: {
|
||||||
|
"document-sidebar": "1000px",
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
// background
|
||||||
|
background: "#f9fafb", // gray-50
|
||||||
|
"background-emphasis": "#f6f7f8",
|
||||||
|
"background-strong": "#eaecef",
|
||||||
|
"background-search": "#ffffff",
|
||||||
|
|
||||||
|
// text or icons
|
||||||
|
link: "#3b82f6", // blue-500
|
||||||
|
subtle: "#6b7280", // gray-500
|
||||||
|
default: "#4b5563", // gray-600
|
||||||
|
emphasis: "#374151", // gray-700
|
||||||
|
strong: "#111827", // gray-900
|
||||||
|
inverted: "#ffffff", // white
|
||||||
|
error: "#ef4444", // red-500
|
||||||
|
success: "#059669", // emerald-600
|
||||||
|
alert: "#f59e0b", // amber-600
|
||||||
|
accent: "#6671d0",
|
||||||
|
|
||||||
|
// borders
|
||||||
|
border: "#e5e7eb", // gray-200
|
||||||
|
"border-light": "#f3f4f6", // gray-100
|
||||||
|
"border-strong": "#9ca3af", // gray-400
|
||||||
|
|
||||||
|
// hover
|
||||||
|
"hover-light": "#f3f4f6", // gray-100
|
||||||
|
hover: "#e5e7eb", // gray-200
|
||||||
|
"hover-emphasis": "#d1d5db", // gray-300
|
||||||
|
"accent-hover": "#5964c2",
|
||||||
|
|
||||||
|
// keyword highlighting
|
||||||
|
highlight: {
|
||||||
|
text: "#fef9c3", // yellow-100
|
||||||
|
},
|
||||||
|
|
||||||
|
// bubbles in chat for each "user"
|
||||||
|
user: "#fb7185", // yellow-400
|
||||||
|
ai: "#60a5fa", // blue-400
|
||||||
|
|
||||||
|
// for display documents
|
||||||
|
document: "#ec4899", // pink-500
|
||||||
|
|
||||||
|
// scrollbar
|
||||||
|
scrollbar: {
|
||||||
|
track: "#f9fafb",
|
||||||
|
thumb: "#e5e7eb",
|
||||||
|
"thumb-hover": "#d1d5db",
|
||||||
|
|
||||||
|
dark: {
|
||||||
|
thumb: "#989a9c",
|
||||||
|
"thumb-hover": "#c7cdd2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// light mode
|
||||||
|
tremor: {
|
||||||
|
brand: {
|
||||||
|
faint: "#eff6ff", // blue-50
|
||||||
|
muted: "#bfdbfe", // blue-200
|
||||||
|
subtle: "#60a5fa", // blue-400
|
||||||
|
DEFAULT: "#3b82f6", // blue-500
|
||||||
|
emphasis: "#1d4ed8", // blue-700
|
||||||
|
inverted: "#ffffff", // white
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
muted: "#f9fafb", // gray-50
|
||||||
|
subtle: "#f3f4f6", // gray-100
|
||||||
|
DEFAULT: "#ffffff", // white
|
||||||
|
emphasis: "#374151", // gray-700
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
DEFAULT: "#e5e7eb", // gray-200
|
||||||
|
},
|
||||||
|
ring: {
|
||||||
|
DEFAULT: "#e5e7eb", // gray-200
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
subtle: "#9ca3af", // gray-400
|
||||||
|
DEFAULT: "#4b5563", // gray-600
|
||||||
|
emphasis: "#374151", // gray-700
|
||||||
|
strong: "#111827", // gray-900
|
||||||
|
inverted: "#ffffff", // white
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// dark mode
|
||||||
|
"dark-tremor": {
|
||||||
|
brand: {
|
||||||
|
faint: "#0B1229", // custom
|
||||||
|
muted: "#172554", // blue-950
|
||||||
|
subtle: "#1e40af", // blue-800
|
||||||
|
DEFAULT: "#3b82f6", // blue-500
|
||||||
|
emphasis: "#60a5fa", // blue-400
|
||||||
|
inverted: "#030712", // gray-950
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
muted: "#131A2B", // custom
|
||||||
|
subtle: "#1f2937", // gray-800
|
||||||
|
DEFAULT: "#111827", // gray-900
|
||||||
|
emphasis: "#d1d5db", // gray-300
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
DEFAULT: "#1f2937", // gray-800
|
||||||
|
},
|
||||||
|
ring: {
|
||||||
|
DEFAULT: "#1f2937", // gray-800
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
subtle: "#6b7280", // gray-500
|
||||||
|
DEFAULT: "#d1d5db", // gray-300
|
||||||
|
emphasis: "#f3f4f6", // gray-100
|
||||||
|
strong: "#f9fafb", // gray-50
|
||||||
|
inverted: "#000000", // black
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
// light
|
||||||
|
"tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
|
||||||
|
"tremor-card":
|
||||||
|
"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
|
||||||
|
"tremor-dropdown":
|
||||||
|
"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
|
||||||
|
// dark
|
||||||
|
"dark-tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
|
||||||
|
"dark-tremor-card":
|
||||||
|
"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
|
||||||
|
"dark-tremor-dropdown":
|
||||||
|
"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
"tremor-small": "0.375rem",
|
||||||
|
"tremor-default": "0.5rem",
|
||||||
|
"tremor-full": "9999px",
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
"tremor-label": ["0.75rem"],
|
||||||
|
"tremor-default": ["0.875rem", { lineHeight: "1.25rem" }],
|
||||||
|
"tremor-title": ["1.125rem", { lineHeight: "1.75rem" }],
|
||||||
|
"tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
safelist: [
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||||
|
variants: ["hover", "ui-selected"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||||
|
variants: ["hover", "ui-selected"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||||
|
variants: ["hover", "ui-selected"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
require("@tailwindcss/typography"),
|
||||||
|
require("@headlessui/tailwindcss"),
|
||||||
|
],
|
||||||
|
};
|
@@ -1,217 +1,12 @@
|
|||||||
|
var merge = require("lodash/merge");
|
||||||
|
|
||||||
|
const baseThemes = require("./tailwind-themes/tailwind.config.js");
|
||||||
|
const customThemes =
|
||||||
|
process.env.NEXT_PUBLIC_EE_ENABLED && process.env.NEXT_PUBLIC_THEME
|
||||||
|
? require(
|
||||||
|
`./tailwind-themes/custom/${process.env.NEXT_PUBLIC_THEME}/tailwind.config.js`
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = customThemes ? merge(baseThemes, customThemes) : baseThemes;
|
||||||
darkMode: "class",
|
|
||||||
content: [
|
|
||||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
||||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
|
||||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
||||||
|
|
||||||
// Or if using `src` directory:
|
|
||||||
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
|
||||||
|
|
||||||
// tremor
|
|
||||||
"./node_modules/@tremor/**/*.{js,ts,jsx,tsx}",
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
transparent: "transparent",
|
|
||||||
current: "currentColor",
|
|
||||||
extend: {
|
|
||||||
screens: {
|
|
||||||
"2xl": "1420px",
|
|
||||||
"3xl": "1700px",
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
sans: ["var(--font-inter)"],
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
"message-xs": "450px",
|
|
||||||
"message-sm": "550px",
|
|
||||||
"message-default": "740px",
|
|
||||||
"searchbar-xs": "560px",
|
|
||||||
"searchbar-sm": "660px",
|
|
||||||
searchbar: "850px",
|
|
||||||
"document-sidebar": "800px",
|
|
||||||
"document-sidebar-large": "1000px",
|
|
||||||
},
|
|
||||||
maxWidth: {
|
|
||||||
"document-sidebar": "1000px",
|
|
||||||
},
|
|
||||||
colors: {
|
|
||||||
// background
|
|
||||||
background: "#f9fafb", // gray-50
|
|
||||||
"background-emphasis": "#f6f7f8",
|
|
||||||
"background-strong": "#eaecef",
|
|
||||||
"background-search": "#ffffff",
|
|
||||||
"background-custom-header": "#f3f4f6",
|
|
||||||
"background-inverted": "#000000",
|
|
||||||
|
|
||||||
// text or icons
|
|
||||||
link: "#3b82f6", // blue-500
|
|
||||||
"link-hover": "#1d4ed8", // blue-700
|
|
||||||
subtle: "#6b7280", // gray-500
|
|
||||||
default: "#4b5563", // gray-600
|
|
||||||
emphasis: "#374151", // gray-700
|
|
||||||
strong: "#111827", // gray-900
|
|
||||||
inverted: "#ffffff", // white
|
|
||||||
error: "#ef4444", // red-500
|
|
||||||
success: "#059669", // emerald-600
|
|
||||||
alert: "#f59e0b", // amber-600
|
|
||||||
accent: "#6671d0",
|
|
||||||
|
|
||||||
// borders
|
|
||||||
border: "#e5e7eb", // gray-200
|
|
||||||
"border-light": "#f3f4f6", // gray-100
|
|
||||||
"border-strong": "#9ca3af", // gray-400
|
|
||||||
|
|
||||||
// hover
|
|
||||||
"hover-light": "#f3f4f6", // gray-100
|
|
||||||
hover: "#e5e7eb", // gray-200
|
|
||||||
"hover-emphasis": "#d1d5db", // gray-300
|
|
||||||
"accent-hover": "#5964c2",
|
|
||||||
|
|
||||||
// keyword highlighting
|
|
||||||
highlight: {
|
|
||||||
text: "#fef9c3", // yellow-100
|
|
||||||
},
|
|
||||||
|
|
||||||
// scrollbar
|
|
||||||
scrollbar: {
|
|
||||||
track: "#f9fafb",
|
|
||||||
thumb: "#e5e7eb",
|
|
||||||
"thumb-hover": "#d1d5db",
|
|
||||||
|
|
||||||
dark: {
|
|
||||||
thumb: "#989a9c",
|
|
||||||
"thumb-hover": "#c7cdd2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// bubbles in chat for each "user"
|
|
||||||
user: "#fb7185", // yellow-400
|
|
||||||
ai: "#60a5fa", // blue-400
|
|
||||||
|
|
||||||
// for display documents
|
|
||||||
document: "#ec4899", // pink-500
|
|
||||||
|
|
||||||
// light mode
|
|
||||||
tremor: {
|
|
||||||
brand: {
|
|
||||||
faint: "#eff6ff", // blue-50
|
|
||||||
muted: "#bfdbfe", // blue-200
|
|
||||||
subtle: "#60a5fa", // blue-400
|
|
||||||
DEFAULT: "#3b82f6", // blue-500
|
|
||||||
emphasis: "#1d4ed8", // blue-700
|
|
||||||
inverted: "#ffffff", // white
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
muted: "#f9fafb", // gray-50
|
|
||||||
subtle: "#f3f4f6", // gray-100
|
|
||||||
DEFAULT: "#ffffff", // white
|
|
||||||
emphasis: "#374151", // gray-700
|
|
||||||
},
|
|
||||||
border: {
|
|
||||||
DEFAULT: "#e5e7eb", // gray-200
|
|
||||||
},
|
|
||||||
ring: {
|
|
||||||
DEFAULT: "#e5e7eb", // gray-200
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
subtle: "#9ca3af", // gray-400
|
|
||||||
DEFAULT: "#4b5563", // gray-600
|
|
||||||
emphasis: "#374151", // gray-700
|
|
||||||
strong: "#111827", // gray-900
|
|
||||||
inverted: "#ffffff", // white
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// dark mode
|
|
||||||
"dark-tremor": {
|
|
||||||
brand: {
|
|
||||||
faint: "#0B1229", // custom
|
|
||||||
muted: "#172554", // blue-950
|
|
||||||
subtle: "#1e40af", // blue-800
|
|
||||||
DEFAULT: "#3b82f6", // blue-500
|
|
||||||
emphasis: "#60a5fa", // blue-400
|
|
||||||
inverted: "#030712", // gray-950
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
muted: "#131A2B", // custom
|
|
||||||
subtle: "#1f2937", // gray-800
|
|
||||||
DEFAULT: "#111827", // gray-900
|
|
||||||
emphasis: "#d1d5db", // gray-300
|
|
||||||
},
|
|
||||||
border: {
|
|
||||||
DEFAULT: "#1f2937", // gray-800
|
|
||||||
},
|
|
||||||
ring: {
|
|
||||||
DEFAULT: "#1f2937", // gray-800
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
subtle: "#6b7280", // gray-500
|
|
||||||
DEFAULT: "#d1d5db", // gray-300
|
|
||||||
emphasis: "#f3f4f6", // gray-100
|
|
||||||
strong: "#f9fafb", // gray-50
|
|
||||||
inverted: "#000000", // black
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
boxShadow: {
|
|
||||||
// light
|
|
||||||
"tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
|
|
||||||
"tremor-card":
|
|
||||||
"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
|
|
||||||
"tremor-dropdown":
|
|
||||||
"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
|
|
||||||
// dark
|
|
||||||
"dark-tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
|
|
||||||
"dark-tremor-card":
|
|
||||||
"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
|
|
||||||
"dark-tremor-dropdown":
|
|
||||||
"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
|
|
||||||
},
|
|
||||||
borderRadius: {
|
|
||||||
"tremor-small": "0.375rem",
|
|
||||||
"tremor-default": "0.5rem",
|
|
||||||
"tremor-full": "9999px",
|
|
||||||
},
|
|
||||||
fontSize: {
|
|
||||||
"tremor-label": ["0.75rem"],
|
|
||||||
"tremor-default": ["0.875rem", { lineHeight: "1.25rem" }],
|
|
||||||
"tremor-title": ["1.125rem", { lineHeight: "1.75rem" }],
|
|
||||||
"tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
safelist: [
|
|
||||||
{
|
|
||||||
pattern:
|
|
||||||
/^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
|
||||||
variants: ["hover", "ui-selected"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern:
|
|
||||||
/^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
|
||||||
variants: ["hover", "ui-selected"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern:
|
|
||||||
/^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
|
||||||
variants: ["hover", "ui-selected"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern:
|
|
||||||
/^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern:
|
|
||||||
/^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern:
|
|
||||||
/^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
require("@tailwindcss/typography"),
|
|
||||||
require("@headlessui/tailwindcss"),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
Reference in New Issue
Block a user