Feature/assistants (#1581)

* include alternate assisstant

- migrate models
- migrate db

* functional alternate assistant selection

* refactor chat components for persona API

* functional assistants api

* add full functionality- assistants

* add functional assistants dropdown handler

* refactor assistants for full compatability

- hooks
- track the live assistant for edge cases
- UI updates

* add assistant UI features

- Autotab
- Arrow selection
- Icons
- Proper @ detection
- Info Popup

prune unnecessary comments

* functional search toggling for assistants

* add functional cross-page assistants

rebase with main

* add proper interactivity for edge cases

- click outside of input / text box
- "force search" assistant consistency

* refactor alt assistant consistency

* update alembic versions

* rebased

* undo formatting changes

* additional formatting

* current processing

* merge fixes

* formatting

* colors

* 2 -> 1

* 1 -> 2

---------

Co-authored-by: “Pablo <“pablo@danswer.ai”>
This commit is contained in:
pablodanswer 2024-06-28 17:18:39 -07:00 committed by GitHub
parent 60dd77393d
commit ed550986a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 469 additions and 97 deletions

View File

@ -49,4 +49,4 @@ PYTHONUNBUFFERED=1
# Enable the full set of Danswer Enterprise Edition features # Enable the full set of Danswer Enterprise Edition features
# NOTE: DO NOT ENABLE THIS UNLESS YOU HAVE A PAID ENTERPRISE LICENSE (or if you are using this for local testing/development) # NOTE: DO NOT ENABLE THIS UNLESS YOU HAVE A PAID ENTERPRISE LICENSE (or if you are using this for local testing/development)
ENABLE_PAID_ENTERPRISE_EDITION_FEATURES=False ENABLE_PAID_ENTERPRISE_EDITION_FEATURES=False

View File

@ -0,0 +1,38 @@
"""add alternate assistant to chat message
Revision ID: 3a7802814195
Revises: 23957775e5f5
Create Date: 2024-06-05 11:18:49.966333
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "3a7802814195"
down_revision = "23957775e5f5"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"chat_message", sa.Column("alternate_assistant_id", sa.Integer(), nullable=True)
)
op.create_foreign_key(
"fk_chat_message_persona",
"chat_message",
"persona",
["alternate_assistant_id"],
["id"],
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint("fk_chat_message_persona", "chat_message", type_="foreignkey")
op.drop_column("chat_message", "alternate_assistant_id")

View File

@ -34,6 +34,7 @@ from danswer.db.llm import fetch_existing_llm_providers
from danswer.db.models import SearchDoc as DbSearchDoc from danswer.db.models import SearchDoc as DbSearchDoc
from danswer.db.models import ToolCall from danswer.db.models import ToolCall
from danswer.db.models import User from danswer.db.models import User
from danswer.db.persona import get_persona_by_id
from danswer.document_index.factory import get_default_document_index from danswer.document_index.factory import get_default_document_index
from danswer.file_store.models import ChatFileType from danswer.file_store.models import ChatFileType
from danswer.file_store.models import FileDescriptor from danswer.file_store.models import FileDescriptor
@ -223,7 +224,15 @@ def stream_chat_message_objects(
parent_id = new_msg_req.parent_message_id parent_id = new_msg_req.parent_message_id
reference_doc_ids = new_msg_req.search_doc_ids reference_doc_ids = new_msg_req.search_doc_ids
retrieval_options = new_msg_req.retrieval_options retrieval_options = new_msg_req.retrieval_options
persona = chat_session.persona alternate_assistant_id = new_msg_req.alternate_assistant_id
# use alternate persona if alternative assistant id is passed in
if alternate_assistant_id is not None:
persona = get_persona_by_id(
alternate_assistant_id, user=user, db_session=db_session
)
else:
persona = chat_session.persona
prompt_id = new_msg_req.prompt_id prompt_id = new_msg_req.prompt_id
if prompt_id is None and persona.prompts: if prompt_id is None and persona.prompts:
@ -380,6 +389,7 @@ def stream_chat_message_objects(
# rephrased_query=, # rephrased_query=,
# token_count=, # token_count=,
message_type=MessageType.ASSISTANT, message_type=MessageType.ASSISTANT,
alternate_assistant_id=new_msg_req.alternate_assistant_id,
# error=, # error=,
# reference_docs=, # reference_docs=,
db_session=db_session, db_session=db_session,
@ -389,11 +399,15 @@ def stream_chat_message_objects(
if not final_msg.prompt: if not final_msg.prompt:
raise RuntimeError("No Prompt found") raise RuntimeError("No Prompt found")
prompt_config = PromptConfig.from_model( prompt_config = (
final_msg.prompt, PromptConfig.from_model(
prompt_override=( final_msg.prompt,
new_msg_req.prompt_override or chat_session.prompt_override prompt_override=(
), new_msg_req.prompt_override or chat_session.prompt_override
),
)
if not persona
else PromptConfig.from_model(persona.prompts[0])
) )
# find out what tools to use # find out what tools to use

View File

@ -316,6 +316,7 @@ def create_new_chat_message(
rephrased_query: str | None = None, rephrased_query: str | None = None,
error: str | None = None, error: str | None = None,
reference_docs: list[DBSearchDoc] | None = None, reference_docs: list[DBSearchDoc] | None = None,
alternate_assistant_id: int | None = None,
# Maps the citation number [n] to the DB SearchDoc # Maps the citation number [n] to the DB SearchDoc
citations: dict[int, int] | None = None, citations: dict[int, int] | None = None,
tool_calls: list[ToolCall] | None = None, tool_calls: list[ToolCall] | None = None,
@ -334,6 +335,7 @@ def create_new_chat_message(
files=files, files=files,
tool_calls=tool_calls if tool_calls else [], tool_calls=tool_calls if tool_calls else [],
error=error, error=error,
alternate_assistant_id=alternate_assistant_id,
) )
# SQL Alchemy will propagate this to update the reference_docs' foreign keys # SQL Alchemy will propagate this to update the reference_docs' foreign keys
@ -497,14 +499,14 @@ def translate_db_search_doc_to_server_search_doc(
hidden=db_search_doc.hidden, hidden=db_search_doc.hidden,
metadata=db_search_doc.doc_metadata if not remove_doc_content else {}, metadata=db_search_doc.doc_metadata if not remove_doc_content else {},
score=db_search_doc.score, score=db_search_doc.score,
match_highlights=db_search_doc.match_highlights match_highlights=(
if not remove_doc_content db_search_doc.match_highlights if not remove_doc_content else []
else [], ),
updated_at=db_search_doc.updated_at if not remove_doc_content else None, updated_at=db_search_doc.updated_at if not remove_doc_content else None,
primary_owners=db_search_doc.primary_owners if not remove_doc_content else [], primary_owners=db_search_doc.primary_owners if not remove_doc_content else [],
secondary_owners=db_search_doc.secondary_owners secondary_owners=(
if not remove_doc_content db_search_doc.secondary_owners if not remove_doc_content else []
else [], ),
) )
@ -545,6 +547,7 @@ def translate_db_message_to_chat_message_detail(
) )
for tool_call in chat_message.tool_calls for tool_call in chat_message.tool_calls
], ],
alternate_assistant_id=chat_message.alternate_assistant_id,
) )
return chat_msg_detail return chat_msg_detail

View File

@ -708,6 +708,11 @@ class ChatMessage(Base):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
chat_session_id: Mapped[int] = mapped_column(ForeignKey("chat_session.id")) chat_session_id: Mapped[int] = mapped_column(ForeignKey("chat_session.id"))
alternate_assistant_id = mapped_column(
Integer, ForeignKey("persona.id"), nullable=True
)
parent_message: Mapped[int | None] = mapped_column(Integer, nullable=True) parent_message: Mapped[int | None] = mapped_column(Integer, nullable=True)
latest_child_message: Mapped[int | None] = mapped_column(Integer, nullable=True) latest_child_message: Mapped[int | None] = mapped_column(Integer, nullable=True)
message: Mapped[str] = mapped_column(Text) message: Mapped[str] = mapped_column(Text)
@ -736,10 +741,12 @@ class ChatMessage(Base):
chat_session: Mapped[ChatSession] = relationship("ChatSession") chat_session: Mapped[ChatSession] = relationship("ChatSession")
prompt: Mapped[Optional["Prompt"]] = relationship("Prompt") prompt: Mapped[Optional["Prompt"]] = relationship("Prompt")
chat_message_feedbacks: Mapped[list["ChatMessageFeedback"]] = relationship( chat_message_feedbacks: Mapped[list["ChatMessageFeedback"]] = relationship(
"ChatMessageFeedback", "ChatMessageFeedback",
back_populates="chat_message", back_populates="chat_message",
) )
document_feedbacks: Mapped[list["DocumentRetrievalFeedback"]] = relationship( document_feedbacks: Mapped[list["DocumentRetrievalFeedback"]] = relationship(
"DocumentRetrievalFeedback", "DocumentRetrievalFeedback",
back_populates="chat_message", back_populates="chat_message",

View File

@ -107,6 +107,9 @@ class CreateChatMessageRequest(ChunkContext):
llm_override: LLMOverride | None = None llm_override: LLMOverride | None = None
prompt_override: PromptOverride | None = None prompt_override: PromptOverride | None = None
# allow user to specify an alternate assistnat
alternate_assistant_id: int | None = None
# used for seeded chats to kick off the generation of an AI answer # used for seeded chats to kick off the generation of an AI answer
use_existing_user_message: bool = False use_existing_user_message: bool = False
@ -181,6 +184,7 @@ class ChatMessageDetail(BaseModel):
context_docs: RetrievalDocs | None context_docs: RetrievalDocs | None
message_type: MessageType message_type: MessageType
time_sent: datetime time_sent: datetime
alternate_assistant_id: str | None
# Dict mapping citation number to db_doc_id # Dict mapping citation number to db_doc_id
citations: dict[int, int] | None citations: dict[int, int] | None
files: list[FileDescriptor] files: list[FileDescriptor]

View File

@ -7,7 +7,6 @@ import { FiBookmark, FiCpu, FiInfo, FiX, FiZoomIn } from "react-icons/fi";
import { HoverPopup } from "@/components/HoverPopup"; import { HoverPopup } from "@/components/HoverPopup";
import { Modal } from "@/components/Modal"; import { Modal } from "@/components/Modal";
import { useState } from "react"; import { useState } from "react";
import { FaCaretDown, FaCaretRight } from "react-icons/fa";
import { Logo } from "@/components/Logo"; import { Logo } from "@/components/Logo";
const MAX_PERSONAS_TO_DISPLAY = 4; const MAX_PERSONAS_TO_DISPLAY = 4;
@ -29,11 +28,9 @@ function HelperItemDisplay({
export function ChatIntro({ export function ChatIntro({
availableSources, availableSources,
availablePersonas,
selectedPersona, selectedPersona,
}: { }: {
availableSources: ValidSources[]; availableSources: ValidSources[];
availablePersonas: Persona[];
selectedPersona: Persona; selectedPersona: Persona;
}) { }) {
const availableSourceMetadata = getSourceMetadataForSources(availableSources); const availableSourceMetadata = getSourceMetadataForSources(availableSources);

View File

@ -22,6 +22,7 @@ import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { import {
buildChatUrl, buildChatUrl,
buildLatestMessageChain, buildLatestMessageChain,
checkAnyAssistantHasSearch,
createChatSession, createChatSession,
getCitedDocumentsFromMessage, getCitedDocumentsFromMessage,
getHumanAndAIMessageFromMessageNumber, getHumanAndAIMessageFromMessageNumber,
@ -62,7 +63,6 @@ import { SettingsContext } from "@/components/settings/SettingsProvider";
import Dropzone from "react-dropzone"; import Dropzone from "react-dropzone";
import { import {
checkLLMSupportsImageInput, checkLLMSupportsImageInput,
destructureValue,
getFinalLLM, getFinalLLM,
structureValue, structureValue,
} from "@/lib/llm/utils"; } from "@/lib/llm/utils";
@ -78,9 +78,7 @@ import { TbLayoutSidebarRightExpand } from "react-icons/tb";
import { SIDEBAR_WIDTH_CONST } from "@/lib/constants"; import { SIDEBAR_WIDTH_CONST } from "@/lib/constants";
import ResizableSection from "@/components/resizable/ResizableSection"; import ResizableSection from "@/components/resizable/ResizableSection";
import { Button } from "@tremor/react";
const MAX_INPUT_HEIGHT = 200;
const TEMP_USER_MESSAGE_ID = -1; const TEMP_USER_MESSAGE_ID = -1;
const TEMP_ASSISTANT_MESSAGE_ID = -2; const TEMP_ASSISTANT_MESSAGE_ID = -2;
const SYSTEM_MESSAGE_ID = -3; const SYSTEM_MESSAGE_ID = -3;
@ -108,6 +106,12 @@ export function ChatPage({
const filteredAssistants = orderAssistantsForUser(availablePersonas, user); const filteredAssistants = orderAssistantsForUser(availablePersonas, user);
const [selectedAssistant, setSelectedAssistant] = useState<Persona | null>(
null
);
const [alternativeGeneratingAssistant, setAlternativeGeneratingAssistant] =
useState<Persona | null>(null);
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const existingChatIdRaw = searchParams.get("chatId"); const existingChatIdRaw = searchParams.get("chatId");
@ -213,6 +217,7 @@ export function ChatPage({
const response = await fetch( const response = await fetch(
`/api/chat/get-chat-session/${existingChatSessionId}` `/api/chat/get-chat-session/${existingChatSessionId}`
); );
const chatSession = (await response.json()) as BackendChatSession; const chatSession = (await response.json()) as BackendChatSession;
setSelectedPersona( setSelectedPersona(
@ -350,6 +355,7 @@ export function ChatPage({
setCompleteMessageMap(newCompleteMessageMap); setCompleteMessageMap(newCompleteMessageMap);
return newCompleteMessageMap; return newCompleteMessageMap;
}; };
const messageHistory = buildLatestMessageChain(completeMessageMap); const messageHistory = buildLatestMessageChain(completeMessageMap);
const [isStreaming, setIsStreaming] = useState(false); const [isStreaming, setIsStreaming] = useState(false);
@ -405,6 +411,7 @@ export function ChatPage({
// just choose a conservative default, this will be updated in the // just choose a conservative default, this will be updated in the
// background on initial load / on persona change // background on initial load / on persona change
const [maxTokens, setMaxTokens] = useState<number>(4096); const [maxTokens, setMaxTokens] = useState<number>(4096);
// fetch # of allowed document tokens for the selected Persona // fetch # of allowed document tokens for the selected Persona
useEffect(() => { useEffect(() => {
async function fetchMaxTokens() { async function fetchMaxTokens() {
@ -619,13 +626,17 @@ export function ChatPage({
queryOverride, queryOverride,
forceSearch, forceSearch,
isSeededChat, isSeededChat,
alternativeAssistant = null,
}: { }: {
messageIdToResend?: number; messageIdToResend?: number;
messageOverride?: string; messageOverride?: string;
queryOverride?: string; queryOverride?: string;
forceSearch?: boolean; forceSearch?: boolean;
isSeededChat?: boolean; isSeededChat?: boolean;
alternativeAssistant?: Persona | null;
} = {}) => { } = {}) => {
setAlternativeGeneratingAssistant(alternativeAssistant);
clientScrollToBottom(); clientScrollToBottom();
let currChatSessionId: number; let currChatSessionId: number;
let isNewSession = chatSessionId === null; let isNewSession = chatSessionId === null;
@ -645,6 +656,7 @@ export function ChatPage({
const messageToResend = messageHistory.find( const messageToResend = messageHistory.find(
(message) => message.messageId === messageIdToResend (message) => message.messageId === messageIdToResend
); );
const messageToResendParent = const messageToResendParent =
messageToResend?.parentMessageId !== null && messageToResend?.parentMessageId !== null &&
messageToResend?.parentMessageId !== undefined messageToResend?.parentMessageId !== undefined
@ -703,12 +715,19 @@ export function ChatPage({
const frozenCompleteMessageMap = upsertToCompleteMessageMap({ const frozenCompleteMessageMap = upsertToCompleteMessageMap({
messages: messageUpdates, messages: messageUpdates,
}); });
// on initial message send, we insert a dummy system message // on initial message send, we insert a dummy system message
// set this as the parent here if no parent is set // set this as the parent here if no parent is set
if (!parentMessage && frozenCompleteMessageMap.size === 2) { if (!parentMessage && frozenCompleteMessageMap.size === 2) {
parentMessage = frozenCompleteMessageMap.get(SYSTEM_MESSAGE_ID) || null; parentMessage = frozenCompleteMessageMap.get(SYSTEM_MESSAGE_ID) || null;
} }
const currentAssistantId = alternativeAssistant
? alternativeAssistant.id
: selectedAssistant?.id;
resetInputBar(); resetInputBar();
setIsStreaming(true); setIsStreaming(true);
let answer = ""; let answer = "";
let query: string | null = null; let query: string | null = null;
@ -721,6 +740,7 @@ export function ChatPage({
let error: string | null = null; let error: string | null = null;
let finalMessage: BackendMessage | null = null; let finalMessage: BackendMessage | null = null;
let toolCalls: ToolCallMetadata[] = []; let toolCalls: ToolCallMetadata[] = [];
try { try {
const lastSuccessfulMessageId = const lastSuccessfulMessageId =
getLastSuccessfulMessageId(currMessageHistory); getLastSuccessfulMessageId(currMessageHistory);
@ -728,6 +748,7 @@ export function ChatPage({
const stack = new CurrentMessageFIFO(); const stack = new CurrentMessageFIFO();
updateCurrentMessageFIFO(stack, { updateCurrentMessageFIFO(stack, {
message: currMessage, message: currMessage,
alternateAssistantId: currentAssistantId,
fileDescriptors: currentMessageFiles, fileDescriptors: currentMessageFiles,
parentMessageId: lastSuccessfulMessageId, parentMessageId: lastSuccessfulMessageId,
chatSessionId: currChatSessionId, chatSessionId: currChatSessionId,
@ -846,6 +867,7 @@ export function ChatPage({
files: finalMessage?.files || aiMessageImages || [], files: finalMessage?.files || aiMessageImages || [],
toolCalls: finalMessage?.tool_calls || toolCalls, toolCalls: finalMessage?.tool_calls || toolCalls,
parentMessageId: newUserMessageId, parentMessageId: newUserMessageId,
alternateAssistantID: selectedAssistant?.id,
}, },
]); ]);
} }
@ -905,6 +927,7 @@ export function ChatPage({
) { ) {
setSelectedMessageForDocDisplay(finalMessage.message_id); setSelectedMessageForDocDisplay(finalMessage.message_id);
} }
setAlternativeGeneratingAssistant(null);
}; };
const onFeedback = async ( const onFeedback = async (
@ -1021,10 +1044,37 @@ export function ChatPage({
setShowDocSidebar((showDocSidebar) => !showDocSidebar); // Toggle the state which will in turn toggle the class setShowDocSidebar((showDocSidebar) => !showDocSidebar); // Toggle the state which will in turn toggle the class
}; };
const retrievalDisabled = !personaIncludesRetrieval(livePersona); useEffect(() => {
const includes = checkAnyAssistantHasSearch(
messageHistory,
availablePersonas,
livePersona
);
setRetrievalEnabled(includes);
}, [messageHistory, availablePersonas, livePersona]);
const [retrievalEnabled, setRetrievalEnabled] = useState(() => {
return checkAnyAssistantHasSearch(
messageHistory,
availablePersonas,
livePersona
);
});
const [editingRetrievalEnabled, setEditingRetrievalEnabled] = useState(false);
const sidebarElementRef = useRef<HTMLDivElement>(null); const sidebarElementRef = useRef<HTMLDivElement>(null);
const innerSidebarElementRef = useRef<HTMLDivElement>(null); const innerSidebarElementRef = useRef<HTMLDivElement>(null);
const currentPersona = selectedAssistant || livePersona;
const updateSelectedAssistant = (newAssistant: Persona | null) => {
setSelectedAssistant(newAssistant);
if (newAssistant) {
setEditingRetrievalEnabled(personaIncludesRetrieval(newAssistant));
} else {
setEditingRetrievalEnabled(false);
}
};
return ( return (
<> <>
<HealthCheckBanner /> <HealthCheckBanner />
@ -1094,7 +1144,7 @@ export function ChatPage({
<> <>
<div <div
className={`w-full sm:relative h-screen ${ className={`w-full sm:relative h-screen ${
retrievalDisabled ? "pb-[111px]" : "pb-[140px]" !retrievalEnabled ? "pb-[111px]" : "pb-[140px]"
} }
flex-auto transition-margin duration-300 flex-auto transition-margin duration-300
overflow-x-auto overflow-x-auto
@ -1102,6 +1152,7 @@ export function ChatPage({
{...getRootProps()} {...getRootProps()}
> >
{/* <input {...getInputProps()} /> */} {/* <input {...getInputProps()} /> */}
<div <div
className={`w-full h-full flex flex-col overflow-y-auto overflow-x-hidden relative`} className={`w-full h-full flex flex-col overflow-y-auto overflow-x-hidden relative`}
ref={scrollableDivRef} ref={scrollableDivRef}
@ -1140,7 +1191,7 @@ export function ChatPage({
<div className="ml-4 flex my-auto"> <div className="ml-4 flex my-auto">
<UserDropdown user={user} /> <UserDropdown user={user} />
{!retrievalDisabled && !showDocSidebar && ( {retrievalEnabled && !showDocSidebar && (
<button <button
className="ml-4 mt-auto" className="ml-4 mt-auto"
onClick={() => toggleSidebar()} onClick={() => toggleSidebar()}
@ -1159,7 +1210,6 @@ export function ChatPage({
!isStreaming && ( !isStreaming && (
<ChatIntro <ChatIntro
availableSources={finalAvailableSources} availableSources={finalAvailableSources}
availablePersonas={filteredAssistants}
selectedPersona={livePersona} selectedPersona={livePersona}
/> />
)} )}
@ -1231,6 +1281,15 @@ export function ChatPage({
i === messageHistory.length - 1); i === messageHistory.length - 1);
const previousMessage = const previousMessage =
i !== 0 ? messageHistory[i - 1] : null; i !== 0 ? messageHistory[i - 1] : null;
const currentAlternativeAssistant =
message.alternateAssistantID != null
? availablePersonas.find(
(persona) =>
persona.id == message.alternateAssistantID
)
: null;
return ( return (
<div <div
key={`${i}-${existingChatSessionId}`} key={`${i}-${existingChatSessionId}`}
@ -1241,6 +1300,10 @@ export function ChatPage({
} }
> >
<AIMessage <AIMessage
currentPersona={livePersona}
alternativeAssistant={
currentAlternativeAssistant
}
messageId={message.messageId} messageId={message.messageId}
content={message.message} content={message.message}
files={message.files} files={message.files}
@ -1297,6 +1360,8 @@ export function ChatPage({
messageIdToResend: messageIdToResend:
previousMessage.messageId, previousMessage.messageId,
queryOverride: newQuery, queryOverride: newQuery,
alternativeAssistant:
currentAlternativeAssistant,
}); });
} }
: undefined : undefined
@ -1326,6 +1391,8 @@ export function ChatPage({
messageIdToResend: messageIdToResend:
previousMessage.messageId, previousMessage.messageId,
forceSearch: true, forceSearch: true,
alternativeAssistant:
currentAlternativeAssistant,
}); });
} else { } else {
setPopup({ setPopup({
@ -1335,7 +1402,13 @@ export function ChatPage({
}); });
} }
}} }}
retrievalDisabled={retrievalDisabled} retrievalDisabled={
currentAlternativeAssistant
? !personaIncludesRetrieval(
currentAlternativeAssistant!
)
: !retrievalEnabled
}
/> />
</div> </div>
); );
@ -1343,6 +1416,7 @@ export function ChatPage({
return ( return (
<div key={`${i}-${existingChatSessionId}`}> <div key={`${i}-${existingChatSessionId}`}>
<AIMessage <AIMessage
currentPersona={livePersona}
messageId={message.messageId} messageId={message.messageId}
personaName={livePersona.name} personaName={livePersona.name}
content={ content={
@ -1355,7 +1429,6 @@ export function ChatPage({
); );
} }
})} })}
{isStreaming && {isStreaming &&
messageHistory.length > 0 && messageHistory.length > 0 &&
messageHistory[messageHistory.length - 1].type === messageHistory[messageHistory.length - 1].type ===
@ -1364,6 +1437,11 @@ export function ChatPage({
key={`${messageHistory.length}-${existingChatSessionId}`} key={`${messageHistory.length}-${existingChatSessionId}`}
> >
<AIMessage <AIMessage
currentPersona={livePersona}
alternativeAssistant={
alternativeGeneratingAssistant ??
selectedAssistant
}
messageId={null} messageId={null}
personaName={livePersona.name} personaName={livePersona.name}
content={ content={
@ -1388,33 +1466,30 @@ export function ChatPage({
<div ref={endPaddingRef} className=" h-[95px]" /> <div ref={endPaddingRef} className=" h-[95px]" />
<div ref={endDivRef}></div> <div ref={endDivRef}></div>
{livePersona && {currentPersona &&
livePersona.starter_messages && currentPersona.starter_messages &&
livePersona.starter_messages.length > 0 && currentPersona.starter_messages.length > 0 &&
selectedPersona && selectedPersona &&
messageHistory.length === 0 && messageHistory.length === 0 &&
!isFetchingChatMessages && ( !isFetchingChatMessages && (
<div <div
className={` className={`
mx-auto mx-auto
px-4 px-4
w-searchbar-xs w-searchbar-xs
2xl:w-searchbar-sm 2xl:w-searchbar-sm
3xl:w-searchbar 3xl:w-searchbar
grid grid
gap-4 gap-4
grid-cols-1 grid-cols-1
grid-rows-1 grid-rows-1
mt-4 mt-4
md:grid-cols-2 md:grid-cols-2
mb-6`} mb-6`}
> >
{livePersona.starter_messages.map( {currentPersona.starter_messages.map(
(starterMessage, i) => ( (starterMessage, i) => (
<div <div key={i} className="w-full">
key={`${i}-${existingChatSessionId}`}
className="w-full"
>
<StarterMessage <StarterMessage
starterMessage={starterMessage} starterMessage={starterMessage}
onClick={() => onClick={() =>
@ -1433,28 +1508,24 @@ export function ChatPage({
</div> </div>
</div> </div>
<div <div className="absolute bottom-0 z-10 w-full">
ref={inputRef} <div className="w-full pb-4">
className="absolute bottom-0 z-10 w-full"
>
<div className="w-full relative pb-4">
{aboveHorizon && (
<div className="pointer-events-none w-full bg-transparent flex sticky justify-center">
<button
onClick={() => clientScrollToBottom(true)}
className="p-1 pointer-events-auto rounded-2xl bg-background-strong border border-border mb-2 mx-auto "
>
<FiArrowDown size={18} />
</button>
</div>
)}
<ChatInputBar <ChatInputBar
onSetSelectedAssistant={(
alternativeAssistant: Persona | null
) => {
updateSelectedAssistant(alternativeAssistant);
}}
alternativeAssistant={selectedAssistant}
personas={filteredAssistants}
message={message} message={message}
setMessage={setMessage} setMessage={setMessage}
onSubmit={onSubmit} onSubmit={onSubmit}
isStreaming={isStreaming} isStreaming={isStreaming}
setIsCancelled={setIsCancelled} setIsCancelled={setIsCancelled}
retrievalDisabled={retrievalDisabled} retrievalDisabled={
!personaIncludesRetrieval(currentPersona)
}
filterManager={filterManager} filterManager={filterManager}
llmOverrideManager={llmOverrideManager} llmOverrideManager={llmOverrideManager}
selectedAssistant={livePersona} selectedAssistant={livePersona}
@ -1468,7 +1539,7 @@ export function ChatPage({
</div> </div>
</div> </div>
{!retrievalDisabled ? ( {retrievalEnabled || editingRetrievalEnabled ? (
<div <div
ref={sidebarElementRef} ref={sidebarElementRef}
className={`relative flex-none overflow-y-hidden sidebar bg-background-weak h-screen`} className={`relative flex-none overflow-y-hidden sidebar bg-background-weak h-screen`}

View File

@ -14,8 +14,8 @@ export function StarterMessage({
} }
onClick={onClick} onClick={onClick}
> >
<p className="font-medium text-neutral-700">{starterMessage.name}</p> <p className="font-medium text-emphasis">{starterMessage.name}</p>
<p className="text-neutral-500 text-sm">{starterMessage.description}</p> <p className="text-subtle text-sm">{starterMessage.description}</p>
</div> </div>
); );
} }

View File

@ -1,18 +1,36 @@
import React, { EventHandler, useEffect, useRef } from "react"; import React, {
import { FiSend, FiFilter, FiPlusCircle, FiCpu } from "react-icons/fi"; Dispatch,
SetStateAction,
useEffect,
useRef,
useState,
} from "react";
import {
FiSend,
FiFilter,
FiPlusCircle,
FiCpu,
FiX,
FiPlus,
FiInfo,
} from "react-icons/fi";
import ChatInputOption from "./ChatInputOption"; import ChatInputOption from "./ChatInputOption";
import { FaBrain } from "react-icons/fa"; import { FaBrain } from "react-icons/fa";
import { Persona } from "@/app/admin/assistants/interfaces"; import { Persona } from "@/app/admin/assistants/interfaces";
import { FilterManager, LlmOverride, LlmOverrideManager } from "@/lib/hooks"; import { FilterManager, LlmOverrideManager } from "@/lib/hooks";
import { SelectedFilterDisplay } from "./SelectedFilterDisplay"; import { SelectedFilterDisplay } from "./SelectedFilterDisplay";
import { useChatContext } from "@/components/context/ChatContext"; import { useChatContext } from "@/components/context/ChatContext";
import { getFinalLLM } from "@/lib/llm/utils"; import { getFinalLLM } from "@/lib/llm/utils";
import { FileDescriptor } from "../interfaces"; import { FileDescriptor } from "../interfaces";
import { InputBarPreview } from "../files/InputBarPreview"; import { InputBarPreview } from "../files/InputBarPreview";
import { RobotIcon } from "@/components/icons/icons";
import { Hoverable } from "@/components/Hoverable";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { Tooltip } from "@/components/tooltip/Tooltip";
const MAX_INPUT_HEIGHT = 200; const MAX_INPUT_HEIGHT = 200;
export function ChatInputBar({ export function ChatInputBar({
personas,
message, message,
setMessage, setMessage,
onSubmit, onSubmit,
@ -21,13 +39,17 @@ export function ChatInputBar({
retrievalDisabled, retrievalDisabled,
filterManager, filterManager,
llmOverrideManager, llmOverrideManager,
onSetSelectedAssistant,
selectedAssistant, selectedAssistant,
files, files,
setFiles, setFiles,
handleFileUpload, handleFileUpload,
setConfigModalActiveTab, setConfigModalActiveTab,
textAreaRef, textAreaRef,
alternativeAssistant,
}: { }: {
onSetSelectedAssistant: (alternativeAssistant: Persona | null) => void;
personas: Persona[];
message: string; message: string;
setMessage: (message: string) => void; setMessage: (message: string) => void;
onSubmit: () => void; onSubmit: () => void;
@ -37,6 +59,7 @@ export function ChatInputBar({
filterManager: FilterManager; filterManager: FilterManager;
llmOverrideManager: LlmOverrideManager; llmOverrideManager: LlmOverrideManager;
selectedAssistant: Persona; selectedAssistant: Persona;
alternativeAssistant: Persona | null;
files: FileDescriptor[]; files: FileDescriptor[];
setFiles: (files: FileDescriptor[]) => void; setFiles: (files: FileDescriptor[]) => void;
handleFileUpload: (files: File[]) => void; handleFileUpload: (files: File[]) => void;
@ -75,6 +98,100 @@ export function ChatInputBar({
const { llmProviders } = useChatContext(); const { llmProviders } = useChatContext();
const [_, llmName] = getFinalLLM(llmProviders, selectedAssistant, null); const [_, llmName] = getFinalLLM(llmProviders, selectedAssistant, null);
const suggestionsRef = useRef<HTMLDivElement | null>(null);
const [showSuggestions, setShowSuggestions] = useState(false);
const interactionsRef = useRef<HTMLDivElement | null>(null);
const hideSuggestions = () => {
setShowSuggestions(false);
setAssistantIconIndex(0);
};
// Update selected persona
const updateCurrentPersona = (persona: Persona) => {
onSetSelectedAssistant(persona.id == selectedAssistant.id ? null : persona);
hideSuggestions();
setMessage("");
};
// Click out of assistant suggestions
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
suggestionsRef.current &&
!suggestionsRef.current.contains(event.target as Node) &&
(!interactionsRef.current ||
!interactionsRef.current.contains(event.target as Node))
) {
hideSuggestions();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
// Complete user input handling
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
const text = event.target.value;
setMessage(text);
if (!text.startsWith("@")) {
hideSuggestions();
return;
}
// If looking for an assistant...fup
const match = text.match(/(?:\s|^)@(\w*)$/);
if (match) {
setShowSuggestions(true);
} else {
hideSuggestions();
}
};
const filteredPersonas = personas.filter((persona) =>
persona.name.toLowerCase().startsWith(
message
.slice(message.lastIndexOf("@") + 1)
.split(/\s/)[0]
.toLowerCase()
)
);
const [assistantIconIndex, setAssistantIconIndex] = useState(0);
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (
showSuggestions &&
filteredPersonas.length > 0 &&
(e.key === "Tab" || e.key == "Enter")
) {
e.preventDefault();
if (assistantIconIndex == filteredPersonas.length) {
window.open("/assistants/new", "_blank");
hideSuggestions();
setMessage("");
} else {
const option =
filteredPersonas[assistantIconIndex >= 0 ? assistantIconIndex : 0];
updateCurrentPersona(option);
}
} else if (e.key === "ArrowDown") {
e.preventDefault();
setAssistantIconIndex((assistantIconIndex) =>
Math.min(assistantIconIndex + 1, filteredPersonas.length)
);
} else if (e.key === "ArrowUp") {
e.preventDefault();
setAssistantIconIndex((assistantIconIndex) =>
Math.max(assistantIconIndex - 1, 0)
);
}
};
return ( return (
<div> <div>
<div className="flex justify-center pb-2 max-w-screen-lg mx-auto mb-2"> <div className="flex justify-center pb-2 max-w-screen-lg mx-auto mb-2">
@ -90,9 +207,45 @@ export function ChatInputBar({
mx-auto mx-auto
" "
> >
{showSuggestions && filteredPersonas.length > 0 && (
<div
ref={suggestionsRef}
className="text-sm absolute inset-x-0 top-0 w-full transform -translate-y-full"
>
<div className="rounded-lg py-1.5 bg-white border border-border-medium overflow-hidden shadow-lg mx-2 px-1.5 mt-2 rounded z-10">
{filteredPersonas.map((currentPersona, index) => (
<button
key={index}
className={`px-2 ${assistantIconIndex == index && "bg-hover"} rounded content-start flex gap-x-1 py-1.5 w-full hover:bg-hover cursor-pointer`}
onClick={() => {
updateCurrentPersona(currentPersona);
}}
>
<p className="font-bold ">{currentPersona.name}</p>
<p className="line-clamp-1">
{currentPersona.id == selectedAssistant.id &&
"(default) "}
{currentPersona.description}
</p>
</button>
))}
<a
key={filteredPersonas.length}
target="_blank"
className={`${assistantIconIndex == filteredPersonas.length && "bg-hover"} px-3 flex gap-x-1 py-2 w-full items-center hover:bg-hover-light cursor-pointer"`}
href="/assistants/new"
>
<FiPlus size={17} />
<p>Create a new assistant</p>
</a>
</div>
</div>
)}
<div> <div>
<SelectedFilterDisplay filterManager={filterManager} /> <SelectedFilterDisplay filterManager={filterManager} />
</div> </div>
<div <div
className=" className="
opacity-100 opacity-100
@ -109,6 +262,38 @@ export function ChatInputBar({
[&:has(textarea:focus)]::ring-black [&:has(textarea:focus)]::ring-black
" "
> >
{alternativeAssistant && (
<div className="flex flex-wrap gap-y-1 gap-x-2 px-2 pt-1.5 w-full">
<div
ref={interactionsRef}
className="bg-background-subtle p-2 rounded-t-lg items-center flex w-full"
>
<AssistantIcon assistant={alternativeAssistant} border />
<p className="ml-3 text-strong my-auto">
{alternativeAssistant.name}
</p>
<div className="flex gap-x-1 ml-auto ">
<Tooltip
content={
<p className="max-w-xs flex flex-wrap">
{alternativeAssistant.description}
</p>
}
>
<button>
<Hoverable icon={FiInfo} />
</button>
</Tooltip>
<Hoverable
icon={FiX}
onClick={() => onSetSelectedAssistant(null)}
/>
</div>
</div>
</div>
)}
{files.length > 0 && ( {files.length > 0 && (
<div className="flex flex-wrap gap-y-1 gap-x-2 px-2 pt-2"> <div className="flex flex-wrap gap-y-1 gap-x-2 px-2 pt-2">
{files.map((file) => ( {files.map((file) => (
@ -128,8 +313,11 @@ export function ChatInputBar({
))} ))}
</div> </div>
)} )}
<textarea <textarea
onPaste={handlePaste} onPaste={handlePaste}
onKeyDownCapture={handleKeyDown}
onChange={handleInputChange}
ref={textAreaRef} ref={textAreaRef}
className={` className={`
m-0 m-0
@ -149,7 +337,7 @@ export function ChatInputBar({
break-word break-word
overscroll-contain overscroll-contain
outline-none outline-none
placeholder-gray-400 placeholder-subtle
overflow-hidden overflow-hidden
resize-none resize-none
pl-4 pl-4
@ -163,7 +351,6 @@ export function ChatInputBar({
aria-multiline aria-multiline
placeholder="Send a message..." placeholder="Send a message..."
value={message} value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(event) => { onKeyDown={(event) => {
if ( if (
event.key === "Enter" && event.key === "Enter" &&
@ -250,9 +437,6 @@ export function ChatInputBar({
</div> </div>
</div> </div>
</div> </div>
{/* <div className="text-center text-sm text-subtle mt-2">
Press "/" for shortcuts and useful prompts
</div> */}
</div> </div>
); );
} }

View File

@ -70,6 +70,7 @@ export interface Message {
parentMessageId: number | null; parentMessageId: number | null;
childrenMessageIds?: number[]; childrenMessageIds?: number[];
latestChildMessageId?: number | null; latestChildMessageId?: number | null;
alternateAssistantID?: number | null;
} }
export interface BackendChatSession { export interface BackendChatSession {
@ -95,6 +96,7 @@ export interface BackendMessage {
citations: CitationMap; citations: CitationMap;
files: FileDescriptor[]; files: FileDescriptor[];
tool_calls: ToolCallFinalResult[]; tool_calls: ToolCallFinalResult[];
alternate_assistant_id?: number | null;
} }
export interface DocumentsResponse { export interface DocumentsResponse {

View File

@ -95,6 +95,7 @@ export async function* sendMessage({
temperature, temperature,
systemPromptOverride, systemPromptOverride,
useExistingUserMessage, useExistingUserMessage,
alternateAssistantId,
}: { }: {
message: string; message: string;
fileDescriptors: FileDescriptor[]; fileDescriptors: FileDescriptor[];
@ -114,15 +115,18 @@ export async function* sendMessage({
// if specified, will use the existing latest user message // if specified, will use the existing latest user message
// and will ignore the specified `message` // and will ignore the specified `message`
useExistingUserMessage?: boolean; useExistingUserMessage?: boolean;
alternateAssistantId?: number;
}) { }) {
const documentsAreSelected = const documentsAreSelected =
selectedDocumentIds && selectedDocumentIds.length > 0; selectedDocumentIds && selectedDocumentIds.length > 0;
const sendMessageResponse = await fetch("/api/chat/send-message", { const sendMessageResponse = await fetch("/api/chat/send-message", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
alternate_assistant_id: alternateAssistantId,
chat_session_id: chatSessionId, chat_session_id: chatSessionId,
parent_message_id: parentMessageId, parent_message_id: parentMessageId,
message: message, message: message,
@ -381,6 +385,7 @@ export function processRawChatHistory(
message: messageInfo.message, message: messageInfo.message,
type: messageInfo.message_type as "user" | "assistant", type: messageInfo.message_type as "user" | "assistant",
files: messageInfo.files, files: messageInfo.files,
alternateAssistantID: Number(messageInfo.alternate_assistant_id),
// only include these fields if this is an assistant message so that // only include these fields if this is an assistant message so that
// this is identical to what is computed at streaming time // this is identical to what is computed at streaming time
...(messageInfo.message_type === "assistant" ...(messageInfo.message_type === "assistant"
@ -496,6 +501,32 @@ export function removeMessage(
completeMessageMap.delete(messageId); completeMessageMap.delete(messageId);
} }
export function checkAnyAssistantHasSearch(
messageHistory: Message[],
availablePersonas: Persona[],
livePersona: Persona
): boolean {
const response =
messageHistory.some((message) => {
if (
message.type !== "assistant" ||
message.alternateAssistantID === null
) {
return false;
}
const alternateAssistant = availablePersonas.find(
(persona) => persona.id === message.alternateAssistantID
);
return alternateAssistant
? personaIncludesRetrieval(alternateAssistant)
: false;
}) || personaIncludesRetrieval(livePersona);
return response;
}
export function personaIncludesRetrieval(selectedPersona: Persona) { export function personaIncludesRetrieval(selectedPersona: Persona) {
return selectedPersona.num_chunks !== 0; return selectedPersona.num_chunks !== 0;
} }

View File

@ -38,6 +38,9 @@ import Prism from "prismjs";
import "prismjs/themes/prism-tomorrow.css"; import "prismjs/themes/prism-tomorrow.css";
import "./custom-code-styles.css"; import "./custom-code-styles.css";
import { Persona } from "@/app/admin/assistants/interfaces";
import { Button } from "@tremor/react";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
const TOOLS_WITH_CUSTOM_HANDLING = [ const TOOLS_WITH_CUSTOM_HANDLING = [
SEARCH_TOOL_NAME, SEARCH_TOOL_NAME,
@ -80,6 +83,7 @@ function FileDisplay({ files }: { files: FileDescriptor[] }) {
} }
export const AIMessage = ({ export const AIMessage = ({
alternativeAssistant,
messageId, messageId,
content, content,
files, files,
@ -95,7 +99,10 @@ export const AIMessage = ({
handleSearchQueryEdit, handleSearchQueryEdit,
handleForceSearch, handleForceSearch,
retrievalDisabled, retrievalDisabled,
currentPersona,
}: { }: {
alternativeAssistant?: Persona | null;
currentPersona: Persona;
messageId: number | null; messageId: number | null;
content: string | JSX.Element; content: string | JSX.Element;
files?: FileDescriptor[]; files?: FileDescriptor[];
@ -165,14 +172,15 @@ export const AIMessage = ({
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar relative"> <div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar relative">
<div className="ml-8"> <div className="ml-8">
<div className="flex"> <div className="flex">
<div className="p-1 bg-ai rounded-lg h-fit my-auto"> <AssistantIcon
<div className="text-inverted"> size="small"
<FiCpu size={16} className="my-auto mx-auto" /> assistant={alternativeAssistant || currentPersona}
</div> />
</div>
<div className="font-bold text-emphasis ml-2 my-auto"> <div className="font-bold text-emphasis ml-2 my-auto">
{personaName || "Danswer"} {alternativeAssistant
? alternativeAssistant.name
: personaName || "Danswer"}
</div> </div>
{query === undefined && {query === undefined &&
@ -519,12 +527,6 @@ export const HumanMessage = ({
handleEditSubmit(); handleEditSubmit();
} }
}} }}
// ref={(textarea) => {
// if (textarea) {
// textarea.selectionStart = textarea.selectionEnd =
// textarea.value.length;
// }
// }}
/> />
<div className="flex justify-end mt-2 gap-2 pr-4"> <div className="flex justify-end mt-2 gap-2 pr-4">
<button <button

View File

@ -41,7 +41,6 @@ export default async function Page({
return ( return (
<> <>
<InstantSSRAutoRefresh /> <InstantSSRAutoRefresh />
{shouldShowWelcomeModal && <WelcomeModal user={user} />} {shouldShowWelcomeModal && <WelcomeModal user={user} />}
{!shouldShowWelcomeModal && !shouldDisplaySourcesIncompleteModal && ( {!shouldShowWelcomeModal && !shouldDisplaySourcesIncompleteModal && (
<ApiKeyModal user={user} /> <ApiKeyModal user={user} />
@ -49,7 +48,6 @@ export default async function Page({
{shouldDisplaySourcesIncompleteModal && ( {shouldDisplaySourcesIncompleteModal && (
<NoCompleteSourcesModal ccPairs={ccPairs} /> <NoCompleteSourcesModal ccPairs={ccPairs} />
)} )}
<ChatProvider <ChatProvider
value={{ value={{
user, user,

View File

@ -10,6 +10,7 @@ import {
import { AIMessage, HumanMessage } from "../../message/Messages"; import { AIMessage, HumanMessage } from "../../message/Messages";
import { Button, Callout, Divider } from "@tremor/react"; import { Button, Callout, Divider } from "@tremor/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useChatContext } from "@/components/context/ChatContext";
function BackToDanswerButton() { function BackToDanswerButton() {
const router = useRouter(); const router = useRouter();
@ -30,6 +31,7 @@ export function SharedChatDisplay({
}: { }: {
chatSession: BackendChatSession | null; chatSession: BackendChatSession | null;
}) { }) {
let { availablePersonas } = useChatContext();
if (!chatSession) { if (!chatSession) {
return ( return (
<div className="min-h-full w-full"> <div className="min-h-full w-full">
@ -44,6 +46,10 @@ export function SharedChatDisplay({
); );
} }
const currentPersona = availablePersonas.find(
(persona) => persona.id === chatSession.persona_id
);
const messages = buildLatestMessageChain( const messages = buildLatestMessageChain(
processRawChatHistory(chatSession.messages) processRawChatHistory(chatSession.messages)
); );
@ -77,6 +83,7 @@ export function SharedChatDisplay({
} else { } else {
return ( return (
<AIMessage <AIMessage
currentPersona={currentPersona!}
key={message.messageId} key={message.messageId}
messageId={message.messageId} messageId={message.messageId}
content={message.message} content={message.message}

View File

@ -90,6 +90,7 @@ export default async function Home() {
} }
let personas: Persona[] = []; let personas: Persona[] = [];
if (personaResponse?.ok) { if (personaResponse?.ok) {
personas = await personaResponse.json(); personas = await personaResponse.json();
} else { } else {

View File

@ -1,7 +1,7 @@
import { Persona } from "@/app/admin/assistants/interfaces"; import { Persona } from "@/app/admin/assistants/interfaces";
import React from "react"; import React from "react";
function generatePastelColorFromId(id: string): string { export function generatePastelColorFromId(id: string): string {
const hash = Array.from(id).reduce( const hash = Array.from(id).reduce(
(acc, char) => acc + char.charCodeAt(0), (acc, char) => acc + char.charCodeAt(0),
0 0
@ -12,16 +12,25 @@ function generatePastelColorFromId(id: string): string {
return `hsl(${hue}, ${saturation}%, ${lightness}%)`; return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
} }
export function AssistantIcon({ assistant }: { assistant: Persona }) { export function AssistantIcon({
assistant,
size,
border,
}: {
assistant: Persona;
size?: "small" | "medium" | "large";
border?: boolean;
}) {
const color = generatePastelColorFromId(assistant.id.toString()); const color = generatePastelColorFromId(assistant.id.toString());
return ( return (
<div <div
className=" className={`
w-10 ${border && " border border-.5 border-border-strong "}
h-10 ${(!size || size == "large") && "w-6 h-6"}
rounded-lg ${size == "small" && "w-6 h-6"}
" rounded-lg
`}
style={{ backgroundColor: color }} style={{ backgroundColor: color }}
/> />
); );

View File

@ -27,7 +27,7 @@ export const HealthCheckBanner = () => {
</p> </p>
<a <a
href="/auth/login" href="/auth/login"
className="w-full mt-4 mx-auto rounded-md text-neutral-200 py-2 bg-neutral-800 text-center hover:bg-neutral-700 animtate duration-300 transition-bg " className="w-full mt-4 mx-auto rounded-md text-light py-2 bg-background-dark text-center hover:bg-emphasis animtate duration-300 transition-bg "
> >
Log in Log in
</a> </a>
@ -36,7 +36,7 @@ export const HealthCheckBanner = () => {
); );
} else { } else {
return ( return (
<div className="text-xs mx-auto bg-gradient-to-r from-red-900 to-red-700 p-2 rounded-sm border-hidden text-gray-300"> <div className="text-xs mx-auto bg-gradient-to-r from-red-900 to-red-700 p-2 rounded-sm border-hidden text-light">
<p className="font-bold pb-1">The backend is currently unavailable.</p> <p className="font-bold pb-1">The backend is currently unavailable.</p>
<p className="px-1"> <p className="px-1">

View File

@ -29,6 +29,7 @@ export function Tooltip({
side={side} side={side}
align={align} align={align}
className=" className="
bg-background-inverted bg-background-inverted
text-inverted text-inverted
text-sm text-sm
@ -36,7 +37,7 @@ export function Tooltip({
py-1 py-1
px-2 px-2
shadow-lg shadow-lg
z-50 z-100
" "
> >
{content} {content}

View File

@ -39,14 +39,17 @@ module.exports = {
colors: { colors: {
// background // background
background: "#f9fafb", // gray-50 background: "#f9fafb", // gray-50
"background-subtle": "#e5e7eb", // gray-200
"background-emphasis": "#f6f7f8", "background-emphasis": "#f6f7f8",
"background-strong": "#eaecef", "background-strong": "#eaecef",
"background-search": "#ffffff", "background-search": "#ffffff",
"background-custom-header": "#f3f4f6", "background-custom-header": "#f3f4f6",
"background-inverted": "#000000", "background-inverted": "#000000",
"background-weak": "#f3f4f6", // gray-100 "background-weak": "#f3f4f6", // gray-100
"background-dark": "#111827", // gray-900
// text or icons // text or icons
light: "#e5e7eb", // gray-200
link: "#3b82f6", // blue-500 link: "#3b82f6", // blue-500
"link-hover": "#1d4ed8", // blue-700 "link-hover": "#1d4ed8", // blue-700
subtle: "#6b7280", // gray-500 subtle: "#6b7280", // gray-500