mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-26 17:51:54 +01:00
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:
parent
60dd77393d
commit
ed550986a6
2
.vscode/env_template.txt
vendored
2
.vscode/env_template.txt
vendored
@ -49,4 +49,4 @@ PYTHONUNBUFFERED=1
|
||||
|
||||
# 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)
|
||||
ENABLE_PAID_ENTERPRISE_EDITION_FEATURES=False
|
||||
ENABLE_PAID_ENTERPRISE_EDITION_FEATURES=False
|
@ -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")
|
@ -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 ToolCall
|
||||
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.file_store.models import ChatFileType
|
||||
from danswer.file_store.models import FileDescriptor
|
||||
@ -223,7 +224,15 @@ def stream_chat_message_objects(
|
||||
parent_id = new_msg_req.parent_message_id
|
||||
reference_doc_ids = new_msg_req.search_doc_ids
|
||||
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
|
||||
if prompt_id is None and persona.prompts:
|
||||
@ -380,6 +389,7 @@ def stream_chat_message_objects(
|
||||
# rephrased_query=,
|
||||
# token_count=,
|
||||
message_type=MessageType.ASSISTANT,
|
||||
alternate_assistant_id=new_msg_req.alternate_assistant_id,
|
||||
# error=,
|
||||
# reference_docs=,
|
||||
db_session=db_session,
|
||||
@ -389,11 +399,15 @@ def stream_chat_message_objects(
|
||||
if not final_msg.prompt:
|
||||
raise RuntimeError("No Prompt found")
|
||||
|
||||
prompt_config = PromptConfig.from_model(
|
||||
final_msg.prompt,
|
||||
prompt_override=(
|
||||
new_msg_req.prompt_override or chat_session.prompt_override
|
||||
),
|
||||
prompt_config = (
|
||||
PromptConfig.from_model(
|
||||
final_msg.prompt,
|
||||
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
|
||||
|
@ -316,6 +316,7 @@ def create_new_chat_message(
|
||||
rephrased_query: str | None = None,
|
||||
error: str | None = None,
|
||||
reference_docs: list[DBSearchDoc] | None = None,
|
||||
alternate_assistant_id: int | None = None,
|
||||
# Maps the citation number [n] to the DB SearchDoc
|
||||
citations: dict[int, int] | None = None,
|
||||
tool_calls: list[ToolCall] | None = None,
|
||||
@ -334,6 +335,7 @@ def create_new_chat_message(
|
||||
files=files,
|
||||
tool_calls=tool_calls if tool_calls else [],
|
||||
error=error,
|
||||
alternate_assistant_id=alternate_assistant_id,
|
||||
)
|
||||
|
||||
# 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,
|
||||
metadata=db_search_doc.doc_metadata if not remove_doc_content else {},
|
||||
score=db_search_doc.score,
|
||||
match_highlights=db_search_doc.match_highlights
|
||||
if not remove_doc_content
|
||||
else [],
|
||||
match_highlights=(
|
||||
db_search_doc.match_highlights if not remove_doc_content else []
|
||||
),
|
||||
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 [],
|
||||
secondary_owners=db_search_doc.secondary_owners
|
||||
if not remove_doc_content
|
||||
else [],
|
||||
secondary_owners=(
|
||||
db_search_doc.secondary_owners if not remove_doc_content else []
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@ -545,6 +547,7 @@ def translate_db_message_to_chat_message_detail(
|
||||
)
|
||||
for tool_call in chat_message.tool_calls
|
||||
],
|
||||
alternate_assistant_id=chat_message.alternate_assistant_id,
|
||||
)
|
||||
|
||||
return chat_msg_detail
|
||||
|
@ -708,6 +708,11 @@ class ChatMessage(Base):
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
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)
|
||||
latest_child_message: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
message: Mapped[str] = mapped_column(Text)
|
||||
@ -736,10 +741,12 @@ class ChatMessage(Base):
|
||||
|
||||
chat_session: Mapped[ChatSession] = relationship("ChatSession")
|
||||
prompt: Mapped[Optional["Prompt"]] = relationship("Prompt")
|
||||
|
||||
chat_message_feedbacks: Mapped[list["ChatMessageFeedback"]] = relationship(
|
||||
"ChatMessageFeedback",
|
||||
back_populates="chat_message",
|
||||
)
|
||||
|
||||
document_feedbacks: Mapped[list["DocumentRetrievalFeedback"]] = relationship(
|
||||
"DocumentRetrievalFeedback",
|
||||
back_populates="chat_message",
|
||||
|
@ -107,6 +107,9 @@ class CreateChatMessageRequest(ChunkContext):
|
||||
llm_override: LLMOverride | 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
|
||||
use_existing_user_message: bool = False
|
||||
|
||||
@ -181,6 +184,7 @@ class ChatMessageDetail(BaseModel):
|
||||
context_docs: RetrievalDocs | None
|
||||
message_type: MessageType
|
||||
time_sent: datetime
|
||||
alternate_assistant_id: str | None
|
||||
# Dict mapping citation number to db_doc_id
|
||||
citations: dict[int, int] | None
|
||||
files: list[FileDescriptor]
|
||||
|
@ -7,7 +7,6 @@ import { FiBookmark, FiCpu, FiInfo, FiX, FiZoomIn } from "react-icons/fi";
|
||||
import { HoverPopup } from "@/components/HoverPopup";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { useState } from "react";
|
||||
import { FaCaretDown, FaCaretRight } from "react-icons/fa";
|
||||
import { Logo } from "@/components/Logo";
|
||||
|
||||
const MAX_PERSONAS_TO_DISPLAY = 4;
|
||||
@ -29,11 +28,9 @@ function HelperItemDisplay({
|
||||
|
||||
export function ChatIntro({
|
||||
availableSources,
|
||||
availablePersonas,
|
||||
selectedPersona,
|
||||
}: {
|
||||
availableSources: ValidSources[];
|
||||
availablePersonas: Persona[];
|
||||
selectedPersona: Persona;
|
||||
}) {
|
||||
const availableSourceMetadata = getSourceMetadataForSources(availableSources);
|
||||
|
@ -22,6 +22,7 @@ import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
import {
|
||||
buildChatUrl,
|
||||
buildLatestMessageChain,
|
||||
checkAnyAssistantHasSearch,
|
||||
createChatSession,
|
||||
getCitedDocumentsFromMessage,
|
||||
getHumanAndAIMessageFromMessageNumber,
|
||||
@ -62,7 +63,6 @@ import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import Dropzone from "react-dropzone";
|
||||
import {
|
||||
checkLLMSupportsImageInput,
|
||||
destructureValue,
|
||||
getFinalLLM,
|
||||
structureValue,
|
||||
} from "@/lib/llm/utils";
|
||||
@ -78,9 +78,7 @@ import { TbLayoutSidebarRightExpand } from "react-icons/tb";
|
||||
import { SIDEBAR_WIDTH_CONST } from "@/lib/constants";
|
||||
|
||||
import ResizableSection from "@/components/resizable/ResizableSection";
|
||||
import { Button } from "@tremor/react";
|
||||
|
||||
const MAX_INPUT_HEIGHT = 200;
|
||||
const TEMP_USER_MESSAGE_ID = -1;
|
||||
const TEMP_ASSISTANT_MESSAGE_ID = -2;
|
||||
const SYSTEM_MESSAGE_ID = -3;
|
||||
@ -108,6 +106,12 @@ export function ChatPage({
|
||||
|
||||
const filteredAssistants = orderAssistantsForUser(availablePersonas, user);
|
||||
|
||||
const [selectedAssistant, setSelectedAssistant] = useState<Persona | null>(
|
||||
null
|
||||
);
|
||||
const [alternativeGeneratingAssistant, setAlternativeGeneratingAssistant] =
|
||||
useState<Persona | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const existingChatIdRaw = searchParams.get("chatId");
|
||||
@ -213,6 +217,7 @@ export function ChatPage({
|
||||
const response = await fetch(
|
||||
`/api/chat/get-chat-session/${existingChatSessionId}`
|
||||
);
|
||||
|
||||
const chatSession = (await response.json()) as BackendChatSession;
|
||||
|
||||
setSelectedPersona(
|
||||
@ -350,6 +355,7 @@ export function ChatPage({
|
||||
setCompleteMessageMap(newCompleteMessageMap);
|
||||
return newCompleteMessageMap;
|
||||
};
|
||||
|
||||
const messageHistory = buildLatestMessageChain(completeMessageMap);
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
|
||||
@ -405,6 +411,7 @@ export function ChatPage({
|
||||
// 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() {
|
||||
@ -619,13 +626,17 @@ export function ChatPage({
|
||||
queryOverride,
|
||||
forceSearch,
|
||||
isSeededChat,
|
||||
alternativeAssistant = null,
|
||||
}: {
|
||||
messageIdToResend?: number;
|
||||
messageOverride?: string;
|
||||
queryOverride?: string;
|
||||
forceSearch?: boolean;
|
||||
isSeededChat?: boolean;
|
||||
alternativeAssistant?: Persona | null;
|
||||
} = {}) => {
|
||||
setAlternativeGeneratingAssistant(alternativeAssistant);
|
||||
|
||||
clientScrollToBottom();
|
||||
let currChatSessionId: number;
|
||||
let isNewSession = chatSessionId === null;
|
||||
@ -645,6 +656,7 @@ export function ChatPage({
|
||||
const messageToResend = messageHistory.find(
|
||||
(message) => message.messageId === messageIdToResend
|
||||
);
|
||||
|
||||
const messageToResendParent =
|
||||
messageToResend?.parentMessageId !== null &&
|
||||
messageToResend?.parentMessageId !== undefined
|
||||
@ -703,12 +715,19 @@ export function ChatPage({
|
||||
const frozenCompleteMessageMap = upsertToCompleteMessageMap({
|
||||
messages: messageUpdates,
|
||||
});
|
||||
|
||||
// on initial message send, we insert a dummy system message
|
||||
// set this as the parent here if no parent is set
|
||||
if (!parentMessage && frozenCompleteMessageMap.size === 2) {
|
||||
parentMessage = frozenCompleteMessageMap.get(SYSTEM_MESSAGE_ID) || null;
|
||||
}
|
||||
|
||||
const currentAssistantId = alternativeAssistant
|
||||
? alternativeAssistant.id
|
||||
: selectedAssistant?.id;
|
||||
|
||||
resetInputBar();
|
||||
|
||||
setIsStreaming(true);
|
||||
let answer = "";
|
||||
let query: string | null = null;
|
||||
@ -721,6 +740,7 @@ export function ChatPage({
|
||||
let error: string | null = null;
|
||||
let finalMessage: BackendMessage | null = null;
|
||||
let toolCalls: ToolCallMetadata[] = [];
|
||||
|
||||
try {
|
||||
const lastSuccessfulMessageId =
|
||||
getLastSuccessfulMessageId(currMessageHistory);
|
||||
@ -728,6 +748,7 @@ export function ChatPage({
|
||||
const stack = new CurrentMessageFIFO();
|
||||
updateCurrentMessageFIFO(stack, {
|
||||
message: currMessage,
|
||||
alternateAssistantId: currentAssistantId,
|
||||
fileDescriptors: currentMessageFiles,
|
||||
parentMessageId: lastSuccessfulMessageId,
|
||||
chatSessionId: currChatSessionId,
|
||||
@ -846,6 +867,7 @@ export function ChatPage({
|
||||
files: finalMessage?.files || aiMessageImages || [],
|
||||
toolCalls: finalMessage?.tool_calls || toolCalls,
|
||||
parentMessageId: newUserMessageId,
|
||||
alternateAssistantID: selectedAssistant?.id,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@ -905,6 +927,7 @@ export function ChatPage({
|
||||
) {
|
||||
setSelectedMessageForDocDisplay(finalMessage.message_id);
|
||||
}
|
||||
setAlternativeGeneratingAssistant(null);
|
||||
};
|
||||
|
||||
const onFeedback = async (
|
||||
@ -1021,10 +1044,37 @@ export function ChatPage({
|
||||
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 innerSidebarElementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const currentPersona = selectedAssistant || livePersona;
|
||||
|
||||
const updateSelectedAssistant = (newAssistant: Persona | null) => {
|
||||
setSelectedAssistant(newAssistant);
|
||||
if (newAssistant) {
|
||||
setEditingRetrievalEnabled(personaIncludesRetrieval(newAssistant));
|
||||
} else {
|
||||
setEditingRetrievalEnabled(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<HealthCheckBanner />
|
||||
@ -1094,7 +1144,7 @@ export function ChatPage({
|
||||
<>
|
||||
<div
|
||||
className={`w-full sm:relative h-screen ${
|
||||
retrievalDisabled ? "pb-[111px]" : "pb-[140px]"
|
||||
!retrievalEnabled ? "pb-[111px]" : "pb-[140px]"
|
||||
}
|
||||
flex-auto transition-margin duration-300
|
||||
overflow-x-auto
|
||||
@ -1102,6 +1152,7 @@ export function ChatPage({
|
||||
{...getRootProps()}
|
||||
>
|
||||
{/* <input {...getInputProps()} /> */}
|
||||
|
||||
<div
|
||||
className={`w-full h-full flex flex-col overflow-y-auto overflow-x-hidden relative`}
|
||||
ref={scrollableDivRef}
|
||||
@ -1140,7 +1191,7 @@ export function ChatPage({
|
||||
|
||||
<div className="ml-4 flex my-auto">
|
||||
<UserDropdown user={user} />
|
||||
{!retrievalDisabled && !showDocSidebar && (
|
||||
{retrievalEnabled && !showDocSidebar && (
|
||||
<button
|
||||
className="ml-4 mt-auto"
|
||||
onClick={() => toggleSidebar()}
|
||||
@ -1159,7 +1210,6 @@ export function ChatPage({
|
||||
!isStreaming && (
|
||||
<ChatIntro
|
||||
availableSources={finalAvailableSources}
|
||||
availablePersonas={filteredAssistants}
|
||||
selectedPersona={livePersona}
|
||||
/>
|
||||
)}
|
||||
@ -1231,6 +1281,15 @@ export function ChatPage({
|
||||
i === messageHistory.length - 1);
|
||||
const previousMessage =
|
||||
i !== 0 ? messageHistory[i - 1] : null;
|
||||
|
||||
const currentAlternativeAssistant =
|
||||
message.alternateAssistantID != null
|
||||
? availablePersonas.find(
|
||||
(persona) =>
|
||||
persona.id == message.alternateAssistantID
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${i}-${existingChatSessionId}`}
|
||||
@ -1241,6 +1300,10 @@ export function ChatPage({
|
||||
}
|
||||
>
|
||||
<AIMessage
|
||||
currentPersona={livePersona}
|
||||
alternativeAssistant={
|
||||
currentAlternativeAssistant
|
||||
}
|
||||
messageId={message.messageId}
|
||||
content={message.message}
|
||||
files={message.files}
|
||||
@ -1297,6 +1360,8 @@ export function ChatPage({
|
||||
messageIdToResend:
|
||||
previousMessage.messageId,
|
||||
queryOverride: newQuery,
|
||||
alternativeAssistant:
|
||||
currentAlternativeAssistant,
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
@ -1326,6 +1391,8 @@ export function ChatPage({
|
||||
messageIdToResend:
|
||||
previousMessage.messageId,
|
||||
forceSearch: true,
|
||||
alternativeAssistant:
|
||||
currentAlternativeAssistant,
|
||||
});
|
||||
} else {
|
||||
setPopup({
|
||||
@ -1335,7 +1402,13 @@ export function ChatPage({
|
||||
});
|
||||
}
|
||||
}}
|
||||
retrievalDisabled={retrievalDisabled}
|
||||
retrievalDisabled={
|
||||
currentAlternativeAssistant
|
||||
? !personaIncludesRetrieval(
|
||||
currentAlternativeAssistant!
|
||||
)
|
||||
: !retrievalEnabled
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -1343,6 +1416,7 @@ export function ChatPage({
|
||||
return (
|
||||
<div key={`${i}-${existingChatSessionId}`}>
|
||||
<AIMessage
|
||||
currentPersona={livePersona}
|
||||
messageId={message.messageId}
|
||||
personaName={livePersona.name}
|
||||
content={
|
||||
@ -1355,7 +1429,6 @@ export function ChatPage({
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
{isStreaming &&
|
||||
messageHistory.length > 0 &&
|
||||
messageHistory[messageHistory.length - 1].type ===
|
||||
@ -1364,6 +1437,11 @@ export function ChatPage({
|
||||
key={`${messageHistory.length}-${existingChatSessionId}`}
|
||||
>
|
||||
<AIMessage
|
||||
currentPersona={livePersona}
|
||||
alternativeAssistant={
|
||||
alternativeGeneratingAssistant ??
|
||||
selectedAssistant
|
||||
}
|
||||
messageId={null}
|
||||
personaName={livePersona.name}
|
||||
content={
|
||||
@ -1388,33 +1466,30 @@ export function ChatPage({
|
||||
<div ref={endPaddingRef} className=" h-[95px]" />
|
||||
<div ref={endDivRef}></div>
|
||||
|
||||
{livePersona &&
|
||||
livePersona.starter_messages &&
|
||||
livePersona.starter_messages.length > 0 &&
|
||||
{currentPersona &&
|
||||
currentPersona.starter_messages &&
|
||||
currentPersona.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`}
|
||||
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(
|
||||
{currentPersona.starter_messages.map(
|
||||
(starterMessage, i) => (
|
||||
<div
|
||||
key={`${i}-${existingChatSessionId}`}
|
||||
className="w-full"
|
||||
>
|
||||
<div key={i} className="w-full">
|
||||
<StarterMessage
|
||||
starterMessage={starterMessage}
|
||||
onClick={() =>
|
||||
@ -1433,28 +1508,24 @@ export function ChatPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={inputRef}
|
||||
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>
|
||||
)}
|
||||
<div className="absolute bottom-0 z-10 w-full">
|
||||
<div className="w-full pb-4">
|
||||
<ChatInputBar
|
||||
onSetSelectedAssistant={(
|
||||
alternativeAssistant: Persona | null
|
||||
) => {
|
||||
updateSelectedAssistant(alternativeAssistant);
|
||||
}}
|
||||
alternativeAssistant={selectedAssistant}
|
||||
personas={filteredAssistants}
|
||||
message={message}
|
||||
setMessage={setMessage}
|
||||
onSubmit={onSubmit}
|
||||
isStreaming={isStreaming}
|
||||
setIsCancelled={setIsCancelled}
|
||||
retrievalDisabled={retrievalDisabled}
|
||||
retrievalDisabled={
|
||||
!personaIncludesRetrieval(currentPersona)
|
||||
}
|
||||
filterManager={filterManager}
|
||||
llmOverrideManager={llmOverrideManager}
|
||||
selectedAssistant={livePersona}
|
||||
@ -1468,7 +1539,7 @@ export function ChatPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!retrievalDisabled ? (
|
||||
{retrievalEnabled || editingRetrievalEnabled ? (
|
||||
<div
|
||||
ref={sidebarElementRef}
|
||||
className={`relative flex-none overflow-y-hidden sidebar bg-background-weak h-screen`}
|
||||
|
@ -14,8 +14,8 @@ export function StarterMessage({
|
||||
}
|
||||
onClick={onClick}
|
||||
>
|
||||
<p className="font-medium text-neutral-700">{starterMessage.name}</p>
|
||||
<p className="text-neutral-500 text-sm">{starterMessage.description}</p>
|
||||
<p className="font-medium text-emphasis">{starterMessage.name}</p>
|
||||
<p className="text-subtle text-sm">{starterMessage.description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,18 +1,36 @@
|
||||
import React, { EventHandler, useEffect, useRef } from "react";
|
||||
import { FiSend, FiFilter, FiPlusCircle, FiCpu } from "react-icons/fi";
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
FiSend,
|
||||
FiFilter,
|
||||
FiPlusCircle,
|
||||
FiCpu,
|
||||
FiX,
|
||||
FiPlus,
|
||||
FiInfo,
|
||||
} from "react-icons/fi";
|
||||
import ChatInputOption from "./ChatInputOption";
|
||||
import { FaBrain } from "react-icons/fa";
|
||||
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 { useChatContext } from "@/components/context/ChatContext";
|
||||
import { getFinalLLM } from "@/lib/llm/utils";
|
||||
import { FileDescriptor } from "../interfaces";
|
||||
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;
|
||||
|
||||
export function ChatInputBar({
|
||||
personas,
|
||||
message,
|
||||
setMessage,
|
||||
onSubmit,
|
||||
@ -21,13 +39,17 @@ export function ChatInputBar({
|
||||
retrievalDisabled,
|
||||
filterManager,
|
||||
llmOverrideManager,
|
||||
onSetSelectedAssistant,
|
||||
selectedAssistant,
|
||||
files,
|
||||
setFiles,
|
||||
handleFileUpload,
|
||||
setConfigModalActiveTab,
|
||||
textAreaRef,
|
||||
alternativeAssistant,
|
||||
}: {
|
||||
onSetSelectedAssistant: (alternativeAssistant: Persona | null) => void;
|
||||
personas: Persona[];
|
||||
message: string;
|
||||
setMessage: (message: string) => void;
|
||||
onSubmit: () => void;
|
||||
@ -37,6 +59,7 @@ export function ChatInputBar({
|
||||
filterManager: FilterManager;
|
||||
llmOverrideManager: LlmOverrideManager;
|
||||
selectedAssistant: Persona;
|
||||
alternativeAssistant: Persona | null;
|
||||
files: FileDescriptor[];
|
||||
setFiles: (files: FileDescriptor[]) => void;
|
||||
handleFileUpload: (files: File[]) => void;
|
||||
@ -75,6 +98,100 @@ export function ChatInputBar({
|
||||
const { llmProviders } = useChatContext();
|
||||
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 (
|
||||
<div>
|
||||
<div className="flex justify-center pb-2 max-w-screen-lg mx-auto mb-2">
|
||||
@ -90,9 +207,45 @@ export function ChatInputBar({
|
||||
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>
|
||||
<SelectedFilterDisplay filterManager={filterManager} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="
|
||||
opacity-100
|
||||
@ -109,6 +262,38 @@ export function ChatInputBar({
|
||||
[&: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 && (
|
||||
<div className="flex flex-wrap gap-y-1 gap-x-2 px-2 pt-2">
|
||||
{files.map((file) => (
|
||||
@ -128,8 +313,11 @@ export function ChatInputBar({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<textarea
|
||||
onPaste={handlePaste}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
onChange={handleInputChange}
|
||||
ref={textAreaRef}
|
||||
className={`
|
||||
m-0
|
||||
@ -149,7 +337,7 @@ export function ChatInputBar({
|
||||
break-word
|
||||
overscroll-contain
|
||||
outline-none
|
||||
placeholder-gray-400
|
||||
placeholder-subtle
|
||||
overflow-hidden
|
||||
resize-none
|
||||
pl-4
|
||||
@ -163,7 +351,6 @@ export function ChatInputBar({
|
||||
aria-multiline
|
||||
placeholder="Send a message..."
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
@ -250,9 +437,6 @@ export function ChatInputBar({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="text-center text-sm text-subtle mt-2">
|
||||
Press "/" for shortcuts and useful prompts
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -70,6 +70,7 @@ export interface Message {
|
||||
parentMessageId: number | null;
|
||||
childrenMessageIds?: number[];
|
||||
latestChildMessageId?: number | null;
|
||||
alternateAssistantID?: number | null;
|
||||
}
|
||||
|
||||
export interface BackendChatSession {
|
||||
@ -95,6 +96,7 @@ export interface BackendMessage {
|
||||
citations: CitationMap;
|
||||
files: FileDescriptor[];
|
||||
tool_calls: ToolCallFinalResult[];
|
||||
alternate_assistant_id?: number | null;
|
||||
}
|
||||
|
||||
export interface DocumentsResponse {
|
||||
|
@ -95,6 +95,7 @@ export async function* sendMessage({
|
||||
temperature,
|
||||
systemPromptOverride,
|
||||
useExistingUserMessage,
|
||||
alternateAssistantId,
|
||||
}: {
|
||||
message: string;
|
||||
fileDescriptors: FileDescriptor[];
|
||||
@ -114,15 +115,18 @@ export async function* sendMessage({
|
||||
// if specified, will use the existing latest user message
|
||||
// and will ignore the specified `message`
|
||||
useExistingUserMessage?: boolean;
|
||||
alternateAssistantId?: number;
|
||||
}) {
|
||||
const documentsAreSelected =
|
||||
selectedDocumentIds && selectedDocumentIds.length > 0;
|
||||
|
||||
const sendMessageResponse = await fetch("/api/chat/send-message", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
alternate_assistant_id: alternateAssistantId,
|
||||
chat_session_id: chatSessionId,
|
||||
parent_message_id: parentMessageId,
|
||||
message: message,
|
||||
@ -381,6 +385,7 @@ export function processRawChatHistory(
|
||||
message: messageInfo.message,
|
||||
type: messageInfo.message_type as "user" | "assistant",
|
||||
files: messageInfo.files,
|
||||
alternateAssistantID: Number(messageInfo.alternate_assistant_id),
|
||||
// only include these fields if this is an assistant message so that
|
||||
// this is identical to what is computed at streaming time
|
||||
...(messageInfo.message_type === "assistant"
|
||||
@ -496,6 +501,32 @@ export function removeMessage(
|
||||
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) {
|
||||
return selectedPersona.num_chunks !== 0;
|
||||
}
|
||||
|
@ -38,6 +38,9 @@ import Prism from "prismjs";
|
||||
|
||||
import "prismjs/themes/prism-tomorrow.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 = [
|
||||
SEARCH_TOOL_NAME,
|
||||
@ -80,6 +83,7 @@ function FileDisplay({ files }: { files: FileDescriptor[] }) {
|
||||
}
|
||||
|
||||
export const AIMessage = ({
|
||||
alternativeAssistant,
|
||||
messageId,
|
||||
content,
|
||||
files,
|
||||
@ -95,7 +99,10 @@ export const AIMessage = ({
|
||||
handleSearchQueryEdit,
|
||||
handleForceSearch,
|
||||
retrievalDisabled,
|
||||
currentPersona,
|
||||
}: {
|
||||
alternativeAssistant?: Persona | null;
|
||||
currentPersona: Persona;
|
||||
messageId: number | null;
|
||||
content: string | JSX.Element;
|
||||
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="ml-8">
|
||||
<div className="flex">
|
||||
<div className="p-1 bg-ai rounded-lg h-fit my-auto">
|
||||
<div className="text-inverted">
|
||||
<FiCpu size={16} className="my-auto mx-auto" />
|
||||
</div>
|
||||
</div>
|
||||
<AssistantIcon
|
||||
size="small"
|
||||
assistant={alternativeAssistant || currentPersona}
|
||||
/>
|
||||
|
||||
<div className="font-bold text-emphasis ml-2 my-auto">
|
||||
{personaName || "Danswer"}
|
||||
{alternativeAssistant
|
||||
? alternativeAssistant.name
|
||||
: personaName || "Danswer"}
|
||||
</div>
|
||||
|
||||
{query === undefined &&
|
||||
@ -519,12 +527,6 @@ export const HumanMessage = ({
|
||||
handleEditSubmit();
|
||||
}
|
||||
}}
|
||||
// ref={(textarea) => {
|
||||
// if (textarea) {
|
||||
// textarea.selectionStart = textarea.selectionEnd =
|
||||
// textarea.value.length;
|
||||
// }
|
||||
// }}
|
||||
/>
|
||||
<div className="flex justify-end mt-2 gap-2 pr-4">
|
||||
<button
|
||||
|
@ -41,7 +41,6 @@ export default async function Page({
|
||||
return (
|
||||
<>
|
||||
<InstantSSRAutoRefresh />
|
||||
|
||||
{shouldShowWelcomeModal && <WelcomeModal user={user} />}
|
||||
{!shouldShowWelcomeModal && !shouldDisplaySourcesIncompleteModal && (
|
||||
<ApiKeyModal user={user} />
|
||||
@ -49,7 +48,6 @@ export default async function Page({
|
||||
{shouldDisplaySourcesIncompleteModal && (
|
||||
<NoCompleteSourcesModal ccPairs={ccPairs} />
|
||||
)}
|
||||
|
||||
<ChatProvider
|
||||
value={{
|
||||
user,
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
import { AIMessage, HumanMessage } from "../../message/Messages";
|
||||
import { Button, Callout, Divider } from "@tremor/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useChatContext } from "@/components/context/ChatContext";
|
||||
|
||||
function BackToDanswerButton() {
|
||||
const router = useRouter();
|
||||
@ -30,6 +31,7 @@ export function SharedChatDisplay({
|
||||
}: {
|
||||
chatSession: BackendChatSession | null;
|
||||
}) {
|
||||
let { availablePersonas } = useChatContext();
|
||||
if (!chatSession) {
|
||||
return (
|
||||
<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(
|
||||
processRawChatHistory(chatSession.messages)
|
||||
);
|
||||
@ -77,6 +83,7 @@ export function SharedChatDisplay({
|
||||
} else {
|
||||
return (
|
||||
<AIMessage
|
||||
currentPersona={currentPersona!}
|
||||
key={message.messageId}
|
||||
messageId={message.messageId}
|
||||
content={message.message}
|
||||
|
@ -90,6 +90,7 @@ export default async function Home() {
|
||||
}
|
||||
|
||||
let personas: Persona[] = [];
|
||||
|
||||
if (personaResponse?.ok) {
|
||||
personas = await personaResponse.json();
|
||||
} else {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import React from "react";
|
||||
|
||||
function generatePastelColorFromId(id: string): string {
|
||||
export function generatePastelColorFromId(id: string): string {
|
||||
const hash = Array.from(id).reduce(
|
||||
(acc, char) => acc + char.charCodeAt(0),
|
||||
0
|
||||
@ -12,16 +12,25 @@ function generatePastelColorFromId(id: string): string {
|
||||
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());
|
||||
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
w-10
|
||||
h-10
|
||||
rounded-lg
|
||||
"
|
||||
className={`
|
||||
${border && " border border-.5 border-border-strong "}
|
||||
${(!size || size == "large") && "w-6 h-6"}
|
||||
${size == "small" && "w-6 h-6"}
|
||||
rounded-lg
|
||||
`}
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
);
|
||||
|
@ -27,7 +27,7 @@ export const HealthCheckBanner = () => {
|
||||
</p>
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
@ -36,7 +36,7 @@ export const HealthCheckBanner = () => {
|
||||
);
|
||||
} else {
|
||||
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="px-1">
|
||||
|
@ -29,6 +29,7 @@ export function Tooltip({
|
||||
side={side}
|
||||
align={align}
|
||||
className="
|
||||
|
||||
bg-background-inverted
|
||||
text-inverted
|
||||
text-sm
|
||||
@ -36,7 +37,7 @@ export function Tooltip({
|
||||
py-1
|
||||
px-2
|
||||
shadow-lg
|
||||
z-50
|
||||
z-100
|
||||
"
|
||||
>
|
||||
{content}
|
||||
|
@ -39,14 +39,17 @@ module.exports = {
|
||||
colors: {
|
||||
// background
|
||||
background: "#f9fafb", // gray-50
|
||||
"background-subtle": "#e5e7eb", // gray-200
|
||||
"background-emphasis": "#f6f7f8",
|
||||
"background-strong": "#eaecef",
|
||||
"background-search": "#ffffff",
|
||||
"background-custom-header": "#f3f4f6",
|
||||
"background-inverted": "#000000",
|
||||
"background-weak": "#f3f4f6", // gray-100
|
||||
"background-dark": "#111827", // gray-900
|
||||
|
||||
// text or icons
|
||||
light: "#e5e7eb", // gray-200
|
||||
link: "#3b82f6", // blue-500
|
||||
"link-hover": "#1d4ed8", // blue-700
|
||||
subtle: "#6b7280", // gray-500
|
||||
|
Loading…
x
Reference in New Issue
Block a user