mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-04-09 12:30:49 +02:00
Add custom-styling ability via themes
This commit is contained in:
parent
153007c57c
commit
ee2a5bbf49
@ -208,6 +208,7 @@ services:
|
||||
- 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_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
||||
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
|
||||
- NEXT_PUBLIC_EE_ENABLED=${NEXT_PUBLIC_EE_ENABLED:-true}
|
||||
depends_on:
|
||||
- api_server
|
||||
@ -215,6 +216,7 @@ services:
|
||||
environment:
|
||||
- INTERNAL_URL=http://api_server:8080
|
||||
- WEB_DOMAIN=${WEB_DOMAIN:-}
|
||||
- THEME_IS_DARK=${THEME_IS_DARK:-}
|
||||
|
||||
|
||||
inference_model_server:
|
||||
@ -271,7 +273,6 @@ services:
|
||||
max-size: "50m"
|
||||
max-file: "6"
|
||||
|
||||
|
||||
relational_db:
|
||||
image: postgres:15.2-alpine
|
||||
restart: always
|
||||
|
@ -70,6 +70,7 @@ services:
|
||||
- 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_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
||||
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
|
||||
- NEXT_PUBLIC_EE_ENABLED=${NEXT_PUBLIC_EE_ENABLED:-true}
|
||||
depends_on:
|
||||
- api_server
|
||||
|
@ -70,6 +70,7 @@ services:
|
||||
- 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_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
||||
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
|
||||
- NEXT_PUBLIC_EE_ENABLED=${NEXT_PUBLIC_EE_ENABLED:-true}
|
||||
depends_on:
|
||||
- api_server
|
||||
|
@ -53,6 +53,9 @@ ENV NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PRED
|
||||
|
||||
ARG 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
|
||||
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
|
||||
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
|
||||
ENV NEXT_PUBLIC_EE_ENABLED=${NEXT_PUBLIC_EE_ENABLED}
|
||||
|
||||
|
@ -160,7 +160,7 @@ export function Explorer({
|
||||
<div>
|
||||
{popup}
|
||||
<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 />
|
||||
<textarea
|
||||
autoFocus
|
||||
|
@ -4,6 +4,10 @@ export interface Settings {
|
||||
default_page: "search" | "chat";
|
||||
}
|
||||
|
||||
export interface ColorConfig {
|
||||
primary: string | null;
|
||||
}
|
||||
|
||||
export interface EnterpriseSettings {
|
||||
application_name: string | null;
|
||||
use_custom_logo: boolean;
|
||||
@ -12,6 +16,7 @@ export interface EnterpriseSettings {
|
||||
custom_header_content: string | null;
|
||||
custom_popup_header: string | null;
|
||||
custom_popup_content: string | null;
|
||||
color_config: ColorConfig | null;
|
||||
}
|
||||
|
||||
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) => (
|
||||
<CodeBlock {...props} content={content as string} />
|
||||
),
|
||||
p: ({ node, ...props }) => (
|
||||
<p {...props} className="text-default" />
|
||||
),
|
||||
}}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[[rehypePrism, { ignoreMissing: true }]]}
|
||||
|
@ -13,7 +13,6 @@ export function ImageUpload({
|
||||
}) {
|
||||
const [tmpImageUrl, setTmpImageUrl] = useState<string>("");
|
||||
|
||||
console.log(selectedFile);
|
||||
useEffect(() => {
|
||||
if (selectedFile) {
|
||||
setTmpImageUrl(URL.createObjectURL(selectedFile));
|
||||
|
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
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 { SettingsContext } from "@/components/settings/SettingsProviderClientSideHelper";
|
||||
import { Form, Formik } from "formik";
|
||||
@ -42,6 +42,7 @@ export function WhitelabelingForm() {
|
||||
alert(`Failed to update settings. ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
console.log(enterpriseSettings);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -49,10 +50,14 @@ export function WhitelabelingForm() {
|
||||
initialValues={{
|
||||
application_name: enterpriseSettings?.application_name || null,
|
||||
use_custom_logo: enterpriseSettings?.use_custom_logo || false,
|
||||
color_config: enterpriseSettings?.color_config || null,
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
application_name: Yup.string(),
|
||||
use_custom_logo: Yup.boolean().required(),
|
||||
color_config: Yup.object().shape({
|
||||
primary: Yup.string(),
|
||||
}),
|
||||
})}
|
||||
onSubmit={async (values, formikHelpers) => {
|
||||
formikHelpers.setSubmitting(true);
|
||||
|
@ -16,27 +16,29 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f9fafb; /* Track background color */
|
||||
background: theme("colors.scrollbar.track"); /* Track background color */
|
||||
}
|
||||
|
||||
/* Style the scrollbar handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #e5e7eb; /* Handle color */
|
||||
background: theme("colors.scrollbar.thumb"); /* Handle color */
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Handle on 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 {
|
||||
background: #c7cdd2; /* Handle color */
|
||||
background: theme("colors.scrollbar.dark.thumb"); /* Handle color */
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
@ -27,7 +27,7 @@ export const SearchBar = ({ query, setQuery, onSearch }: SearchBarProps) => {
|
||||
|
||||
return (
|
||||
<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" />
|
||||
<textarea
|
||||
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} */
|
||||
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",
|
||||
"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"),
|
||||
],
|
||||
};
|
||||
module.exports = customThemes ? merge(baseThemes, customThemes) : baseThemes;
|
||||
|
Loading…
x
Reference in New Issue
Block a user