mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-06-13 09:30:53 +02: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
|
# 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
|
@ -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 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
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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]
|
||||||
|
@ -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);
|
||||||
|
@ -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`}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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}
|
||||||
|
@ -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 {
|
||||||
|
@ -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 }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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">
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user