mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-17 13:22:42 +01:00
Updated chat flow (#3244)
* proper no assistant typing + no assistant modal * updated chat flow * k * updates * update * k * clean up * fix mystery reorg * cleanup * update scroll * default * update logs * push fade * scroll nit * finalize tags * updates * k * various updates * viewport height update * source types update * clean up unused components * minor cleanup * cleanup complete * finalize changes * badge up * update filters * small nit * k * k * address comments * quick unification of icons * minor date range clarity * minor nit * k * update sidebar line * update for all screen sizes * k * k * k * k * rm shs * fix memoization * fix memoization * slack chat * k * k * build org
This commit is contained in:
parent
3432d932d1
commit
de66f7adb2
@ -0,0 +1,27 @@
|
||||
"""add auto scroll to user model
|
||||
|
||||
Revision ID: a8c2065484e6
|
||||
Revises: abe7378b8217
|
||||
Create Date: 2024-11-22 17:34:09.690295
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "a8c2065484e6"
|
||||
down_revision = "abe7378b8217"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"user",
|
||||
sa.Column("auto_scroll", sa.Boolean(), nullable=True, server_default=None),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("user", "auto_scroll")
|
@ -23,7 +23,9 @@ def load_no_auth_user_preferences(store: KeyValueStore) -> UserPreferences:
|
||||
)
|
||||
return UserPreferences(**preferences_data)
|
||||
except KvKeyNotFoundError:
|
||||
return UserPreferences(chosen_assistants=None, default_model=None)
|
||||
return UserPreferences(
|
||||
chosen_assistants=None, default_model=None, auto_scroll=True
|
||||
)
|
||||
|
||||
|
||||
def fetch_no_auth_user(store: KeyValueStore) -> UserInfo:
|
||||
|
@ -605,6 +605,7 @@ def stream_chat_message_objects(
|
||||
additional_headers=custom_tool_additional_headers,
|
||||
),
|
||||
)
|
||||
|
||||
tools: list[Tool] = []
|
||||
for tool_list in tool_dict.values():
|
||||
tools.extend(tool_list)
|
||||
|
@ -126,6 +126,7 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||
|
||||
# if specified, controls the assistants that are shown to the user + their order
|
||||
# if not specified, all assistants are shown
|
||||
auto_scroll: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
chosen_assistants: Mapped[list[int] | None] = mapped_column(
|
||||
postgresql.JSONB(), nullable=True, default=None
|
||||
)
|
||||
|
@ -5,7 +5,7 @@ personas:
|
||||
# this is for DanswerBot to use when tagged in a non-configured channel
|
||||
# Careful setting specific IDs, this won't autoincrement the next ID value for postgres
|
||||
- id: 0
|
||||
name: "Knowledge"
|
||||
name: "Search"
|
||||
description: >
|
||||
Assistant with access to documents from your Connected Sources.
|
||||
# Default Prompt objects attached to the persona, see prompts.yaml
|
||||
|
@ -45,6 +45,7 @@ class UserPreferences(BaseModel):
|
||||
visible_assistants: list[int] = []
|
||||
recent_assistants: list[int] | None = None
|
||||
default_model: str | None = None
|
||||
auto_scroll: bool | None = None
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
@ -79,6 +80,7 @@ class UserInfo(BaseModel):
|
||||
role=user.role,
|
||||
preferences=(
|
||||
UserPreferences(
|
||||
auto_scroll=user.auto_scroll,
|
||||
chosen_assistants=user.chosen_assistants,
|
||||
default_model=user.default_model,
|
||||
hidden_assistants=user.hidden_assistants,
|
||||
@ -128,6 +130,10 @@ class HiddenUpdateRequest(BaseModel):
|
||||
hidden: bool
|
||||
|
||||
|
||||
class AutoScrollRequest(BaseModel):
|
||||
auto_scroll: bool | None
|
||||
|
||||
|
||||
class SlackBotCreationRequest(BaseModel):
|
||||
name: str
|
||||
enabled: bool
|
||||
|
@ -52,6 +52,7 @@ from danswer.db.users import list_users
|
||||
from danswer.db.users import validate_user_role_update
|
||||
from danswer.key_value_store.factory import get_kv_store
|
||||
from danswer.server.manage.models import AllUsersResponse
|
||||
from danswer.server.manage.models import AutoScrollRequest
|
||||
from danswer.server.manage.models import UserByEmail
|
||||
from danswer.server.manage.models import UserInfo
|
||||
from danswer.server.manage.models import UserPreferences
|
||||
@ -497,7 +498,6 @@ def verify_user_logged_in(
|
||||
return fetch_no_auth_user(store)
|
||||
|
||||
raise BasicAuthenticationError(detail="User Not Authenticated")
|
||||
|
||||
if user.oidc_expiry and user.oidc_expiry < datetime.now(timezone.utc):
|
||||
raise BasicAuthenticationError(
|
||||
detail="Access denied. User's OIDC token has expired.",
|
||||
@ -581,6 +581,30 @@ def update_user_recent_assistants(
|
||||
db_session.commit()
|
||||
|
||||
|
||||
@router.patch("/auto-scroll")
|
||||
def update_user_auto_scroll(
|
||||
request: AutoScrollRequest,
|
||||
user: User | None = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> None:
|
||||
if user is None:
|
||||
if AUTH_TYPE == AuthType.DISABLED:
|
||||
store = get_kv_store()
|
||||
no_auth_user = fetch_no_auth_user(store)
|
||||
no_auth_user.preferences.auto_scroll = request.auto_scroll
|
||||
set_no_auth_user_preferences(store, no_auth_user.preferences)
|
||||
return
|
||||
else:
|
||||
raise RuntimeError("This should never happen")
|
||||
|
||||
db_session.execute(
|
||||
update(User)
|
||||
.where(User.id == user.id) # type: ignore
|
||||
.values(auto_scroll=request.auto_scroll)
|
||||
)
|
||||
db_session.commit()
|
||||
|
||||
|
||||
@router.patch("/user/default-model")
|
||||
def update_user_default_model(
|
||||
request: ChosenDefaultModelRequest,
|
||||
|
@ -79,6 +79,7 @@ class CreateChatMessageRequest(ChunkContext):
|
||||
message: str
|
||||
# Files that we should attach to this message
|
||||
file_descriptors: list[FileDescriptor]
|
||||
|
||||
# If no prompt provided, uses the largest prompt of the chat session
|
||||
# but really this should be explicitly specified, only in the simplified APIs is this inferred
|
||||
# Use prompt_id 0 to use the system default prompt which is Answer-Question
|
||||
|
@ -2,7 +2,6 @@ from typing import cast
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@ -38,10 +37,6 @@ basic_router = APIRouter(prefix="/settings")
|
||||
def put_settings(
|
||||
settings: Settings, _: User | None = Depends(current_admin_user)
|
||||
) -> None:
|
||||
try:
|
||||
settings.check_validity()
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
store_settings(settings)
|
||||
|
||||
|
||||
|
@ -41,33 +41,10 @@ class Notification(BaseModel):
|
||||
class Settings(BaseModel):
|
||||
"""General settings"""
|
||||
|
||||
chat_page_enabled: bool = True
|
||||
search_page_enabled: bool = True
|
||||
default_page: PageType = PageType.SEARCH
|
||||
maximum_chat_retention_days: int | None = None
|
||||
gpu_enabled: bool | None = None
|
||||
product_gating: GatingType = GatingType.NONE
|
||||
|
||||
def check_validity(self) -> None:
|
||||
chat_page_enabled = self.chat_page_enabled
|
||||
search_page_enabled = self.search_page_enabled
|
||||
default_page = self.default_page
|
||||
|
||||
if chat_page_enabled is False and search_page_enabled is False:
|
||||
raise ValueError(
|
||||
"One of `search_page_enabled` and `chat_page_enabled` must be True."
|
||||
)
|
||||
|
||||
if default_page == PageType.CHAT and chat_page_enabled is False:
|
||||
raise ValueError(
|
||||
"The default page cannot be 'chat' if the chat page is disabled."
|
||||
)
|
||||
|
||||
if default_page == PageType.SEARCH and search_page_enabled is False:
|
||||
raise ValueError(
|
||||
"The default page cannot be 'search' if the search page is disabled."
|
||||
)
|
||||
|
||||
|
||||
class UserSettings(Settings):
|
||||
notifications: list[Notification]
|
||||
|
@ -113,10 +113,6 @@ async def refresh_access_token(
|
||||
def put_settings(
|
||||
settings: EnterpriseSettings, _: User | None = Depends(current_admin_user)
|
||||
) -> None:
|
||||
try:
|
||||
settings.check_validity()
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
store_settings(settings)
|
||||
|
||||
|
||||
|
@ -157,7 +157,6 @@ def _seed_personas(db_session: Session, personas: list[CreatePersonaRequest]) ->
|
||||
def _seed_settings(settings: Settings) -> None:
|
||||
logger.notice("Seeding Settings")
|
||||
try:
|
||||
settings.check_validity()
|
||||
store_base_settings(settings)
|
||||
logger.notice("Successfully seeded Settings")
|
||||
except ValueError as e:
|
||||
|
9
web/@types/favicon-fetch.d.ts
vendored
Normal file
9
web/@types/favicon-fetch.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
declare module "favicon-fetch" {
|
||||
interface FaviconFetchOptions {
|
||||
uri: string;
|
||||
}
|
||||
|
||||
function faviconFetch(options: FaviconFetchOptions): string | null;
|
||||
|
||||
export default faviconFetch;
|
||||
}
|
1007
web/package-lock.json
generated
1007
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,11 +17,13 @@
|
||||
"@headlessui/react": "^2.2.0",
|
||||
"@headlessui/tailwindcss": "^0.2.1",
|
||||
"@phosphor-icons/react": "^2.0.8",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.3",
|
||||
"@sentry/nextjs": "^8.34.0",
|
||||
@ -37,6 +39,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"favicon-fetch": "^1.0.0",
|
||||
"formik": "^2.2.9",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
@ -67,6 +70,7 @@
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "5.0.3",
|
||||
"uuid": "^9.0.1",
|
||||
"vaul": "^1.1.1",
|
||||
"yup": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -83,7 +83,7 @@ const EditRow = ({
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{!documentSet.is_up_to_date && (
|
||||
<TooltipContent maxWidth="max-w-sm">
|
||||
<TooltipContent width="max-w-sm">
|
||||
<div className="flex break-words break-keep whitespace-pre-wrap items-start">
|
||||
<InfoIcon className="mr-2 mt-0.5" />
|
||||
Cannot update while syncing! Wait for the sync to finish, then
|
||||
|
@ -175,29 +175,6 @@ export function SettingsForm() {
|
||||
{ fieldName, newValue: checked },
|
||||
];
|
||||
|
||||
// If we're disabling a page, check if we need to update the default page
|
||||
if (
|
||||
!checked &&
|
||||
(fieldName === "search_page_enabled" || fieldName === "chat_page_enabled")
|
||||
) {
|
||||
const otherPageField =
|
||||
fieldName === "search_page_enabled"
|
||||
? "chat_page_enabled"
|
||||
: "search_page_enabled";
|
||||
const otherPageEnabled = settings && settings[otherPageField];
|
||||
|
||||
if (
|
||||
otherPageEnabled &&
|
||||
settings?.default_page ===
|
||||
(fieldName === "search_page_enabled" ? "search" : "chat")
|
||||
) {
|
||||
updates.push({
|
||||
fieldName: "default_page",
|
||||
newValue: fieldName === "search_page_enabled" ? "chat" : "search",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateSettingField(updates);
|
||||
}
|
||||
|
||||
@ -218,42 +195,17 @@ export function SettingsForm() {
|
||||
return (
|
||||
<div>
|
||||
{popup}
|
||||
<Title className="mb-4">Page Visibility</Title>
|
||||
<Title className="mb-4">Workspace Settings</Title>
|
||||
|
||||
<Checkbox
|
||||
label="Search Page Enabled?"
|
||||
sublabel="If set, then the 'Search' page will be accessible to all users and will show up as an option on the top navbar. If unset, then this page will not be available."
|
||||
checked={settings.search_page_enabled}
|
||||
label="Auto-scroll"
|
||||
sublabel="If set, the chat window will automatically scroll to the bottom as new lines of text are generated by the AI model."
|
||||
checked={settings.auto_scroll}
|
||||
onChange={(e) =>
|
||||
handleToggleSettingsField("search_page_enabled", e.target.checked)
|
||||
handleToggleSettingsField("auto_scroll", e.target.checked)
|
||||
}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
label="Chat Page Enabled?"
|
||||
sublabel="If set, then the 'Chat' page will be accessible to all users and will show up as an option on the top navbar. If unset, then this page will not be available."
|
||||
checked={settings.chat_page_enabled}
|
||||
onChange={(e) =>
|
||||
handleToggleSettingsField("chat_page_enabled", e.target.checked)
|
||||
}
|
||||
/>
|
||||
|
||||
<Selector
|
||||
label="Default Page"
|
||||
subtext="The page that users will be redirected to after logging in. Can only be set to a page that is enabled."
|
||||
options={[
|
||||
{ value: "search", name: "Search" },
|
||||
{ value: "chat", name: "Chat" },
|
||||
]}
|
||||
selected={settings.default_page}
|
||||
onSelect={(value) => {
|
||||
value &&
|
||||
updateSettingField([
|
||||
{ fieldName: "default_page", newValue: value },
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
|
||||
{isEnterpriseEnabled && (
|
||||
<>
|
||||
<Title className="mb-4">Chat Settings</Title>
|
||||
|
@ -5,14 +5,12 @@ export enum GatingType {
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
chat_page_enabled: boolean;
|
||||
search_page_enabled: boolean;
|
||||
default_page: "search" | "chat";
|
||||
maximum_chat_retention_days: number | null;
|
||||
notifications: Notification[];
|
||||
needs_reindexing: boolean;
|
||||
gpu_enabled: boolean;
|
||||
product_gating: GatingType;
|
||||
auto_scroll: boolean;
|
||||
}
|
||||
|
||||
export enum NotificationType {
|
||||
@ -54,6 +52,7 @@ export interface EnterpriseSettings {
|
||||
custom_popup_header: string | null;
|
||||
custom_popup_content: string | null;
|
||||
enable_consent_screen: boolean | null;
|
||||
auto_scroll: boolean;
|
||||
}
|
||||
|
||||
export interface CombinedSettings {
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
ChatFileType,
|
||||
ChatSession,
|
||||
ChatSessionSharedStatus,
|
||||
DocumentsResponse,
|
||||
FileDescriptor,
|
||||
FileChatDisplay,
|
||||
Message,
|
||||
@ -60,7 +59,7 @@ import { useDocumentSelection } from "./useDocumentSelection";
|
||||
import { LlmOverride, useFilters, useLlmOverride } from "@/lib/hooks";
|
||||
import { computeAvailableFilters } from "@/lib/filters";
|
||||
import { ChatState, FeedbackType, RegenerationState } from "./types";
|
||||
import { DocumentSidebar } from "./documentSidebar/DocumentSidebar";
|
||||
import { ChatFilters } from "./documentSidebar/ChatFilters";
|
||||
import { DanswerInitializingLoader } from "@/components/DanswerInitializingLoader";
|
||||
import { FeedbackModal } from "./modal/FeedbackModal";
|
||||
import { ShareChatSessionModal } from "./modal/ShareChatSessionModal";
|
||||
@ -71,6 +70,7 @@ import { StarterMessages } from "../../components/assistants/StarterMessage";
|
||||
import {
|
||||
AnswerPiecePacket,
|
||||
DanswerDocument,
|
||||
FinalContextDocs,
|
||||
StreamStopInfo,
|
||||
StreamStopReason,
|
||||
} from "@/lib/search/interfaces";
|
||||
@ -105,14 +105,9 @@ import BlurBackground from "./shared_chat_search/BlurBackground";
|
||||
import { NoAssistantModal } from "@/components/modals/NoAssistantModal";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
} from "@/components/ui/card";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import AssistantBanner from "../../components/assistants/AssistantBanner";
|
||||
import AssistantSelector from "@/components/chat_search/AssistantSelector";
|
||||
import { Modal } from "@/components/Modal";
|
||||
|
||||
const TEMP_USER_MESSAGE_ID = -1;
|
||||
const TEMP_ASSISTANT_MESSAGE_ID = -2;
|
||||
@ -132,8 +127,9 @@ export function ChatPage({
|
||||
|
||||
const {
|
||||
chatSessions,
|
||||
availableSources,
|
||||
availableDocumentSets,
|
||||
ccPairs,
|
||||
tags,
|
||||
documentSets,
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
@ -142,6 +138,36 @@ export function ChatPage({
|
||||
shouldShowWelcomeModal,
|
||||
refreshChatSessions,
|
||||
} = useChatContext();
|
||||
function useScreenSize() {
|
||||
const [screenSize, setScreenSize] = useState({
|
||||
width: typeof window !== "undefined" ? window.innerWidth : 0,
|
||||
height: typeof window !== "undefined" ? window.innerHeight : 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setScreenSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
return screenSize;
|
||||
}
|
||||
|
||||
const { height: screenHeight } = useScreenSize();
|
||||
|
||||
const getContainerHeight = () => {
|
||||
if (autoScrollEnabled) return undefined;
|
||||
|
||||
if (screenHeight < 600) return "20vh";
|
||||
if (screenHeight < 1200) return "30vh";
|
||||
return "40vh";
|
||||
};
|
||||
|
||||
// handle redirect if chat page is disabled
|
||||
// NOTE: this must be done here, in a client component since
|
||||
@ -149,9 +175,11 @@ export function ChatPage({
|
||||
// available in server-side components
|
||||
const settings = useContext(SettingsContext);
|
||||
const enterpriseSettings = settings?.enterpriseSettings;
|
||||
if (settings?.settings?.chat_page_enabled === false) {
|
||||
router.push("/search");
|
||||
}
|
||||
|
||||
const [documentSidebarToggled, setDocumentSidebarToggled] = useState(false);
|
||||
const [filtersToggled, setFiltersToggled] = useState(false);
|
||||
|
||||
const [userSettingsToggled, setUserSettingsToggled] = useState(false);
|
||||
|
||||
const { assistants: availableAssistants, finalAssistants } = useAssistants();
|
||||
|
||||
@ -159,16 +187,13 @@ export function ChatPage({
|
||||
!shouldShowWelcomeModal
|
||||
);
|
||||
|
||||
const { user, isAdmin, isLoadingUser, refreshUser } = useUser();
|
||||
|
||||
const { user, isAdmin, isLoadingUser } = useUser();
|
||||
const slackChatId = searchParams.get("slackChatId");
|
||||
|
||||
const existingChatIdRaw = searchParams.get("chatId");
|
||||
const [sendOnLoad, setSendOnLoad] = useState<string | null>(
|
||||
searchParams.get(SEARCH_PARAM_NAMES.SEND_ON_LOAD)
|
||||
);
|
||||
|
||||
const currentPersonaId = searchParams.get(SEARCH_PARAM_NAMES.PERSONA_ID);
|
||||
const modelVersionFromSearchParams = searchParams.get(
|
||||
SEARCH_PARAM_NAMES.STRUCTURED_MODEL
|
||||
);
|
||||
@ -261,7 +286,7 @@ export function ChatPage({
|
||||
refreshRecentAssistants,
|
||||
} = useAssistants();
|
||||
|
||||
const liveAssistant =
|
||||
const liveAssistant: Persona | undefined =
|
||||
alternativeAssistant ||
|
||||
selectedAssistant ||
|
||||
recentAssistants[0] ||
|
||||
@ -269,8 +294,20 @@ export function ChatPage({
|
||||
availableAssistants[0];
|
||||
|
||||
const noAssistants = liveAssistant == null || liveAssistant == undefined;
|
||||
|
||||
const availableSources = ccPairs.map((ccPair) => ccPair.source);
|
||||
const [finalAvailableSources, finalAvailableDocumentSets] =
|
||||
computeAvailableFilters({
|
||||
selectedPersona: availableAssistants.find(
|
||||
(assistant) => assistant.id === liveAssistant?.id
|
||||
),
|
||||
availableSources: availableSources,
|
||||
availableDocumentSets: documentSets,
|
||||
});
|
||||
|
||||
// always set the model override for the chat session, when an assistant, llm provider, or user preference exists
|
||||
useEffect(() => {
|
||||
if (noAssistants) return;
|
||||
const personaDefault = getLLMProviderOverrideForPersona(
|
||||
liveAssistant,
|
||||
llmProviders
|
||||
@ -357,9 +394,7 @@ export function ChatPage({
|
||||
textAreaRef.current?.focus();
|
||||
|
||||
// only clear things if we're going from one chat session to another
|
||||
const isChatSessionSwitch =
|
||||
chatSessionIdRef.current !== null &&
|
||||
existingChatSessionId !== priorChatSessionId;
|
||||
const isChatSessionSwitch = existingChatSessionId !== priorChatSessionId;
|
||||
if (isChatSessionSwitch) {
|
||||
// de-select documents
|
||||
clearSelectedDocuments();
|
||||
@ -449,9 +484,9 @@ export function ChatPage({
|
||||
}
|
||||
|
||||
if (shouldScrollToBottom) {
|
||||
if (!hasPerformedInitialScroll) {
|
||||
if (!hasPerformedInitialScroll && autoScrollEnabled) {
|
||||
clientScrollToBottom();
|
||||
} else if (isChatSessionSwitch) {
|
||||
} else if (isChatSessionSwitch && autoScrollEnabled) {
|
||||
clientScrollToBottom(true);
|
||||
}
|
||||
}
|
||||
@ -759,7 +794,7 @@ export function ChatPage({
|
||||
useEffect(() => {
|
||||
async function fetchMaxTokens() {
|
||||
const response = await fetch(
|
||||
`/api/chat/max-selected-document-tokens?persona_id=${liveAssistant.id}`
|
||||
`/api/chat/max-selected-document-tokens?persona_id=${liveAssistant?.id}`
|
||||
);
|
||||
if (response.ok) {
|
||||
const maxTokens = (await response.json()).max_tokens as number;
|
||||
@ -833,11 +868,13 @@ export function ChatPage({
|
||||
0
|
||||
)}px`;
|
||||
|
||||
scrollableDivRef?.current.scrollBy({
|
||||
left: 0,
|
||||
top: Math.max(heightDifference, 0),
|
||||
behavior: "smooth",
|
||||
});
|
||||
if (autoScrollEnabled) {
|
||||
scrollableDivRef?.current.scrollBy({
|
||||
left: 0,
|
||||
top: Math.max(heightDifference, 0),
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
previousHeight.current = newHeight;
|
||||
}
|
||||
@ -884,6 +921,7 @@ export function ChatPage({
|
||||
endDivRef.current.scrollIntoView({
|
||||
behavior: fast ? "auto" : "smooth",
|
||||
});
|
||||
|
||||
setHasPerformedInitialScroll(true);
|
||||
}
|
||||
}, 50);
|
||||
@ -1035,7 +1073,9 @@ export function ChatPage({
|
||||
}
|
||||
|
||||
setAlternativeGeneratingAssistant(alternativeAssistantOverride);
|
||||
|
||||
clientScrollToBottom();
|
||||
|
||||
let currChatSessionId: string;
|
||||
const isNewSession = chatSessionIdRef.current === null;
|
||||
const searchParamBasedChatSessionName =
|
||||
@ -1281,8 +1321,8 @@ export function ChatPage({
|
||||
|
||||
if (Object.hasOwn(packet, "answer_piece")) {
|
||||
answer += (packet as AnswerPiecePacket).answer_piece;
|
||||
} else if (Object.hasOwn(packet, "top_documents")) {
|
||||
documents = (packet as DocumentsResponse).top_documents;
|
||||
} else if (Object.hasOwn(packet, "final_context_docs")) {
|
||||
documents = (packet as FinalContextDocs).final_context_docs;
|
||||
retrievalType = RetrievalType.Search;
|
||||
if (documents && documents.length > 0) {
|
||||
// point to the latest message (we don't know the messageId yet, which is why
|
||||
@ -1379,8 +1419,7 @@ export function ChatPage({
|
||||
type: error ? "error" : "assistant",
|
||||
retrievalType,
|
||||
query: finalMessage?.rephrased_query || query,
|
||||
documents:
|
||||
finalMessage?.context_docs?.top_documents || documents,
|
||||
documents: documents,
|
||||
citations: finalMessage?.citations || {},
|
||||
files: finalMessage?.files || aiMessageImages || [],
|
||||
toolCall: finalMessage?.tool_call || toolCall,
|
||||
@ -1599,6 +1638,11 @@ export function ChatPage({
|
||||
mobile: settings?.isMobile,
|
||||
});
|
||||
|
||||
const autoScrollEnabled =
|
||||
user?.preferences?.auto_scroll == null
|
||||
? settings?.enterpriseSettings?.auto_scroll || false
|
||||
: user?.preferences?.auto_scroll!;
|
||||
|
||||
useScrollonStream({
|
||||
chatState: currentSessionChatState,
|
||||
scrollableDivRef,
|
||||
@ -1607,6 +1651,7 @@ export function ChatPage({
|
||||
debounceNumber,
|
||||
waitForScrollRef,
|
||||
mobile: settings?.isMobile,
|
||||
enableAutoScroll: autoScrollEnabled,
|
||||
});
|
||||
|
||||
// Virtualization + Scrolling related effects and functions
|
||||
@ -1756,6 +1801,13 @@ export function ChatPage({
|
||||
liveAssistant
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!retrievalEnabled) {
|
||||
setDocumentSidebarToggled(false);
|
||||
}
|
||||
}, [retrievalEnabled]);
|
||||
|
||||
const [stackTraceModalContent, setStackTraceModalContent] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
@ -1764,58 +1816,6 @@ export function ChatPage({
|
||||
const [settingsToggled, setSettingsToggled] = useState(false);
|
||||
|
||||
const currentPersona = alternativeAssistant || liveAssistant;
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
switch (event.key.toLowerCase()) {
|
||||
case "e":
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router]);
|
||||
const [sharedChatSession, setSharedChatSession] =
|
||||
useState<ChatSession | null>();
|
||||
const [deletingChatSession, setDeletingChatSession] =
|
||||
useState<ChatSession | null>();
|
||||
|
||||
const showDeleteModal = (chatSession: ChatSession) => {
|
||||
setDeletingChatSession(chatSession);
|
||||
};
|
||||
const showShareModal = (chatSession: ChatSession) => {
|
||||
setSharedChatSession(chatSession);
|
||||
};
|
||||
const [documentSelection, setDocumentSelection] = useState(false);
|
||||
const toggleDocumentSelectionAspects = () => {
|
||||
setDocumentSelection((documentSelection) => !documentSelection);
|
||||
setShowDocSidebar(false);
|
||||
};
|
||||
|
||||
interface RegenerationRequest {
|
||||
messageId: number;
|
||||
parentMessage: Message;
|
||||
}
|
||||
|
||||
function createRegenerator(regenerationRequest: RegenerationRequest) {
|
||||
// Returns new function that only needs `modelOverRide` to be specified when called
|
||||
return async function (modelOverRide: LlmOverride) {
|
||||
return await onSubmit({
|
||||
modelOverRide,
|
||||
messageIdToResend: regenerationRequest.parentMessage.messageId,
|
||||
regenerationRequest,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleSlackChatRedirect = async () => {
|
||||
if (!slackChatId) return;
|
||||
@ -1851,18 +1851,94 @@ export function ChatPage({
|
||||
|
||||
handleSlackChatRedirect();
|
||||
}, [searchParams, router]);
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
switch (event.key.toLowerCase()) {
|
||||
case "e":
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router]);
|
||||
const [sharedChatSession, setSharedChatSession] =
|
||||
useState<ChatSession | null>();
|
||||
const [deletingChatSession, setDeletingChatSession] =
|
||||
useState<ChatSession | null>();
|
||||
|
||||
const showDeleteModal = (chatSession: ChatSession) => {
|
||||
setDeletingChatSession(chatSession);
|
||||
};
|
||||
const showShareModal = (chatSession: ChatSession) => {
|
||||
setSharedChatSession(chatSession);
|
||||
};
|
||||
const [documentSelection, setDocumentSelection] = useState(false);
|
||||
// const toggleDocumentSelectionAspects = () => {
|
||||
// setDocumentSelection((documentSelection) => !documentSelection);
|
||||
// setShowDocSidebar(false);
|
||||
// };
|
||||
|
||||
const toggleDocumentSidebar = () => {
|
||||
if (!documentSidebarToggled) {
|
||||
setFiltersToggled(false);
|
||||
setDocumentSidebarToggled(true);
|
||||
} else if (!filtersToggled) {
|
||||
setDocumentSidebarToggled(false);
|
||||
} else {
|
||||
setFiltersToggled(false);
|
||||
}
|
||||
};
|
||||
const toggleFilters = () => {
|
||||
if (!documentSidebarToggled) {
|
||||
setFiltersToggled(true);
|
||||
setDocumentSidebarToggled(true);
|
||||
} else if (filtersToggled) {
|
||||
setDocumentSidebarToggled(false);
|
||||
} else {
|
||||
setFiltersToggled(true);
|
||||
}
|
||||
};
|
||||
|
||||
interface RegenerationRequest {
|
||||
messageId: number;
|
||||
parentMessage: Message;
|
||||
}
|
||||
|
||||
function createRegenerator(regenerationRequest: RegenerationRequest) {
|
||||
// Returns new function that only needs `modelOverRide` to be specified when called
|
||||
return async function (modelOverRide: LlmOverride) {
|
||||
return await onSubmit({
|
||||
modelOverRide,
|
||||
messageIdToResend: regenerationRequest.parentMessage.messageId,
|
||||
regenerationRequest,
|
||||
});
|
||||
};
|
||||
}
|
||||
if (noAssistants)
|
||||
return (
|
||||
<>
|
||||
<HealthCheckBanner />
|
||||
<NoAssistantModal isAdmin={isAdmin} />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HealthCheckBanner />
|
||||
|
||||
{showApiKeyModal && !shouldShowWelcomeModal ? (
|
||||
{showApiKeyModal && !shouldShowWelcomeModal && (
|
||||
<ApiKeyModal
|
||||
hide={() => setShowApiKeyModal(false)}
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
) : (
|
||||
noAssistants && <NoAssistantModal isAdmin={isAdmin} />
|
||||
)}
|
||||
|
||||
{/* ChatPopup is a custom popup that displays a admin-specified message on initial user visit.
|
||||
@ -1886,16 +1962,46 @@ export function ChatPage({
|
||||
/>
|
||||
)}
|
||||
|
||||
{settingsToggled && (
|
||||
{(settingsToggled || userSettingsToggled) && (
|
||||
<SetDefaultModelModal
|
||||
setPopup={setPopup}
|
||||
setLlmOverride={llmOverrideManager.setGlobalDefault}
|
||||
defaultModel={user?.preferences.default_model!}
|
||||
llmProviders={llmProviders}
|
||||
onClose={() => setSettingsToggled(false)}
|
||||
onClose={() => {
|
||||
setUserSettingsToggled(false);
|
||||
setSettingsToggled(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{retrievalEnabled && documentSidebarToggled && settings?.isMobile && (
|
||||
<div className="md:hidden">
|
||||
<Modal noPadding noScroll>
|
||||
<ChatFilters
|
||||
modal={true}
|
||||
filterManager={filterManager}
|
||||
ccPairs={ccPairs}
|
||||
tags={tags}
|
||||
documentSets={documentSets}
|
||||
ref={innerSidebarElementRef}
|
||||
showFilters={filtersToggled}
|
||||
closeSidebar={() => {
|
||||
setDocumentSidebarToggled(false);
|
||||
}}
|
||||
selectedMessage={aiMessage}
|
||||
selectedDocuments={selectedDocuments}
|
||||
toggleDocumentSelection={toggleDocumentSelection}
|
||||
clearSelectedDocuments={clearSelectedDocuments}
|
||||
selectedDocumentTokens={selectedDocumentTokens}
|
||||
maxTokens={maxTokens}
|
||||
initialWidth={400}
|
||||
isOpen={true}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{deletingChatSession && (
|
||||
<DeleteEntityModal
|
||||
entityType="chat"
|
||||
@ -1996,6 +2102,50 @@ export function ChatPage({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!settings?.isMobile && retrievalEnabled && (
|
||||
<div
|
||||
style={{ transition: "width 0.30s ease-out" }}
|
||||
className={`
|
||||
flex-none
|
||||
fixed
|
||||
right-0
|
||||
z-[1000]
|
||||
|
||||
bg-background
|
||||
h-screen
|
||||
transition-all
|
||||
bg-opacity-80
|
||||
duration-300
|
||||
ease-in-out
|
||||
bg-transparent
|
||||
transition-all
|
||||
bg-opacity-80
|
||||
duration-300
|
||||
ease-in-out
|
||||
h-full
|
||||
${documentSidebarToggled ? "w-[400px]" : "w-[0px]"}
|
||||
`}
|
||||
>
|
||||
<ChatFilters
|
||||
modal={false}
|
||||
filterManager={filterManager}
|
||||
ccPairs={ccPairs}
|
||||
tags={tags}
|
||||
documentSets={documentSets}
|
||||
ref={innerSidebarElementRef}
|
||||
showFilters={filtersToggled}
|
||||
closeSidebar={() => setDocumentSidebarToggled(false)}
|
||||
selectedMessage={aiMessage}
|
||||
selectedDocuments={selectedDocuments}
|
||||
toggleDocumentSelection={toggleDocumentSelection}
|
||||
clearSelectedDocuments={clearSelectedDocuments}
|
||||
selectedDocumentTokens={selectedDocumentTokens}
|
||||
maxTokens={maxTokens}
|
||||
initialWidth={400}
|
||||
isOpen={documentSidebarToggled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<BlurBackground
|
||||
visible={!untoggled && (showDocSidebar || toggledSidebar)}
|
||||
@ -2005,9 +2155,12 @@ export function ChatPage({
|
||||
ref={masterFlexboxRef}
|
||||
className="flex h-full w-full overflow-x-hidden"
|
||||
>
|
||||
<div className="flex h-full flex-col w-full">
|
||||
<div className="flex h-full relative px-2 flex-col w-full">
|
||||
{liveAssistant && (
|
||||
<FunctionalHeader
|
||||
toggleUserSettings={() => setUserSettingsToggled(true)}
|
||||
liveAssistant={liveAssistant}
|
||||
onAssistantChange={onAssistantChange}
|
||||
sidebarToggled={toggledSidebar}
|
||||
reset={() => setMessage("")}
|
||||
page="chat"
|
||||
@ -2018,6 +2171,8 @@ export function ChatPage({
|
||||
}
|
||||
toggleSidebar={toggleSidebar}
|
||||
currentChatSession={selectedChatSession}
|
||||
documentSidebarToggled={documentSidebarToggled}
|
||||
llmOverrideManager={llmOverrideManager}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -2039,7 +2194,7 @@ export function ChatPage({
|
||||
duration-300
|
||||
ease-in-out
|
||||
h-full
|
||||
${toggledSidebar ? "w-[250px]" : "w-[0px]"}
|
||||
${toggledSidebar ? "w-[200px]" : "w-[0px]"}
|
||||
`}
|
||||
></div>
|
||||
)}
|
||||
@ -2049,9 +2204,55 @@ export function ChatPage({
|
||||
{...getRootProps()}
|
||||
>
|
||||
<div
|
||||
className={`w-full h-full flex flex-col default-scrollbar overflow-y-auto overflow-x-hidden relative`}
|
||||
className={`w-full h-[calc(100vh-160px)] flex flex-col default-scrollbar overflow-y-auto overflow-x-hidden relative`}
|
||||
ref={scrollableDivRef}
|
||||
>
|
||||
{liveAssistant && onAssistantChange && (
|
||||
<div className="z-20 fixed top-4 pointer-events-none left-0 w-full flex justify-center overflow-visible">
|
||||
{!settings?.isMobile && (
|
||||
<div
|
||||
style={{ transition: "width 0.30s ease-out" }}
|
||||
className={`
|
||||
flex-none
|
||||
overflow-y-hidden
|
||||
transition-all
|
||||
pointer-events-none
|
||||
duration-300
|
||||
ease-in-out
|
||||
h-full
|
||||
${toggledSidebar ? "w-[200px]" : "w-[0px]"}
|
||||
`}
|
||||
></div>
|
||||
)}
|
||||
|
||||
<AssistantSelector
|
||||
isMobile={settings?.isMobile!}
|
||||
liveAssistant={liveAssistant}
|
||||
onAssistantChange={onAssistantChange}
|
||||
llmOverrideManager={llmOverrideManager}
|
||||
/>
|
||||
{!settings?.isMobile && (
|
||||
<div
|
||||
style={{ transition: "width 0.30s ease-out" }}
|
||||
className={`
|
||||
flex-none
|
||||
overflow-y-hidden
|
||||
transition-all
|
||||
duration-300
|
||||
ease-in-out
|
||||
h-full
|
||||
pointer-events-none
|
||||
${
|
||||
documentSidebarToggled && retrievalEnabled
|
||||
? "w-[400px]"
|
||||
: "w-[0px]"
|
||||
}
|
||||
`}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ChatBanner is a custom banner that displays a admin-specified message at
|
||||
the top of the chat page. Oly used in the EE version of the app. */}
|
||||
|
||||
@ -2059,7 +2260,7 @@ export function ChatPage({
|
||||
!isFetchingChatMessages &&
|
||||
currentSessionChatState == "input" &&
|
||||
!loadingError && (
|
||||
<div className="h-full mt-12 flex flex-col justify-center items-center">
|
||||
<div className="h-full w-[95%] mx-auto mt-12 flex flex-col justify-center items-center">
|
||||
<ChatIntro selectedPersona={liveAssistant} />
|
||||
|
||||
<StarterMessages
|
||||
@ -2081,6 +2282,7 @@ export function ChatPage({
|
||||
Recent Assistants
|
||||
</div>
|
||||
<AssistantBanner
|
||||
mobile={settings?.isMobile}
|
||||
recentAssistants={recentAssistants}
|
||||
liveAssistant={liveAssistant}
|
||||
allAssistants={allAssistants}
|
||||
@ -2222,6 +2424,14 @@ export function ChatPage({
|
||||
}
|
||||
>
|
||||
<AIMessage
|
||||
index={i}
|
||||
selectedMessageForDocDisplay={
|
||||
selectedMessageForDocDisplay
|
||||
}
|
||||
documentSelectionToggled={
|
||||
documentSidebarToggled &&
|
||||
!filtersToggled
|
||||
}
|
||||
continueGenerating={
|
||||
i == messageHistory.length - 1 &&
|
||||
currentCanContinue()
|
||||
@ -2258,9 +2468,19 @@ export function ChatPage({
|
||||
}}
|
||||
isActive={messageHistory.length - 1 == i}
|
||||
selectedDocuments={selectedDocuments}
|
||||
toggleDocumentSelection={
|
||||
toggleDocumentSelectionAspects
|
||||
}
|
||||
toggleDocumentSelection={() => {
|
||||
if (
|
||||
!documentSidebarToggled ||
|
||||
(documentSidebarToggled &&
|
||||
selectedMessageForDocDisplay ===
|
||||
message.messageId)
|
||||
) {
|
||||
toggleDocumentSidebar();
|
||||
}
|
||||
setSelectedMessageForDocDisplay(
|
||||
message.messageId
|
||||
);
|
||||
}}
|
||||
docs={message.documents}
|
||||
currentPersona={liveAssistant}
|
||||
alternativeAssistant={
|
||||
@ -2268,7 +2488,6 @@ export function ChatPage({
|
||||
}
|
||||
messageId={message.messageId}
|
||||
content={message.message}
|
||||
// content={message.message}
|
||||
files={message.files}
|
||||
query={
|
||||
messageHistory[i]?.query || undefined
|
||||
@ -2454,6 +2673,15 @@ export function ChatPage({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{messageHistory.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
height: !autoScrollEnabled
|
||||
? getContainerHeight()
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Some padding at the bottom so the search bar has space at the bottom to not cover the last message*/}
|
||||
<div ref={endPaddingRef} className="h-[95px]" />
|
||||
@ -2477,6 +2705,15 @@ export function ChatPage({
|
||||
</div>
|
||||
)}
|
||||
<ChatInputBar
|
||||
removeDocs={() => {
|
||||
clearSelectedDocuments();
|
||||
}}
|
||||
removeFilters={() => {
|
||||
filterManager.setSelectedSources([]);
|
||||
filterManager.setSelectedTags([]);
|
||||
filterManager.setSelectedDocumentSets([]);
|
||||
setDocumentSidebarToggled(false);
|
||||
}}
|
||||
showConfigureAPIKey={() =>
|
||||
setShowApiKeyModal(true)
|
||||
}
|
||||
@ -2499,6 +2736,9 @@ export function ChatPage({
|
||||
llmOverrideManager={llmOverrideManager}
|
||||
files={currentMessageFiles}
|
||||
setFiles={setCurrentMessageFiles}
|
||||
toggleFilters={
|
||||
retrievalEnabled ? toggleFilters : undefined
|
||||
}
|
||||
handleFileUpload={handleImageUpload}
|
||||
textAreaRef={textAreaRef}
|
||||
chatSessionId={chatSessionIdRef.current!}
|
||||
@ -2529,6 +2769,23 @@ export function ChatPage({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!settings?.isMobile && (
|
||||
<div
|
||||
style={{ transition: "width 0.30s ease-out" }}
|
||||
className={`
|
||||
flex-none
|
||||
overflow-y-hidden
|
||||
transition-all
|
||||
duration-300
|
||||
ease-in-out
|
||||
${
|
||||
documentSidebarToggled && retrievalEnabled
|
||||
? "w-[400px]"
|
||||
: "w-[0px]"
|
||||
}
|
||||
`}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
@ -2537,7 +2794,11 @@ export function ChatPage({
|
||||
<div
|
||||
style={{ transition: "width 0.30s ease-out" }}
|
||||
className={`flex-none bg-transparent transition-all bg-opacity-80 duration-300 epase-in-out h-full
|
||||
${toggledSidebar ? "w-[250px] " : "w-[0px]"}`}
|
||||
${
|
||||
toggledSidebar && !settings?.isMobile
|
||||
? "w-[250px] "
|
||||
: "w-[0px]"
|
||||
}`}
|
||||
/>
|
||||
<div className="my-auto">
|
||||
<DanswerInitializingLoader />
|
||||
@ -2548,20 +2809,8 @@ export function ChatPage({
|
||||
</div>
|
||||
<FixedLogo backgroundToggled={toggledSidebar || showDocSidebar} />
|
||||
</div>
|
||||
{/* Right Sidebar - DocumentSidebar */}
|
||||
</div>
|
||||
<DocumentSidebar
|
||||
initialWidth={350}
|
||||
ref={innerSidebarElementRef}
|
||||
closeSidebar={() => setDocumentSelection(false)}
|
||||
selectedMessage={aiMessage}
|
||||
selectedDocuments={selectedDocuments}
|
||||
toggleDocumentSelection={toggleDocumentSelection}
|
||||
clearSelectedDocuments={clearSelectedDocuments}
|
||||
selectedDocumentTokens={selectedDocumentTokens}
|
||||
maxTokens={maxTokens}
|
||||
isLoading={isFetchingChatMessages}
|
||||
isOpen={documentSelection}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,133 +1,117 @@
|
||||
import { HoverPopup } from "@/components/HoverPopup";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { DanswerDocument } from "@/lib/search/interfaces";
|
||||
import { FiInfo, FiRadio } from "react-icons/fi";
|
||||
import { FiTag } from "react-icons/fi";
|
||||
import { DocumentSelector } from "./DocumentSelector";
|
||||
import {
|
||||
DocumentMetadataBlock,
|
||||
buildDocumentSummaryDisplay,
|
||||
} from "@/components/search/DocumentDisplay";
|
||||
import { InternetSearchIcon } from "@/components/InternetSearchIcon";
|
||||
import { buildDocumentSummaryDisplay } from "@/components/search/DocumentDisplay";
|
||||
import { DocumentUpdatedAtBadge } from "@/components/search/DocumentUpdatedAtBadge";
|
||||
import { MetadataBadge } from "@/components/MetadataBadge";
|
||||
import { WebResultIcon } from "@/components/WebResultIcon";
|
||||
|
||||
interface DocumentDisplayProps {
|
||||
document: DanswerDocument;
|
||||
queryEventId: number | null;
|
||||
isAIPick: boolean;
|
||||
modal?: boolean;
|
||||
isSelected: boolean;
|
||||
handleSelect: (documentId: string) => void;
|
||||
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||
tokenLimitReached: boolean;
|
||||
}
|
||||
|
||||
export function DocumentMetadataBlock({
|
||||
modal,
|
||||
document,
|
||||
}: {
|
||||
modal?: boolean;
|
||||
document: DanswerDocument;
|
||||
}) {
|
||||
const MAX_METADATA_ITEMS = 3;
|
||||
const metadataEntries = Object.entries(document.metadata);
|
||||
|
||||
return (
|
||||
<div className="flex items-center overflow-hidden">
|
||||
{document.updated_at && (
|
||||
<DocumentUpdatedAtBadge updatedAt={document.updated_at} modal={modal} />
|
||||
)}
|
||||
|
||||
{metadataEntries.length > 0 && (
|
||||
<>
|
||||
<div className="mx-1 h-4 border-l border-border" />
|
||||
<div className="flex items-center overflow-hidden">
|
||||
{metadataEntries
|
||||
.slice(0, MAX_METADATA_ITEMS)
|
||||
.map(([key, value], index) => (
|
||||
<MetadataBadge
|
||||
key={index}
|
||||
icon={FiTag}
|
||||
value={`${key}=${value}`}
|
||||
/>
|
||||
))}
|
||||
{metadataEntries.length > MAX_METADATA_ITEMS && (
|
||||
<span className="ml-1 text-xs text-gray-500">...</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatDocumentDisplay({
|
||||
document,
|
||||
queryEventId,
|
||||
isAIPick,
|
||||
modal,
|
||||
isSelected,
|
||||
handleSelect,
|
||||
setPopup,
|
||||
tokenLimitReached,
|
||||
}: DocumentDisplayProps) {
|
||||
const isInternet = document.is_internet;
|
||||
// Consider reintroducing null scored docs in the future
|
||||
|
||||
if (document.score === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={document.semantic_identifier}
|
||||
className={`p-2 w-[325px] justify-start rounded-md ${
|
||||
isSelected ? "bg-background-200" : "bg-background-125"
|
||||
} text-sm mx-3`}
|
||||
>
|
||||
<div className="flex relative justify-start overflow-y-visible">
|
||||
<div className={`opacity-100 ${modal ? "w-[90vw]" : "w-full"}`}>
|
||||
<div
|
||||
className={`flex relative flex-col gap-0.5 rounded-xl mx-2 my-1 ${
|
||||
isSelected ? "bg-gray-200" : "hover:bg-background-125"
|
||||
}`}
|
||||
>
|
||||
<a
|
||||
href={document.link}
|
||||
target="_blank"
|
||||
className={
|
||||
"rounded-lg flex font-bold flex-shrink truncate" +
|
||||
(document.link ? "" : "pointer-events-none")
|
||||
}
|
||||
rel="noreferrer"
|
||||
rel="noopener noreferrer"
|
||||
className="cursor-pointer flex flex-col px-2 py-1.5"
|
||||
>
|
||||
{isInternet ? (
|
||||
<InternetSearchIcon url={document.link} />
|
||||
) : (
|
||||
<SourceIcon sourceType={document.source_type} iconSize={18} />
|
||||
)}
|
||||
<p className="overflow-hidden text-left text-ellipsis mx-2 my-auto text-sm">
|
||||
{document.semantic_identifier || document.document_id}
|
||||
</p>
|
||||
</a>
|
||||
{document.score !== null && (
|
||||
<div className="my-auto">
|
||||
{isAIPick && (
|
||||
<div className="w-4 h-4 my-auto mr-1 flex flex-col">
|
||||
<HoverPopup
|
||||
mainContent={<FiRadio className="text-gray-500 my-auto" />}
|
||||
popupContent={
|
||||
<div className="text-xs text-gray-300 w-36 flex">
|
||||
<div className="flex mx-auto">
|
||||
<div className="w-3 h-3 flex flex-col my-auto mr-1">
|
||||
<FiInfo className="my-auto" />
|
||||
</div>
|
||||
<div className="my-auto">The AI liked this doc!</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
direction="bottom"
|
||||
style="dark"
|
||||
/>
|
||||
</div>
|
||||
<div className="line-clamp-1 mb-1 flex h-6 items-center gap-2 text-xs">
|
||||
{document.is_internet || document.source_type === "web" ? (
|
||||
<WebResultIcon url={document.link} />
|
||||
) : (
|
||||
<SourceIcon sourceType={document.source_type} iconSize={18} />
|
||||
)}
|
||||
<div
|
||||
className={`
|
||||
text-xs
|
||||
text-emphasis
|
||||
bg-hover
|
||||
rounded
|
||||
p-0.5
|
||||
w-fit
|
||||
my-auto
|
||||
select-none
|
||||
my-auto
|
||||
mr-2`}
|
||||
>
|
||||
{Math.abs(document.score).toFixed(2)}
|
||||
<div className="line-clamp-1 text-text-900 text-sm font-semibold">
|
||||
{(document.semantic_identifier || document.document_id).length >
|
||||
(modal ? 30 : 40)
|
||||
? `${(document.semantic_identifier || document.document_id)
|
||||
.slice(0, modal ? 30 : 40)
|
||||
.trim()}...`
|
||||
: document.semantic_identifier || document.document_id}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isInternet && (
|
||||
<DocumentSelector
|
||||
isSelected={isSelected}
|
||||
handleSelect={() => handleSelect(document.document_id)}
|
||||
isDisabled={tokenLimitReached && !isSelected}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="mt-1">
|
||||
<DocumentMetadataBlock document={document} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="line-clamp-3 pl-1 pt-2 mb-1 text-start break-words">
|
||||
{buildDocumentSummaryDisplay(document.match_highlights, document.blurb)}
|
||||
test
|
||||
</p>
|
||||
<div className="mb-2">
|
||||
{/*
|
||||
// TODO: find a way to include this
|
||||
{queryEventId && (
|
||||
<DocumentFeedbackBlock
|
||||
documentId={document.document_id}
|
||||
queryId={queryEventId}
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
)} */}
|
||||
<DocumentMetadataBlock modal={modal} document={document} />
|
||||
<div className="line-clamp-3 pt-2 text-sm font-normal leading-snug text-gray-600">
|
||||
{buildDocumentSummaryDisplay(
|
||||
document.match_highlights,
|
||||
document.blurb
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute top-2 right-2">
|
||||
{!isInternet && (
|
||||
<DocumentSelector
|
||||
isSelected={isSelected}
|
||||
handleSelect={() => handleSelect(document.document_id)}
|
||||
isDisabled={tokenLimitReached && !isSelected}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
186
web/src/app/chat/documentSidebar/ChatFilters.tsx
Normal file
186
web/src/app/chat/documentSidebar/ChatFilters.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import { DanswerDocument } from "@/lib/search/interfaces";
|
||||
import { ChatDocumentDisplay } from "./ChatDocumentDisplay";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { removeDuplicateDocs } from "@/lib/documentUtils";
|
||||
import { Message } from "../interfaces";
|
||||
import { ForwardedRef, forwardRef, useEffect, useState } from "react";
|
||||
import { FilterManager } from "@/lib/hooks";
|
||||
import { CCPairBasicInfo, DocumentSet, Tag } from "@/lib/types";
|
||||
import { SourceSelector } from "../shared_chat_search/SearchFilters";
|
||||
import { XIcon } from "@/components/icons/icons";
|
||||
|
||||
interface ChatFiltersProps {
|
||||
filterManager: FilterManager;
|
||||
closeSidebar: () => void;
|
||||
selectedMessage: Message | null;
|
||||
selectedDocuments: DanswerDocument[] | null;
|
||||
toggleDocumentSelection: (document: DanswerDocument) => void;
|
||||
clearSelectedDocuments: () => void;
|
||||
selectedDocumentTokens: number;
|
||||
maxTokens: number;
|
||||
initialWidth: number;
|
||||
isOpen: boolean;
|
||||
modal: boolean;
|
||||
ccPairs: CCPairBasicInfo[];
|
||||
tags: Tag[];
|
||||
documentSets: DocumentSet[];
|
||||
showFilters: boolean;
|
||||
}
|
||||
|
||||
export const ChatFilters = forwardRef<HTMLDivElement, ChatFiltersProps>(
|
||||
(
|
||||
{
|
||||
closeSidebar,
|
||||
modal,
|
||||
selectedMessage,
|
||||
selectedDocuments,
|
||||
filterManager,
|
||||
toggleDocumentSelection,
|
||||
clearSelectedDocuments,
|
||||
selectedDocumentTokens,
|
||||
maxTokens,
|
||||
initialWidth,
|
||||
isOpen,
|
||||
ccPairs,
|
||||
tags,
|
||||
documentSets,
|
||||
showFilters,
|
||||
},
|
||||
ref: ForwardedRef<HTMLDivElement>
|
||||
) => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
const [delayedSelectedDocumentCount, setDelayedSelectedDocumentCount] =
|
||||
useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(
|
||||
() => {
|
||||
setDelayedSelectedDocumentCount(selectedDocuments?.length || 0);
|
||||
},
|
||||
selectedDocuments?.length == 0 ? 1000 : 0
|
||||
);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [selectedDocuments]);
|
||||
|
||||
const selectedDocumentIds =
|
||||
selectedDocuments?.map((document) => document.document_id) || [];
|
||||
|
||||
const currentDocuments = selectedMessage?.documents || null;
|
||||
const dedupedDocuments = removeDuplicateDocs(currentDocuments || []);
|
||||
|
||||
const tokenLimitReached = selectedDocumentTokens > maxTokens - 75;
|
||||
|
||||
const hasSelectedDocuments = selectedDocumentIds.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="danswer-chat-sidebar"
|
||||
className={`relative py-2 max-w-full ${
|
||||
!modal ? "border-l h-full border-sidebar-border" : ""
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
closeSidebar();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`ml-auto h-full relative sidebar transition-all duration-300
|
||||
${
|
||||
isOpen
|
||||
? "opacity-100 translate-x-0"
|
||||
: "opacity-0 translate-x-[10%]"
|
||||
}`}
|
||||
style={{
|
||||
width: modal ? undefined : initialWidth,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
{popup}
|
||||
<div className="p-4 flex justify-between items-center">
|
||||
<h2 className="text-xl font-bold text-text-900">
|
||||
{showFilters ? "Filters" : "Sources"}
|
||||
</h2>
|
||||
<button
|
||||
onClick={closeSidebar}
|
||||
className="text-sm text-primary-600 mr-2 hover:text-primary-800 transition-colors duration-200 ease-in-out"
|
||||
>
|
||||
<XIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="border-b border-divider-history-sidebar-bar mx-3" />
|
||||
<div className="overflow-y-auto -mx-1 sm:mx-0 flex-grow gap-y-0 default-scrollbar dark-scrollbar flex flex-col">
|
||||
{showFilters ? (
|
||||
<SourceSelector
|
||||
modal={modal}
|
||||
tagsOnLeft={true}
|
||||
filtersUntoggled={false}
|
||||
{...filterManager}
|
||||
availableDocumentSets={documentSets}
|
||||
existingSources={ccPairs.map((ccPair) => ccPair.source)}
|
||||
availableTags={tags}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{dedupedDocuments.length > 0 ? (
|
||||
dedupedDocuments.map((document, ind) => (
|
||||
<div
|
||||
key={document.document_id}
|
||||
className={`${
|
||||
ind === dedupedDocuments.length - 1
|
||||
? ""
|
||||
: "border-b border-border-light w-full"
|
||||
}`}
|
||||
>
|
||||
<ChatDocumentDisplay
|
||||
modal={modal}
|
||||
document={document}
|
||||
isSelected={selectedDocumentIds.includes(
|
||||
document.document_id
|
||||
)}
|
||||
handleSelect={(documentId) => {
|
||||
toggleDocumentSelection(
|
||||
dedupedDocuments.find(
|
||||
(doc) => doc.document_id === documentId
|
||||
)!
|
||||
);
|
||||
}}
|
||||
tokenLimitReached={tokenLimitReached}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="mx-3" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!showFilters && (
|
||||
<div
|
||||
className={`sticky bottom-4 w-full left-0 flex justify-center transition-opacity duration-300 ${
|
||||
hasSelectedDocuments
|
||||
? "opacity-100"
|
||||
: "opacity-0 pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="text-sm font-medium py-2 px-4 rounded-full transition-colors bg-gray-900 text-white"
|
||||
onClick={clearSelectedDocuments}
|
||||
>
|
||||
{`Remove ${
|
||||
delayedSelectedDocumentCount > 0
|
||||
? delayedSelectedDocumentCount
|
||||
: ""
|
||||
} Source${delayedSelectedDocumentCount > 1 ? "s" : ""}`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChatFilters.displayName = "ChatFilters";
|
@ -1,168 +0,0 @@
|
||||
import { DanswerDocument } from "@/lib/search/interfaces";
|
||||
import Text from "@/components/ui/text";
|
||||
import { ChatDocumentDisplay } from "./ChatDocumentDisplay";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { removeDuplicateDocs } from "@/lib/documentUtils";
|
||||
import { Message } from "../interfaces";
|
||||
import { ForwardedRef, forwardRef } from "react";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
interface DocumentSidebarProps {
|
||||
closeSidebar: () => void;
|
||||
selectedMessage: Message | null;
|
||||
selectedDocuments: DanswerDocument[] | null;
|
||||
toggleDocumentSelection: (document: DanswerDocument) => void;
|
||||
clearSelectedDocuments: () => void;
|
||||
selectedDocumentTokens: number;
|
||||
maxTokens: number;
|
||||
isLoading: boolean;
|
||||
initialWidth: number;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export const DocumentSidebar = forwardRef<HTMLDivElement, DocumentSidebarProps>(
|
||||
(
|
||||
{
|
||||
closeSidebar,
|
||||
selectedMessage,
|
||||
selectedDocuments,
|
||||
toggleDocumentSelection,
|
||||
clearSelectedDocuments,
|
||||
selectedDocumentTokens,
|
||||
maxTokens,
|
||||
isLoading,
|
||||
initialWidth,
|
||||
isOpen,
|
||||
},
|
||||
ref: ForwardedRef<HTMLDivElement>
|
||||
) => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
const selectedDocumentIds =
|
||||
selectedDocuments?.map((document) => document.document_id) || [];
|
||||
|
||||
const currentDocuments = selectedMessage?.documents || null;
|
||||
const dedupedDocuments = removeDuplicateDocs(currentDocuments || []);
|
||||
|
||||
// NOTE: do not allow selection if less than 75 tokens are left
|
||||
// this is to prevent the case where they are able to select the doc
|
||||
// but it basically is unused since it's truncated right at the very
|
||||
// start of the document (since title + metadata + misc overhead) takes up
|
||||
// space
|
||||
const tokenLimitReached = selectedDocumentTokens > maxTokens - 75;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="danswer-chat-sidebar"
|
||||
className={`fixed inset-0 transition-opacity duration-300 z-50 bg-black/80 ${
|
||||
isOpen ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
closeSidebar();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`ml-auto rounded-l-lg relative border-l bg-text-100 sidebar z-50 absolute right-0 h-screen transition-all duration-300 ${
|
||||
isOpen ? "opacity-100 translate-x-0" : "opacity-0 translate-x-[10%]"
|
||||
}`}
|
||||
ref={ref}
|
||||
style={{
|
||||
width: initialWidth,
|
||||
}}
|
||||
>
|
||||
<div className="pb-6 flex-initial overflow-y-hidden flex flex-col h-screen">
|
||||
{popup}
|
||||
<div className="pl-3 mx-2 pr-6 mt-3 flex text-text-800 flex-col text-2xl text-emphasis flex font-semibold">
|
||||
{dedupedDocuments.length} Document
|
||||
{dedupedDocuments.length > 1 ? "s" : ""}
|
||||
<p className="text-sm font-semibold flex flex-wrap gap-x-2 text-text-600 mt-1">
|
||||
Select to add to continuous context
|
||||
<a
|
||||
href="https://docs.danswer.dev/introduction"
|
||||
className="underline cursor-pointer hover:text-strong"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator className="mb-0 mt-4 pb-2" />
|
||||
|
||||
{currentDocuments ? (
|
||||
<div className="overflow-y-auto flex-grow dark-scrollbar flex relative flex-col">
|
||||
{dedupedDocuments.length > 0 ? (
|
||||
dedupedDocuments.map((document, ind) => (
|
||||
<div
|
||||
key={document.document_id}
|
||||
className={`${
|
||||
ind === dedupedDocuments.length - 1
|
||||
? "mb-5"
|
||||
: "border-b border-border-light mb-3"
|
||||
}`}
|
||||
>
|
||||
<ChatDocumentDisplay
|
||||
document={document}
|
||||
setPopup={setPopup}
|
||||
queryEventId={null}
|
||||
isAIPick={false}
|
||||
isSelected={selectedDocumentIds.includes(
|
||||
document.document_id
|
||||
)}
|
||||
handleSelect={(documentId) => {
|
||||
toggleDocumentSelection(
|
||||
dedupedDocuments.find(
|
||||
(document) => document.document_id === documentId
|
||||
)!
|
||||
);
|
||||
}}
|
||||
tokenLimitReached={tokenLimitReached}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="mx-3">
|
||||
<Text>No documents found for the query.</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
!isLoading && (
|
||||
<div className="ml-4 mr-3">
|
||||
<Text>
|
||||
When you run ask a question, the retrieved documents will
|
||||
show up here!
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="absolute left-0 bottom-0 w-full bg-gradient-to-b from-neutral-100/0 via-neutral-100/40 backdrop-blur-xs to-neutral-100 h-[100px]" />
|
||||
<div className="sticky bottom-4 w-full left-0 justify-center flex gap-x-4">
|
||||
<button
|
||||
className="bg-[#84e49e] text-xs p-2 rounded text-text-800"
|
||||
onClick={() => closeSidebar()}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="bg-error text-xs p-2 rounded text-text-200"
|
||||
onClick={() => {
|
||||
clearSelectedDocuments();
|
||||
|
||||
closeSidebar();
|
||||
}}
|
||||
>
|
||||
Delete Context
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DocumentSidebar.displayName = "DocumentSidebar";
|
@ -1,13 +1,9 @@
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import { FiPlusCircle, FiPlus, FiInfo, FiX } from "react-icons/fi";
|
||||
import { FiPlusCircle, FiPlus, FiInfo, FiX, FiSearch } from "react-icons/fi";
|
||||
import { ChatInputOption } from "./ChatInputOption";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { InputPrompt } from "@/app/admin/prompt-library/interfaces";
|
||||
import {
|
||||
FilterManager,
|
||||
getDisplayNameForModel,
|
||||
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";
|
||||
@ -18,15 +14,10 @@ import {
|
||||
} from "../files/InputBarPreview";
|
||||
import {
|
||||
AssistantsIconSkeleton,
|
||||
CpuIconSkeleton,
|
||||
FileIcon,
|
||||
SendIcon,
|
||||
StopGeneratingIcon,
|
||||
} from "@/components/icons/icons";
|
||||
import { IconType } from "react-icons";
|
||||
import Popup from "../../../components/popup/Popup";
|
||||
import { LlmTab } from "../modal/configuration/LlmTab";
|
||||
import { AssistantsTab } from "../modal/configuration/AssistantsTab";
|
||||
import { DanswerDocument } from "@/lib/search/interfaces";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import {
|
||||
@ -40,10 +31,18 @@ import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { ChatState } from "../types";
|
||||
import UnconfiguredProviderText from "@/components/chat_search/UnconfiguredProviderText";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import AnimatedToggle from "@/components/search/SearchBar";
|
||||
import { Popup } from "@/components/admin/connectors/Popup";
|
||||
import { AssistantsTab } from "../modal/configuration/AssistantsTab";
|
||||
import { IconType } from "react-icons";
|
||||
import { LlmTab } from "../modal/configuration/LlmTab";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
const MAX_INPUT_HEIGHT = 200;
|
||||
|
||||
export function ChatInputBar({
|
||||
removeFilters,
|
||||
removeDocs,
|
||||
openModelSettings,
|
||||
showDocs,
|
||||
showConfigureAPIKey,
|
||||
@ -68,7 +67,10 @@ export function ChatInputBar({
|
||||
alternativeAssistant,
|
||||
chatSessionId,
|
||||
inputPrompts,
|
||||
toggleFilters,
|
||||
}: {
|
||||
removeFilters: () => void;
|
||||
removeDocs: () => void;
|
||||
showConfigureAPIKey: () => void;
|
||||
openModelSettings: () => void;
|
||||
chatState: ChatState;
|
||||
@ -90,6 +92,7 @@ export function ChatInputBar({
|
||||
handleFileUpload: (files: File[]) => void;
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
chatSessionId?: string;
|
||||
toggleFilters?: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
const textarea = textAreaRef.current;
|
||||
@ -370,9 +373,9 @@ export function ChatInputBar({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
{/* <div>
|
||||
<SelectedFilterDisplay filterManager={filterManager} />
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<UnconfiguredProviderText showConfigureAPIKey={showConfigureAPIKey} />
|
||||
|
||||
@ -429,16 +432,21 @@ export function ChatInputBar({
|
||||
)}
|
||||
{(selectedDocuments.length > 0 || files.length > 0) && (
|
||||
<div className="flex gap-x-2 px-2 pt-2">
|
||||
<div className="flex gap-x-1 px-2 overflow-y-auto overflow-x-scroll items-end miniscroll">
|
||||
<div className="flex gap-x-1 px-2 overflow-visible overflow-x-scroll items-end miniscroll">
|
||||
{selectedDocuments.length > 0 && (
|
||||
<button
|
||||
onClick={showDocs}
|
||||
className="flex-none flex cursor-pointer hover:bg-background-200 transition-colors duration-300 h-10 p-1 items-center gap-x-1 rounded-lg bg-background-150 max-w-[100px]"
|
||||
className="flex-none relative overflow-visible flex items-center gap-x-2 h-10 px-3 rounded-lg bg-background-150 hover:bg-background-200 transition-colors duration-300 cursor-pointer max-w-[150px]"
|
||||
>
|
||||
<FileIcon size={24} />
|
||||
<p className="text-xs">
|
||||
<FileIcon size={20} />
|
||||
<span className="text-sm whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{selectedDocuments.length} selected
|
||||
</p>
|
||||
</span>
|
||||
<XIcon
|
||||
onClick={removeDocs}
|
||||
size={16}
|
||||
className="text-text-400 hover:text-text-600 ml-auto"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{files.map((file) => (
|
||||
@ -529,72 +537,6 @@ export function ChatInputBar({
|
||||
suppressContentEditableWarning={true}
|
||||
/>
|
||||
<div className="flex items-center space-x-3 mr-12 px-4 pb-2">
|
||||
<Popup
|
||||
removePadding
|
||||
content={(close) => (
|
||||
<AssistantsTab
|
||||
llmProviders={llmProviders}
|
||||
selectedAssistant={selectedAssistant}
|
||||
onSelect={(assistant) => {
|
||||
setSelectedAssistant(assistant);
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
flexPriority="shrink"
|
||||
position="top"
|
||||
mobilePosition="top-right"
|
||||
>
|
||||
<ChatInputOption
|
||||
toggle
|
||||
flexPriority="shrink"
|
||||
name={
|
||||
selectedAssistant ? selectedAssistant.name : "Assistants"
|
||||
}
|
||||
Icon={AssistantsIconSkeleton as IconType}
|
||||
/>
|
||||
</Popup>
|
||||
<Popup
|
||||
tab
|
||||
content={(close, ref) => (
|
||||
<LlmTab
|
||||
currentAssistant={alternativeAssistant || selectedAssistant}
|
||||
openModelSettings={openModelSettings}
|
||||
currentLlm={
|
||||
llmOverrideManager.llmOverride.modelName ||
|
||||
(selectedAssistant
|
||||
? selectedAssistant.llm_model_version_override ||
|
||||
llmOverrideManager.globalDefault.modelName ||
|
||||
llmName
|
||||
: llmName)
|
||||
}
|
||||
close={close}
|
||||
ref={ref}
|
||||
llmOverrideManager={llmOverrideManager}
|
||||
chatSessionId={chatSessionId}
|
||||
/>
|
||||
)}
|
||||
position="top"
|
||||
>
|
||||
<ChatInputOption
|
||||
flexPriority="second"
|
||||
toggle
|
||||
name={
|
||||
settings?.isMobile
|
||||
? undefined
|
||||
: getDisplayNameForModel(
|
||||
llmOverrideManager.llmOverride.modelName ||
|
||||
(selectedAssistant
|
||||
? selectedAssistant.llm_model_version_override ||
|
||||
llmOverrideManager.globalDefault.modelName ||
|
||||
llmName
|
||||
: llmName)
|
||||
)
|
||||
}
|
||||
Icon={CpuIconSkeleton}
|
||||
/>
|
||||
</Popup>
|
||||
|
||||
<ChatInputOption
|
||||
flexPriority="stiff"
|
||||
name="File"
|
||||
@ -614,6 +556,14 @@ export function ChatInputBar({
|
||||
input.click();
|
||||
}}
|
||||
/>
|
||||
{toggleFilters && (
|
||||
<ChatInputOption
|
||||
flexPriority="stiff"
|
||||
name="Filters"
|
||||
Icon={FiSearch}
|
||||
onClick={toggleFilters}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-2.5 mobile:right-4 desktop:right-10">
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
AnswerPiecePacket,
|
||||
DanswerDocument,
|
||||
Filters,
|
||||
FinalContextDocs,
|
||||
StreamStopInfo,
|
||||
} from "@/lib/search/interfaces";
|
||||
import { handleSSEStream } from "@/lib/search/streamingUtils";
|
||||
@ -102,6 +103,7 @@ export type PacketType =
|
||||
| ToolCallMetadata
|
||||
| BackendMessage
|
||||
| AnswerPiecePacket
|
||||
| FinalContextDocs
|
||||
| DocumentsResponse
|
||||
| FileChatDisplay
|
||||
| StreamingError
|
||||
@ -147,7 +149,6 @@ export async function* sendMessage({
|
||||
}): AsyncGenerator<PacketType, void, unknown> {
|
||||
const documentsAreSelected =
|
||||
selectedDocumentIds && selectedDocumentIds.length > 0;
|
||||
|
||||
const body = JSON.stringify({
|
||||
alternate_assistant_id: alternateAssistantId,
|
||||
chat_session_id: chatSessionId,
|
||||
@ -639,6 +640,7 @@ export async function useScrollonStream({
|
||||
endDivRef,
|
||||
debounceNumber,
|
||||
mobile,
|
||||
enableAutoScroll,
|
||||
}: {
|
||||
chatState: ChatState;
|
||||
scrollableDivRef: RefObject<HTMLDivElement>;
|
||||
@ -647,6 +649,7 @@ export async function useScrollonStream({
|
||||
endDivRef: RefObject<HTMLDivElement>;
|
||||
debounceNumber: number;
|
||||
mobile?: boolean;
|
||||
enableAutoScroll?: boolean;
|
||||
}) {
|
||||
const mobileDistance = 900; // distance that should "engage" the scroll
|
||||
const desktopDistance = 500; // distance that should "engage" the scroll
|
||||
@ -659,6 +662,10 @@ export async function useScrollonStream({
|
||||
const previousScroll = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableAutoScroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (chatState != "input" && scrollableDivRef && scrollableDivRef.current) {
|
||||
const newHeight: number = scrollableDivRef.current?.scrollTop!;
|
||||
const heightDifference = newHeight - previousScroll.current;
|
||||
@ -716,7 +723,7 @@ export async function useScrollonStream({
|
||||
|
||||
// scroll on end of stream if within distance
|
||||
useEffect(() => {
|
||||
if (scrollableDivRef?.current && chatState == "input") {
|
||||
if (scrollableDivRef?.current && chatState == "input" && enableAutoScroll) {
|
||||
if (scrollDist.current < distance - 50) {
|
||||
scrollableDivRef?.current?.scrollBy({
|
||||
left: 0,
|
||||
|
@ -1,8 +1,50 @@
|
||||
import { Citation } from "@/components/search/results/Citation";
|
||||
import { WebResultIcon } from "@/components/WebResultIcon";
|
||||
import { LoadedDanswerDocument } from "@/lib/search/interfaces";
|
||||
import { getSourceMetadata } from "@/lib/sources";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import React, { memo } from "react";
|
||||
import isEqual from "lodash/isEqual";
|
||||
|
||||
export const MemoizedAnchor = memo(({ docs, children }: any) => {
|
||||
console.log(children);
|
||||
const value = children?.toString();
|
||||
if (value?.startsWith("[") && value?.endsWith("]")) {
|
||||
const match = value.match(/\[(\d+)\]/);
|
||||
if (match) {
|
||||
const index = parseInt(match[1], 10) - 1;
|
||||
const associatedDoc = docs && docs[index];
|
||||
|
||||
const url = associatedDoc?.link
|
||||
? new URL(associatedDoc.link).origin + "/favicon.ico"
|
||||
: "";
|
||||
|
||||
const getIcon = (sourceType: ValidSources, link: string) => {
|
||||
return getSourceMetadata(sourceType).icon({ size: 18 });
|
||||
};
|
||||
|
||||
const icon =
|
||||
associatedDoc?.source_type === "web" ? (
|
||||
<WebResultIcon url={associatedDoc.link} />
|
||||
) : (
|
||||
getIcon(
|
||||
associatedDoc?.source_type || "web",
|
||||
associatedDoc?.link || ""
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<MemoizedLink document={{ ...associatedDoc, icon, url }}>
|
||||
{children}
|
||||
</MemoizedLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <MemoizedLink>{children}</MemoizedLink>;
|
||||
});
|
||||
|
||||
export const MemoizedLink = memo((props: any) => {
|
||||
const { node, ...rest } = props;
|
||||
const { node, document, ...rest } = props;
|
||||
const value = rest.children;
|
||||
|
||||
if (value?.toString().startsWith("*")) {
|
||||
@ -10,7 +52,16 @@ export const MemoizedLink = memo((props: any) => {
|
||||
<div className="flex-none bg-background-800 inline-block rounded-full h-3 w-3 ml-2" />
|
||||
);
|
||||
} else if (value?.toString().startsWith("[")) {
|
||||
return <Citation link={rest?.href}>{rest.children}</Citation>;
|
||||
return (
|
||||
<Citation
|
||||
url={document?.url}
|
||||
icon={document?.icon as React.ReactNode}
|
||||
link={rest?.href}
|
||||
document={document as LoadedDanswerDocument}
|
||||
>
|
||||
{rest.children}
|
||||
</Citation>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a
|
||||
@ -25,9 +76,16 @@ export const MemoizedLink = memo((props: any) => {
|
||||
}
|
||||
});
|
||||
|
||||
export const MemoizedParagraph = memo(({ ...props }: any) => {
|
||||
return <p {...props} className="text-default" />;
|
||||
});
|
||||
export const MemoizedParagraph = memo(
|
||||
function MemoizedParagraph({ children }: any) {
|
||||
return <p className="text-default">{children}</p>;
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
const areEqual = isEqual(prevProps.children, nextProps.children);
|
||||
return areEqual;
|
||||
}
|
||||
);
|
||||
|
||||
MemoizedAnchor.displayName = "MemoizedAnchor";
|
||||
MemoizedLink.displayName = "MemoizedLink";
|
||||
MemoizedParagraph.displayName = "MemoizedParagraph";
|
||||
|
@ -8,14 +8,22 @@ import {
|
||||
FiGlobe,
|
||||
} from "react-icons/fi";
|
||||
import { FeedbackType } from "../types";
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, {
|
||||
memo,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import {
|
||||
DanswerDocument,
|
||||
FilteredDanswerDocument,
|
||||
} from "@/lib/search/interfaces";
|
||||
import { SearchSummary } from "./SearchSummary";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
|
||||
import { SkippedSearch } from "./SkippedSearch";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { CopyButton } from "@/components/CopyButton";
|
||||
@ -36,8 +44,6 @@ import "prismjs/themes/prism-tomorrow.css";
|
||||
import "./custom-code-styles.css";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { Citation } from "@/components/search/results/Citation";
|
||||
import { DocumentMetadataBlock } from "@/components/search/DocumentDisplay";
|
||||
|
||||
import { LikeFeedback, DislikeFeedback } from "@/components/icons/icons";
|
||||
import {
|
||||
@ -52,16 +58,18 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useMouseTracking } from "./hooks";
|
||||
import { InternetSearchIcon } from "@/components/InternetSearchIcon";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import GeneratingImageDisplay from "../tools/GeneratingImageDisplay";
|
||||
import RegenerateOption from "../RegenerateOption";
|
||||
import { LlmOverride } from "@/lib/hooks";
|
||||
import { ContinueGenerating } from "./ContinueMessage";
|
||||
import { MemoizedLink, MemoizedParagraph } from "./MemoizedTextComponents";
|
||||
import { MemoizedAnchor, MemoizedParagraph } from "./MemoizedTextComponents";
|
||||
import { extractCodeText } from "./codeUtils";
|
||||
import ToolResult from "../../../components/tools/ToolResult";
|
||||
import CsvContent from "../../../components/tools/CSVContent";
|
||||
import SourceCard, {
|
||||
SeeMoreBlock,
|
||||
} from "@/components/chat_search/sources/SourceCard";
|
||||
|
||||
const TOOLS_WITH_CUSTOM_HANDLING = [
|
||||
SEARCH_TOOL_NAME,
|
||||
@ -155,6 +163,7 @@ function FileDisplay({
|
||||
export const AIMessage = ({
|
||||
regenerate,
|
||||
overriddenModel,
|
||||
selectedMessageForDocDisplay,
|
||||
continueGenerating,
|
||||
shared,
|
||||
isActive,
|
||||
@ -162,6 +171,7 @@ export const AIMessage = ({
|
||||
alternativeAssistant,
|
||||
docs,
|
||||
messageId,
|
||||
documentSelectionToggled,
|
||||
content,
|
||||
files,
|
||||
selectedDocuments,
|
||||
@ -178,7 +188,10 @@ export const AIMessage = ({
|
||||
currentPersona,
|
||||
otherMessagesCanSwitchTo,
|
||||
onMessageSelection,
|
||||
index,
|
||||
}: {
|
||||
index?: number;
|
||||
selectedMessageForDocDisplay?: number | null;
|
||||
shared?: boolean;
|
||||
isActive?: boolean;
|
||||
continueGenerating?: () => void;
|
||||
@ -191,6 +204,7 @@ export const AIMessage = ({
|
||||
currentPersona: Persona;
|
||||
messageId: number | null;
|
||||
content: string | JSX.Element;
|
||||
documentSelectionToggled?: boolean;
|
||||
files?: FileDescriptor[];
|
||||
query?: string;
|
||||
citedDocuments?: [string, DanswerDocument][] | null;
|
||||
@ -287,18 +301,31 @@ export const AIMessage = ({
|
||||
});
|
||||
}
|
||||
|
||||
const paragraphCallback = useCallback(
|
||||
(props: any) => <MemoizedParagraph>{props.children}</MemoizedParagraph>,
|
||||
[]
|
||||
);
|
||||
|
||||
const anchorCallback = useCallback(
|
||||
(props: any) => (
|
||||
<MemoizedAnchor docs={docs}>{props.children}</MemoizedAnchor>
|
||||
),
|
||||
[docs]
|
||||
);
|
||||
|
||||
const currentMessageInd = messageId
|
||||
? otherMessagesCanSwitchTo?.indexOf(messageId)
|
||||
: undefined;
|
||||
|
||||
const uniqueSources: ValidSources[] = Array.from(
|
||||
new Set((docs || []).map((doc) => doc.source_type))
|
||||
).slice(0, 3);
|
||||
|
||||
const markdownComponents = useMemo(
|
||||
() => ({
|
||||
a: MemoizedLink,
|
||||
p: MemoizedParagraph,
|
||||
code: ({ node, className, children, ...props }: any) => {
|
||||
a: anchorCallback,
|
||||
p: paragraphCallback,
|
||||
code: ({ node, className, children }: any) => {
|
||||
const codeText = extractCodeText(
|
||||
node,
|
||||
finalContent as string,
|
||||
@ -312,7 +339,7 @@ export const AIMessage = ({
|
||||
);
|
||||
},
|
||||
}),
|
||||
[finalContent]
|
||||
[anchorCallback, paragraphCallback, finalContent]
|
||||
);
|
||||
|
||||
const renderedMarkdown = useMemo(() => {
|
||||
@ -333,12 +360,11 @@ export const AIMessage = ({
|
||||
onMessageSelection &&
|
||||
otherMessagesCanSwitchTo &&
|
||||
otherMessagesCanSwitchTo.length > 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="danswer-ai-message"
|
||||
ref={trackedElementRef}
|
||||
className={"py-5 ml-4 px-5 relative flex "}
|
||||
className={`py-5 ml-4 px-5 relative flex `}
|
||||
>
|
||||
<div
|
||||
className={`mx-auto ${
|
||||
@ -363,6 +389,7 @@ export const AIMessage = ({
|
||||
!retrievalDisabled && (
|
||||
<div className="mb-1">
|
||||
<SearchSummary
|
||||
index={index || 0}
|
||||
query={query}
|
||||
finished={toolCall?.tool_result != undefined}
|
||||
hasDocs={hasDocs || false}
|
||||
@ -423,6 +450,31 @@ export const AIMessage = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{docs && docs.length > 0 && (
|
||||
<div className="mt-2 -mx-8 w-full mb-4 flex relative">
|
||||
<div className="w-full">
|
||||
<div className="px-8 flex gap-x-2">
|
||||
{!settings?.isMobile &&
|
||||
docs.length > 0 &&
|
||||
docs
|
||||
.slice(0, 2)
|
||||
.map((doc, ind) => (
|
||||
<SourceCard doc={doc} key={ind} />
|
||||
))}
|
||||
<SeeMoreBlock
|
||||
documentSelectionToggled={
|
||||
(documentSelectionToggled &&
|
||||
selectedMessageForDocDisplay === messageId) ||
|
||||
false
|
||||
}
|
||||
toggleDocumentSelection={toggleDocumentSelection}
|
||||
uniqueSources={uniqueSources}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{content || files ? (
|
||||
<>
|
||||
<FileDisplay files={files || []} />
|
||||
@ -438,81 +490,6 @@ export const AIMessage = ({
|
||||
) : isComplete ? null : (
|
||||
<></>
|
||||
)}
|
||||
{isComplete && docs && docs.length > 0 && (
|
||||
<div className="mt-2 -mx-8 w-full mb-4 flex relative">
|
||||
<div className="w-full">
|
||||
<div className="px-8 flex gap-x-2">
|
||||
{!settings?.isMobile &&
|
||||
filteredDocs.length > 0 &&
|
||||
filteredDocs.slice(0, 2).map((doc, ind) => (
|
||||
<div
|
||||
key={doc.document_id}
|
||||
className={`w-[200px] rounded-lg flex-none transition-all duration-500 hover:bg-background-125 bg-text-100 px-4 pb-2 pt-1 border-b
|
||||
`}
|
||||
>
|
||||
<a
|
||||
href={doc.link || undefined}
|
||||
target="_blank"
|
||||
className="text-sm flex w-full pt-1 gap-x-1.5 overflow-hidden justify-between font-semibold text-text-700"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Citation link={doc.link} index={ind + 1} />
|
||||
<p className="shrink truncate ellipsis break-all">
|
||||
{doc.semantic_identifier ||
|
||||
doc.document_id}
|
||||
</p>
|
||||
<div className="ml-auto flex-none">
|
||||
{doc.is_internet ? (
|
||||
<InternetSearchIcon url={doc.link} />
|
||||
) : (
|
||||
<SourceIcon
|
||||
sourceType={doc.source_type}
|
||||
iconSize={18}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
<div className="flex overscroll-x-scroll mt-.5">
|
||||
<DocumentMetadataBlock document={doc} />
|
||||
</div>
|
||||
<div className="line-clamp-3 text-xs break-words pt-1">
|
||||
{doc.blurb}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
onClick={() => {
|
||||
if (messageId) {
|
||||
onMessageSelection?.(messageId);
|
||||
}
|
||||
toggleDocumentSelection?.();
|
||||
}}
|
||||
key={-1}
|
||||
className="cursor-pointer w-[200px] rounded-lg flex-none transition-all duration-500 hover:bg-background-125 bg-text-100 px-4 py-2 border-b"
|
||||
>
|
||||
<div className="text-sm flex justify-between font-semibold text-text-700">
|
||||
<p className="line-clamp-1">See context</p>
|
||||
<div className="flex gap-x-1">
|
||||
{uniqueSources.map((sourceType, ind) => {
|
||||
return (
|
||||
<div key={ind} className="flex-none">
|
||||
<SourceIcon
|
||||
sourceType={sourceType}
|
||||
iconSize={18}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="line-clamp-3 text-xs break-words pt-1">
|
||||
See more
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{handleFeedback &&
|
||||
|
@ -41,6 +41,7 @@ export function ShowHideDocsButton({
|
||||
}
|
||||
|
||||
export function SearchSummary({
|
||||
index,
|
||||
query,
|
||||
hasDocs,
|
||||
finished,
|
||||
@ -48,6 +49,7 @@ export function SearchSummary({
|
||||
handleShowRetrieved,
|
||||
handleSearchQueryEdit,
|
||||
}: {
|
||||
index: number;
|
||||
finished: boolean;
|
||||
query: string;
|
||||
hasDocs: boolean;
|
||||
@ -98,7 +100,14 @@ export function SearchSummary({
|
||||
!text-sm !line-clamp-1 !break-all px-0.5`}
|
||||
ref={searchingForRef}
|
||||
>
|
||||
{finished ? "Searched" : "Searching"} for: <i> {finalQuery}</i>
|
||||
{finished ? "Searched" : "Searching"} for:{" "}
|
||||
<i>
|
||||
{index === 1
|
||||
? finalQuery.length > 50
|
||||
? `${finalQuery.slice(0, 50)}...`
|
||||
: finalQuery
|
||||
: finalQuery}
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -53,7 +53,7 @@ export const FeedbackModal = ({
|
||||
: predefinedNegativeFeedbackOptions;
|
||||
|
||||
return (
|
||||
<Modal onOutsideClick={onClose} width="max-w-3xl">
|
||||
<Modal onOutsideClick={onClose} width="w-full max-w-3xl">
|
||||
<>
|
||||
<h2 className="text-2xl text-emphasis font-bold mb-4 flex">
|
||||
<div className="mr-1 my-auto">
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useRef } from "react";
|
||||
import { Dispatch, SetStateAction, useContext, useEffect, useRef } from "react";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import Text from "@/components/ui/text";
|
||||
import { getDisplayNameForModel, LlmOverride } from "@/lib/hooks";
|
||||
@ -9,6 +9,10 @@ import { setUserDefaultModel } from "@/lib/users/UserSettings";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/admin/connectors/Field";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
|
||||
export function SetDefaultModelModal({
|
||||
setPopup,
|
||||
@ -23,7 +27,7 @@ export function SetDefaultModelModal({
|
||||
onClose: () => void;
|
||||
defaultModel: string | null;
|
||||
}) {
|
||||
const { refreshUser } = useUser();
|
||||
const { refreshUser, user, updateUserAutoScroll } = useUser();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const messageRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -121,16 +125,41 @@ export function SetDefaultModelModal({
|
||||
const defaultProvider = llmProviders.find(
|
||||
(llmProvider) => llmProvider.is_default_provider
|
||||
);
|
||||
const settings = useContext(SettingsContext);
|
||||
const autoScroll = settings?.enterpriseSettings?.auto_scroll;
|
||||
|
||||
const checked =
|
||||
user?.preferences?.auto_scroll === null
|
||||
? autoScroll
|
||||
: user?.preferences?.auto_scroll;
|
||||
|
||||
return (
|
||||
<Modal onOutsideClick={onClose} width="rounded-lg bg-white max-w-xl">
|
||||
<>
|
||||
<div className="flex mb-4">
|
||||
<h2 className="text-2xl text-emphasis font-bold flex my-auto">
|
||||
Set Default Model
|
||||
User settings
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Switch
|
||||
checked={checked}
|
||||
onCheckedChange={(checked) => {
|
||||
updateUserAutoScroll(checked);
|
||||
}}
|
||||
/>
|
||||
<Label className="text-sm">Enable auto-scroll</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<h3 className="text-lg text-emphasis font-bold">
|
||||
Default model for assistants
|
||||
</h3>
|
||||
|
||||
<Text className="mb-4">
|
||||
Choose a Large Language Model (LLM) to serve as the default for
|
||||
assistants that don't have a default model assigned.
|
||||
|
@ -32,6 +32,7 @@ export default async function Page(props: {
|
||||
defaultAssistantId,
|
||||
shouldShowWelcomeModal,
|
||||
userInputPrompts,
|
||||
ccPairs,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
@ -44,6 +45,9 @@ export default async function Page(props: {
|
||||
value={{
|
||||
chatSessions,
|
||||
availableSources,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
tags,
|
||||
availableDocumentSets: documentSets,
|
||||
availableTags: tags,
|
||||
llmProviders,
|
||||
|
@ -113,7 +113,7 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
{page == "chat" && (
|
||||
<div className="mx-3 mt-4 gap-y-1 flex-col text-text-history-sidebar-button flex gap-x-1.5 items-center items-center">
|
||||
<Link
|
||||
className="w-full p-2 bg-white border-border border rounded items-center hover:bg-background-200 cursor-pointer transition-all duration-150 flex gap-x-2"
|
||||
className=" w-full p-2 bg-white border-border border rounded items-center hover:bg-background-200 cursor-pointer transition-all duration-150 flex gap-x-2"
|
||||
href={
|
||||
`/${page}` +
|
||||
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA &&
|
||||
|
635
web/src/app/chat/shared_chat_search/Filters.tsx
Normal file
635
web/src/app/chat/shared_chat_search/Filters.tsx
Normal file
@ -0,0 +1,635 @@
|
||||
import React, { useState } from "react";
|
||||
import { DocumentSet, Tag, ValidSources } from "@/lib/types";
|
||||
import { SourceMetadata } from "@/lib/search/interfaces";
|
||||
import {
|
||||
GearIcon,
|
||||
InfoIcon,
|
||||
MinusIcon,
|
||||
PlusCircleIcon,
|
||||
PlusIcon,
|
||||
defaultTailwindCSS,
|
||||
} from "@/components/icons/icons";
|
||||
import { HoverPopup } from "@/components/HoverPopup";
|
||||
import {
|
||||
FiBook,
|
||||
FiBookmark,
|
||||
FiFilter,
|
||||
FiMap,
|
||||
FiTag,
|
||||
FiX,
|
||||
} from "react-icons/fi";
|
||||
import { DateRangeSelector } from "@/components/search/DateRangeSelector";
|
||||
import { DateRangePickerValue } from "@/app/ee/admin/performance/DateRangeSelector";
|
||||
import { listSourceMetadata } from "@/lib/sources";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import { TagFilter } from "@/components/search/filtering/TagFilter";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Popover, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { PopoverContent } from "@radix-ui/react-popover";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import { buildDateString, getTimeAgoString } from "@/lib/dateUtils";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { FilterDropdown } from "@/components/search/filtering/FilterDropdown";
|
||||
|
||||
const SectionTitle = ({ children }: { children: string }) => (
|
||||
<div className="font-bold text-xs mt-2 flex">{children}</div>
|
||||
);
|
||||
|
||||
export interface SourceSelectorProps {
|
||||
timeRange: DateRangePickerValue | null;
|
||||
setTimeRange: React.Dispatch<
|
||||
React.SetStateAction<DateRangePickerValue | null>
|
||||
>;
|
||||
showDocSidebar?: boolean;
|
||||
selectedSources: SourceMetadata[];
|
||||
setSelectedSources: React.Dispatch<React.SetStateAction<SourceMetadata[]>>;
|
||||
selectedDocumentSets: string[];
|
||||
setSelectedDocumentSets: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
selectedTags: Tag[];
|
||||
setSelectedTags: React.Dispatch<React.SetStateAction<Tag[]>>;
|
||||
availableDocumentSets: DocumentSet[];
|
||||
existingSources: ValidSources[];
|
||||
availableTags: Tag[];
|
||||
toggleFilters: () => void;
|
||||
filtersUntoggled: boolean;
|
||||
tagsOnLeft: boolean;
|
||||
}
|
||||
|
||||
export function SourceSelector({
|
||||
timeRange,
|
||||
setTimeRange,
|
||||
selectedSources,
|
||||
setSelectedSources,
|
||||
selectedDocumentSets,
|
||||
setSelectedDocumentSets,
|
||||
selectedTags,
|
||||
setSelectedTags,
|
||||
availableDocumentSets,
|
||||
existingSources,
|
||||
availableTags,
|
||||
showDocSidebar,
|
||||
toggleFilters,
|
||||
filtersUntoggled,
|
||||
tagsOnLeft,
|
||||
}: SourceSelectorProps) {
|
||||
const handleSelect = (source: SourceMetadata) => {
|
||||
setSelectedSources((prev: SourceMetadata[]) => {
|
||||
if (
|
||||
prev.map((source) => source.internalName).includes(source.internalName)
|
||||
) {
|
||||
return prev.filter((s) => s.internalName !== source.internalName);
|
||||
} else {
|
||||
return [...prev, source];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDocumentSetSelect = (documentSetName: string) => {
|
||||
setSelectedDocumentSets((prev: string[]) => {
|
||||
if (prev.includes(documentSetName)) {
|
||||
return prev.filter((s) => s !== documentSetName);
|
||||
} else {
|
||||
return [...prev, documentSetName];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let allSourcesSelected = selectedSources.length > 0;
|
||||
|
||||
const toggleAllSources = () => {
|
||||
if (allSourcesSelected) {
|
||||
setSelectedSources([]);
|
||||
} else {
|
||||
const allSources = listSourceMetadata().filter((source) =>
|
||||
existingSources.includes(source.internalName)
|
||||
);
|
||||
setSelectedSources(allSources);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`hidden ${
|
||||
showDocSidebar ? "4xl:block" : "!block"
|
||||
} duration-1000 flex ease-out transition-all transform origin-top-right`}
|
||||
>
|
||||
<button onClick={() => toggleFilters()} className="flex text-emphasis">
|
||||
<h2 className="font-bold my-auto">Filters</h2>
|
||||
<FiFilter className="my-auto ml-2" size="16" />
|
||||
</button>
|
||||
{!filtersUntoggled && (
|
||||
<>
|
||||
<Separator />
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="cursor-pointer">
|
||||
<div className="flex justify-between items-center">
|
||||
<SectionTitle>Time Range</SectionTitle>
|
||||
{true && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setTimeRange(null);
|
||||
}}
|
||||
className="text-sm text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-default mt-2">
|
||||
{getTimeAgoString(timeRange?.from!) || "Select a time range"}
|
||||
</p>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="bg-background-search-filter border-border border rounded-md z-[200] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="range"
|
||||
selected={
|
||||
timeRange
|
||||
? {
|
||||
from: new Date(timeRange.from),
|
||||
to: new Date(timeRange.to),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onSelect={(daterange) => {
|
||||
const initialDate = daterange?.from || new Date();
|
||||
const endDate = daterange?.to || new Date();
|
||||
setTimeRange({
|
||||
from: initialDate,
|
||||
to: endDate,
|
||||
selectValue: timeRange?.selectValue || "",
|
||||
});
|
||||
}}
|
||||
className="rounded-md "
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{availableTags.length > 0 && (
|
||||
<>
|
||||
<div className="mt-4 mb-2">
|
||||
<SectionTitle>Tags</SectionTitle>
|
||||
</div>
|
||||
<TagFilter
|
||||
showTagsOnLeft={true}
|
||||
tags={availableTags}
|
||||
selectedTags={selectedTags}
|
||||
setSelectedTags={setSelectedTags}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{existingSources.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<div className="flex w-full gap-x-2 items-center">
|
||||
<div className="font-bold text-xs mt-2 flex items-center gap-x-2">
|
||||
<p>Sources</p>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allSourcesSelected}
|
||||
onChange={toggleAllSources}
|
||||
className="my-auto form-checkbox h-3 w-3 text-primary border-background-900 rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-1">
|
||||
{listSourceMetadata()
|
||||
.filter((source) =>
|
||||
existingSources.includes(source.internalName)
|
||||
)
|
||||
.map((source) => (
|
||||
<div
|
||||
key={source.internalName}
|
||||
className={
|
||||
"flex cursor-pointer w-full items-center " +
|
||||
"py-1.5 my-1.5 rounded-lg px-2 select-none " +
|
||||
(selectedSources
|
||||
.map((source) => source.internalName)
|
||||
.includes(source.internalName)
|
||||
? "bg-hover"
|
||||
: "hover:bg-hover-light")
|
||||
}
|
||||
onClick={() => handleSelect(source)}
|
||||
>
|
||||
<SourceIcon
|
||||
sourceType={source.internalName}
|
||||
iconSize={16}
|
||||
/>
|
||||
<span className="ml-2 text-sm text-default">
|
||||
{source.displayName}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{availableDocumentSets.length > 0 && (
|
||||
<>
|
||||
<div className="mt-4">
|
||||
<SectionTitle>Knowledge Sets</SectionTitle>
|
||||
</div>
|
||||
<div className="px-1">
|
||||
{availableDocumentSets.map((documentSet) => (
|
||||
<div key={documentSet.name} className="my-1.5 flex">
|
||||
<div
|
||||
key={documentSet.name}
|
||||
className={
|
||||
"flex cursor-pointer w-full items-center " +
|
||||
"py-1.5 rounded-lg px-2 " +
|
||||
(selectedDocumentSets.includes(documentSet.name)
|
||||
? "bg-hover"
|
||||
: "hover:bg-hover-light")
|
||||
}
|
||||
onClick={() => handleDocumentSetSelect(documentSet.name)}
|
||||
>
|
||||
<HoverPopup
|
||||
mainContent={
|
||||
<div className="flex my-auto mr-2">
|
||||
<InfoIcon className={defaultTailwindCSS} />
|
||||
</div>
|
||||
}
|
||||
popupContent={
|
||||
<div className="text-sm w-64">
|
||||
<div className="flex font-medium">Description</div>
|
||||
<div className="mt-1">
|
||||
{documentSet.description}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
classNameModifications="-ml-2"
|
||||
/>
|
||||
<span className="text-sm">{documentSet.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SelectedBubble({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: string | JSX.Element;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"flex cursor-pointer items-center border border-border " +
|
||||
"py-1 my-1.5 rounded-lg px-2 w-fit hover:bg-hover"
|
||||
}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
<FiX className="ml-2" size={14} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HorizontalFilters({
|
||||
timeRange,
|
||||
setTimeRange,
|
||||
selectedSources,
|
||||
setSelectedSources,
|
||||
selectedDocumentSets,
|
||||
setSelectedDocumentSets,
|
||||
availableDocumentSets,
|
||||
existingSources,
|
||||
}: SourceSelectorProps) {
|
||||
const handleSourceSelect = (source: SourceMetadata) => {
|
||||
setSelectedSources((prev: SourceMetadata[]) => {
|
||||
const prevSourceNames = prev.map((source) => source.internalName);
|
||||
if (prevSourceNames.includes(source.internalName)) {
|
||||
return prev.filter((s) => s.internalName !== source.internalName);
|
||||
} else {
|
||||
return [...prev, source];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDocumentSetSelect = (documentSetName: string) => {
|
||||
setSelectedDocumentSets((prev: string[]) => {
|
||||
if (prev.includes(documentSetName)) {
|
||||
return prev.filter((s) => s !== documentSetName);
|
||||
} else {
|
||||
return [...prev, documentSetName];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const allSources = listSourceMetadata();
|
||||
const availableSources = allSources.filter((source) =>
|
||||
existingSources.includes(source.internalName)
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-x-3">
|
||||
<div className="w-64">
|
||||
<DateRangeSelector value={timeRange} onValueChange={setTimeRange} />
|
||||
</div>
|
||||
|
||||
<FilterDropdown
|
||||
options={availableSources.map((source) => {
|
||||
return {
|
||||
key: source.displayName,
|
||||
display: (
|
||||
<>
|
||||
<SourceIcon sourceType={source.internalName} iconSize={16} />
|
||||
<span className="ml-2 text-sm">{source.displayName}</span>
|
||||
</>
|
||||
),
|
||||
};
|
||||
})}
|
||||
selected={selectedSources.map((source) => source.displayName)}
|
||||
handleSelect={(option) =>
|
||||
handleSourceSelect(
|
||||
allSources.find((source) => source.displayName === option.key)!
|
||||
)
|
||||
}
|
||||
icon={
|
||||
<div className="my-auto mr-2 w-[16px] h-[16px]">
|
||||
<FiMap size={16} />
|
||||
</div>
|
||||
}
|
||||
defaultDisplay="All Sources"
|
||||
/>
|
||||
|
||||
<FilterDropdown
|
||||
options={availableDocumentSets.map((documentSet) => {
|
||||
return {
|
||||
key: documentSet.name,
|
||||
display: (
|
||||
<>
|
||||
<div className="my-auto">
|
||||
<FiBookmark />
|
||||
</div>
|
||||
<span className="ml-2 text-sm">{documentSet.name}</span>
|
||||
</>
|
||||
),
|
||||
};
|
||||
})}
|
||||
selected={selectedDocumentSets}
|
||||
handleSelect={(option) => handleDocumentSetSelect(option.key)}
|
||||
icon={
|
||||
<div className="my-auto mr-2 w-[16px] h-[16px]">
|
||||
<FiBook size={16} />
|
||||
</div>
|
||||
}
|
||||
defaultDisplay="All Document Sets"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex pb-4 mt-2 h-12">
|
||||
<div className="flex flex-wrap gap-x-2">
|
||||
{timeRange && timeRange.selectValue && (
|
||||
<SelectedBubble onClick={() => setTimeRange(null)}>
|
||||
<div className="text-sm flex">{timeRange.selectValue}</div>
|
||||
</SelectedBubble>
|
||||
)}
|
||||
{existingSources.length > 0 &&
|
||||
selectedSources.map((source) => (
|
||||
<SelectedBubble
|
||||
key={source.internalName}
|
||||
onClick={() => handleSourceSelect(source)}
|
||||
>
|
||||
<>
|
||||
<SourceIcon sourceType={source.internalName} iconSize={16} />
|
||||
<span className="ml-2 text-sm">{source.displayName}</span>
|
||||
</>
|
||||
</SelectedBubble>
|
||||
))}
|
||||
{selectedDocumentSets.length > 0 &&
|
||||
selectedDocumentSets.map((documentSetName) => (
|
||||
<SelectedBubble
|
||||
key={documentSetName}
|
||||
onClick={() => handleDocumentSetSelect(documentSetName)}
|
||||
>
|
||||
<>
|
||||
<div>
|
||||
<FiBookmark />
|
||||
</div>
|
||||
<span className="ml-2 text-sm">{documentSetName}</span>
|
||||
</>
|
||||
</SelectedBubble>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HorizontalSourceSelector({
|
||||
timeRange,
|
||||
setTimeRange,
|
||||
selectedSources,
|
||||
setSelectedSources,
|
||||
selectedDocumentSets,
|
||||
setSelectedDocumentSets,
|
||||
selectedTags,
|
||||
setSelectedTags,
|
||||
availableDocumentSets,
|
||||
existingSources,
|
||||
availableTags,
|
||||
}: SourceSelectorProps) {
|
||||
const handleSourceSelect = (source: SourceMetadata) => {
|
||||
setSelectedSources((prev: SourceMetadata[]) => {
|
||||
if (prev.map((s) => s.internalName).includes(source.internalName)) {
|
||||
return prev.filter((s) => s.internalName !== source.internalName);
|
||||
} else {
|
||||
return [...prev, source];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDocumentSetSelect = (documentSetName: string) => {
|
||||
setSelectedDocumentSets((prev: string[]) => {
|
||||
if (prev.includes(documentSetName)) {
|
||||
return prev.filter((s) => s !== documentSetName);
|
||||
} else {
|
||||
return [...prev, documentSetName];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleTagSelect = (tag: Tag) => {
|
||||
setSelectedTags((prev: Tag[]) => {
|
||||
if (
|
||||
prev.some(
|
||||
(t) => t.tag_key === tag.tag_key && t.tag_value === tag.tag_value
|
||||
)
|
||||
) {
|
||||
return prev.filter(
|
||||
(t) => !(t.tag_key === tag.tag_key && t.tag_value === tag.tag_value)
|
||||
);
|
||||
} else {
|
||||
return [...prev, tag];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resetSources = () => {
|
||||
setSelectedSources([]);
|
||||
};
|
||||
const resetDocuments = () => {
|
||||
setSelectedDocumentSets([]);
|
||||
};
|
||||
|
||||
const resetTags = () => {
|
||||
setSelectedTags([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-nowrap space-x-2">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div
|
||||
className={`
|
||||
border
|
||||
max-w-36
|
||||
border-border
|
||||
rounded-lg
|
||||
max-h-96
|
||||
overflow-y-scroll
|
||||
overscroll-contain
|
||||
px-3
|
||||
text-sm
|
||||
py-1.5
|
||||
select-none
|
||||
cursor-pointer
|
||||
w-fit
|
||||
gap-x-1
|
||||
hover:bg-hover
|
||||
flex
|
||||
items-center
|
||||
bg-background-search-filter
|
||||
`}
|
||||
>
|
||||
<CalendarIcon className="h-4 w-4" />
|
||||
|
||||
{timeRange?.from ? getTimeAgoString(timeRange.from) : "Since"}
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="bg-background-search-filter border-border border rounded-md z-[200] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="range"
|
||||
selected={
|
||||
timeRange
|
||||
? { from: new Date(timeRange.from), to: new Date(timeRange.to) }
|
||||
: undefined
|
||||
}
|
||||
onSelect={(daterange) => {
|
||||
const initialDate = daterange?.from || new Date();
|
||||
const endDate = daterange?.to || new Date();
|
||||
setTimeRange({
|
||||
from: initialDate,
|
||||
to: endDate,
|
||||
selectValue: timeRange?.selectValue || "",
|
||||
});
|
||||
}}
|
||||
className="rounded-md"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{existingSources.length > 0 && (
|
||||
<FilterDropdown
|
||||
backgroundColor="bg-background-search-filter"
|
||||
options={listSourceMetadata()
|
||||
.filter((source) => existingSources.includes(source.internalName))
|
||||
.map((source) => ({
|
||||
key: source.internalName,
|
||||
display: (
|
||||
<>
|
||||
<SourceIcon sourceType={source.internalName} iconSize={16} />
|
||||
<span className="ml-2 text-sm">{source.displayName}</span>
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
selected={selectedSources.map((source) => source.internalName)}
|
||||
handleSelect={(option) =>
|
||||
handleSourceSelect(
|
||||
listSourceMetadata().find((s) => s.internalName === option.key)!
|
||||
)
|
||||
}
|
||||
icon={<FiMap size={16} />}
|
||||
defaultDisplay="Sources"
|
||||
dropdownColor="bg-background-search-filter-dropdown"
|
||||
width="w-fit ellipsis truncate"
|
||||
resetValues={resetSources}
|
||||
dropdownWidth="w-40"
|
||||
optionClassName="truncate w-full break-all ellipsis"
|
||||
/>
|
||||
)}
|
||||
|
||||
{availableDocumentSets.length > 0 && (
|
||||
<FilterDropdown
|
||||
backgroundColor="bg-background-search-filter"
|
||||
options={availableDocumentSets.map((documentSet) => ({
|
||||
key: documentSet.name,
|
||||
display: <>{documentSet.name}</>,
|
||||
}))}
|
||||
selected={selectedDocumentSets}
|
||||
handleSelect={(option) => handleDocumentSetSelect(option.key)}
|
||||
icon={<FiBook size={16} />}
|
||||
defaultDisplay="Sets"
|
||||
resetValues={resetDocuments}
|
||||
width="w-fit max-w-24 text-ellipsis truncate"
|
||||
dropdownColor="bg-background-search-filter-dropdown"
|
||||
dropdownWidth="max-w-36 w-fit"
|
||||
optionClassName="truncate w-full break-all"
|
||||
/>
|
||||
)}
|
||||
|
||||
{availableTags.length > 0 && (
|
||||
<FilterDropdown
|
||||
backgroundColor="bg-background-search-filter"
|
||||
options={availableTags.map((tag) => ({
|
||||
key: `${tag.tag_key}=${tag.tag_value}`,
|
||||
display: (
|
||||
<span className="text-sm">
|
||||
{tag.tag_key}
|
||||
<b>=</b>
|
||||
{tag.tag_value}
|
||||
</span>
|
||||
),
|
||||
}))}
|
||||
selected={selectedTags.map(
|
||||
(tag) => `${tag.tag_key}=${tag.tag_value}`
|
||||
)}
|
||||
handleSelect={(option) => {
|
||||
const [tag_key, tag_value] = option.key.split("=");
|
||||
const selectedTag = availableTags.find(
|
||||
(tag) => tag.tag_key === tag_key && tag.tag_value === tag_value
|
||||
);
|
||||
if (selectedTag) {
|
||||
handleTagSelect(selectedTag);
|
||||
}
|
||||
}}
|
||||
icon={<FiTag size={16} />}
|
||||
defaultDisplay="Tags"
|
||||
resetValues={resetTags}
|
||||
dropdownColor="bg-background-search-filter-dropdown"
|
||||
width="w-fit max-w-24 ellipsis truncate"
|
||||
dropdownWidth="max-w-80 w-fit"
|
||||
optionClassName="truncate w-full break-all ellipsis"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -21,9 +21,7 @@ export default function FixedLogo({
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
href={
|
||||
settings && settings.default_page === "chat" ? "/chat" : "/search"
|
||||
}
|
||||
href="/chat"
|
||||
className="fixed cursor-pointer flex z-40 left-2.5 top-2"
|
||||
>
|
||||
<div className="max-w-[200px] mobile:hidden flex items-center gap-x-1 my-auto">
|
||||
@ -49,7 +47,7 @@ export default function FixedLogo({
|
||||
</div>
|
||||
</Link>
|
||||
<div className="mobile:hidden fixed left-2.5 bottom-4">
|
||||
<FiSidebar className="text-text-mobile-sidebar" />
|
||||
{/* <FiSidebar className="text-text-mobile-sidebar" /> */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -1,90 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { ReactNode, useContext, useEffect, useState } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { ChatIcon, SearchIcon } from "@/components/icons/icons";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import KeyboardSymbol from "@/lib/browserUtilities";
|
||||
|
||||
const ToggleSwitch = () => {
|
||||
const commandSymbol = KeyboardSymbol();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const settings = useContext(SettingsContext);
|
||||
|
||||
const [activeTab, setActiveTab] = useState(() => {
|
||||
return pathname == "/search" ? "search" : "chat";
|
||||
});
|
||||
|
||||
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const newTab = pathname === "/search" ? "search" : "chat";
|
||||
setActiveTab(newTab);
|
||||
localStorage.setItem("activeTab", newTab);
|
||||
setIsInitialLoad(false);
|
||||
}, [pathname]);
|
||||
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab);
|
||||
localStorage.setItem("activeTab", tab);
|
||||
if (settings?.isMobile && window) {
|
||||
window.location.href = tab;
|
||||
} else {
|
||||
router.push(tab === "search" ? "/search" : "/chat");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-background-toggle mobile:mt-8 flex rounded-full p-1">
|
||||
<div
|
||||
className={`absolute mobile:mt-8 top-1 bottom-1 ${
|
||||
activeTab === "chat" ? "w-[45%]" : "w-[50%]"
|
||||
} bg-white rounded-full shadow ${
|
||||
isInitialLoad ? "" : "transition-transform duration-300 ease-in-out"
|
||||
} ${activeTab === "chat" ? "translate-x-[115%]" : "translate-x-[1%]"}`}
|
||||
/>
|
||||
<button
|
||||
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors duration-300 ease-in-out flex items-center relative z-10 ${
|
||||
activeTab === "search"
|
||||
? "text-text-application-toggled"
|
||||
: "text-text-application-untoggled hover:text-text-application-untoggled-hover"
|
||||
}`}
|
||||
onClick={() => handleTabChange("search")}
|
||||
>
|
||||
<SearchIcon size={16} className="mr-2" />
|
||||
<div className="flex items-center">
|
||||
Search
|
||||
<div className="ml-2 flex content-center">
|
||||
<span className="leading-none pb-[1px] my-auto">
|
||||
{commandSymbol}
|
||||
</span>
|
||||
<span className="my-auto">S</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors duration-300 ease-in-out flex items-center relative z-10 ${
|
||||
activeTab === "chat"
|
||||
? "text-text-application-toggled"
|
||||
: "text-text-application-untoggled hover:text-text-application-untoggled-hover"
|
||||
}`}
|
||||
onClick={() => handleTabChange("chat")}
|
||||
>
|
||||
<ChatIcon size={16} className="mr-2" />
|
||||
<div className="items-end flex">
|
||||
Chat
|
||||
<div className="ml-2 flex content-center">
|
||||
<span className="leading-none pb-[1px] my-auto">
|
||||
{commandSymbol}
|
||||
</span>
|
||||
<span className="my-auto">D</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import React, { ReactNode, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function FunctionalWrapper({
|
||||
initiallyToggled,
|
||||
@ -128,12 +45,6 @@ export default function FunctionalWrapper({
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [router]);
|
||||
const combinedSettings = useContext(SettingsContext);
|
||||
const settings = combinedSettings?.settings;
|
||||
const chatBannerPresent =
|
||||
combinedSettings?.enterpriseSettings?.custom_header_content;
|
||||
const twoLines =
|
||||
combinedSettings?.enterpriseSettings?.two_lines_for_chat_header;
|
||||
|
||||
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
|
||||
|
||||
@ -145,24 +56,7 @@ export default function FunctionalWrapper({
|
||||
|
||||
return (
|
||||
<>
|
||||
{(!settings ||
|
||||
(settings.search_page_enabled && settings.chat_page_enabled)) && (
|
||||
<div
|
||||
className={`mobile:hidden z-30 flex fixed ${
|
||||
chatBannerPresent ? (twoLines ? "top-20" : "top-14") : "top-4"
|
||||
} left-1/2 transform -translate-x-1/2`}
|
||||
>
|
||||
<div
|
||||
style={{ transition: "width 0.30s ease-out" }}
|
||||
className={`flex-none overflow-y-hidden bg-background-100 transition-all bg-opacity-80 duration-300 ease-in-out h-full
|
||||
${toggledSidebar ? "w-[250px] " : "w-[0px]"}`}
|
||||
/>
|
||||
<div className="relative">
|
||||
<ToggleSwitch />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{" "}
|
||||
<div className="overscroll-y-contain overflow-y-scroll overscroll-contain left-0 top-0 w-full h-svh">
|
||||
{content(toggledSidebar, toggle)}
|
||||
</div>
|
||||
|
294
web/src/app/chat/shared_chat_search/SearchFilters.tsx
Normal file
294
web/src/app/chat/shared_chat_search/SearchFilters.tsx
Normal file
@ -0,0 +1,294 @@
|
||||
import { DocumentSet, Tag, ValidSources } from "@/lib/types";
|
||||
import { SourceMetadata } from "@/lib/search/interfaces";
|
||||
import { InfoIcon, defaultTailwindCSS } from "@/components/icons/icons";
|
||||
import { HoverPopup } from "@/components/HoverPopup";
|
||||
import { DateRangePickerValue } from "@/app/ee/admin/performance/DateRangeSelector";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { TagFilter } from "@/components/search/filtering/TagFilter";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { listSourceMetadata } from "@/lib/sources";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { getDateRangeString } from "@/lib/dateUtils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ToolTipDetails } from "@/components/admin/connectors/Field";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
|
||||
const SectionTitle = ({
|
||||
children,
|
||||
modal,
|
||||
}: {
|
||||
children: string;
|
||||
modal?: boolean;
|
||||
}) => (
|
||||
<div className={`mt-4 pb-2 ${modal ? "w-[80vw]" : "w-full"}`}>
|
||||
<p className="text-sm font-semibold">{children}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export interface SourceSelectorProps {
|
||||
timeRange: DateRangePickerValue | null;
|
||||
setTimeRange: React.Dispatch<
|
||||
React.SetStateAction<DateRangePickerValue | null>
|
||||
>;
|
||||
showDocSidebar?: boolean;
|
||||
selectedSources: SourceMetadata[];
|
||||
setSelectedSources: React.Dispatch<React.SetStateAction<SourceMetadata[]>>;
|
||||
selectedDocumentSets: string[];
|
||||
setSelectedDocumentSets: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
selectedTags: Tag[];
|
||||
setSelectedTags: React.Dispatch<React.SetStateAction<Tag[]>>;
|
||||
availableDocumentSets: DocumentSet[];
|
||||
existingSources: ValidSources[];
|
||||
availableTags: Tag[];
|
||||
filtersUntoggled: boolean;
|
||||
modal?: boolean;
|
||||
tagsOnLeft: boolean;
|
||||
}
|
||||
|
||||
export function SourceSelector({
|
||||
timeRange,
|
||||
filtersUntoggled,
|
||||
setTimeRange,
|
||||
selectedSources,
|
||||
setSelectedSources,
|
||||
selectedDocumentSets,
|
||||
setSelectedDocumentSets,
|
||||
selectedTags,
|
||||
setSelectedTags,
|
||||
availableDocumentSets,
|
||||
existingSources,
|
||||
modal,
|
||||
availableTags,
|
||||
}: SourceSelectorProps) {
|
||||
const handleSelect = (source: SourceMetadata) => {
|
||||
setSelectedSources((prev: SourceMetadata[]) => {
|
||||
if (
|
||||
prev.map((source) => source.internalName).includes(source.internalName)
|
||||
) {
|
||||
return prev.filter((s) => s.internalName !== source.internalName);
|
||||
} else {
|
||||
return [...prev, source];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDocumentSetSelect = (documentSetName: string) => {
|
||||
setSelectedDocumentSets((prev: string[]) => {
|
||||
if (prev.includes(documentSetName)) {
|
||||
return prev.filter((s) => s !== documentSetName);
|
||||
} else {
|
||||
return [...prev, documentSetName];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let allSourcesSelected = selectedSources.length > 0;
|
||||
|
||||
const toggleAllSources = () => {
|
||||
if (allSourcesSelected) {
|
||||
setSelectedSources([]);
|
||||
} else {
|
||||
const allSources = listSourceMetadata().filter((source) =>
|
||||
existingSources.includes(source.internalName)
|
||||
);
|
||||
setSelectedSources(allSources);
|
||||
}
|
||||
};
|
||||
|
||||
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const calendar = document.querySelector(".rdp");
|
||||
if (calendar && !calendar.contains(event.target as Node)) {
|
||||
setIsCalendarOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!filtersUntoggled && (
|
||||
<CardContent className=" space-y-2">
|
||||
<div>
|
||||
<div className="flex py-2 mt-2 justify-start gap-x-2 items-center">
|
||||
<p className="text-sm font-semibold">Time Range</p>
|
||||
{timeRange && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setTimeRange(null);
|
||||
}}
|
||||
className="text-sm text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`w-full justify-start text-left font-normal`}
|
||||
>
|
||||
<span>
|
||||
{getDateRangeString(timeRange?.from!, timeRange?.to!) ||
|
||||
"Select a time range"}
|
||||
</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="z-[10000] w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="range"
|
||||
selected={
|
||||
timeRange
|
||||
? {
|
||||
from: new Date(timeRange.from),
|
||||
to: new Date(timeRange.to),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onSelect={(daterange) => {
|
||||
const today = new Date();
|
||||
const initialDate = daterange?.from
|
||||
? new Date(
|
||||
Math.min(daterange.from.getTime(), today.getTime())
|
||||
)
|
||||
: today;
|
||||
const endDate = daterange?.to
|
||||
? new Date(
|
||||
Math.min(daterange.to.getTime(), today.getTime())
|
||||
)
|
||||
: today;
|
||||
setTimeRange({
|
||||
from: initialDate,
|
||||
to: endDate,
|
||||
selectValue: timeRange?.selectValue || "",
|
||||
});
|
||||
}}
|
||||
className="rounded-md"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{availableTags.length > 0 && (
|
||||
<div>
|
||||
<SectionTitle modal={modal}>Tags</SectionTitle>
|
||||
<TagFilter
|
||||
modal={modal}
|
||||
showTagsOnLeft={true}
|
||||
tags={availableTags}
|
||||
selectedTags={selectedTags}
|
||||
setSelectedTags={setSelectedTags}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{existingSources.length > 0 && (
|
||||
<div>
|
||||
<SectionTitle modal={modal}>Sources</SectionTitle>
|
||||
|
||||
<div className="space-y-0">
|
||||
{existingSources.length > 1 && (
|
||||
<div className="flex items-center space-x-2 cursor-pointer hover:bg-background-200 rounded-md p-2">
|
||||
<Checkbox
|
||||
id="select-all-sources"
|
||||
checked={allSourcesSelected}
|
||||
onCheckedChange={toggleAllSources}
|
||||
/>
|
||||
|
||||
<label
|
||||
htmlFor="select-all-sources"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Select All
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{listSourceMetadata()
|
||||
.filter((source) =>
|
||||
existingSources.includes(source.internalName)
|
||||
)
|
||||
.map((source) => (
|
||||
<div
|
||||
key={source.internalName}
|
||||
className="flex items-center space-x-2 cursor-pointer hover:bg-background-200 rounded-md p-2"
|
||||
onClick={() => handleSelect(source)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedSources
|
||||
.map((s) => s.internalName)
|
||||
.includes(source.internalName)}
|
||||
/>
|
||||
<SourceIcon
|
||||
sourceType={source.internalName}
|
||||
iconSize={16}
|
||||
/>
|
||||
<span className="text-sm">{source.displayName}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{availableDocumentSets.length > 0 && (
|
||||
<div>
|
||||
<SectionTitle modal={modal}>Knowledge Sets</SectionTitle>
|
||||
<div className="space-y-2">
|
||||
{availableDocumentSets.map((documentSet) => (
|
||||
<div
|
||||
key={documentSet.name}
|
||||
className="flex items-center space-x-2 cursor-pointer hover:bg-background-200 rounded-md p-2"
|
||||
onClick={() => handleDocumentSetSelect(documentSet.name)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedDocumentSets.includes(documentSet.name)}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon
|
||||
className={`${defaultTailwindCSS} h-4 w-4`}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="text-sm w-64">
|
||||
<div className="font-medium">Description</div>
|
||||
<div className="mt-1">
|
||||
{documentSet.description}
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<span className="text-sm">{documentSet.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -55,6 +55,7 @@ export function WhitelabelingForm() {
|
||||
<div>
|
||||
<Formik
|
||||
initialValues={{
|
||||
auto_scroll: enterpriseSettings?.auto_scroll || false,
|
||||
application_name: enterpriseSettings?.application_name || null,
|
||||
use_custom_logo: enterpriseSettings?.use_custom_logo || false,
|
||||
use_custom_logotype: enterpriseSettings?.use_custom_logotype || false,
|
||||
@ -71,6 +72,7 @@ export function WhitelabelingForm() {
|
||||
enterpriseSettings?.enable_consent_screen || false,
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
auto_scroll: Yup.boolean().nullable(),
|
||||
application_name: Yup.string().nullable(),
|
||||
use_custom_logo: Yup.boolean().required(),
|
||||
use_custom_logotype: Yup.boolean().required(),
|
||||
|
@ -1,15 +1,5 @@
|
||||
import { fetchSettingsSS } from "@/components/settings/lib";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function Page() {
|
||||
const settings = await fetchSettingsSS();
|
||||
if (!settings) {
|
||||
redirect("/search");
|
||||
}
|
||||
|
||||
if (settings.settings.default_page === "search") {
|
||||
redirect("/search");
|
||||
} else {
|
||||
redirect("/chat");
|
||||
}
|
||||
redirect("/chat");
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
"use client";
|
||||
import { SearchSection } from "@/components/search/SearchSection";
|
||||
import FunctionalWrapper from "../chat/shared_chat_search/FunctionalWrapper";
|
||||
|
||||
export default function WrappedSearch({
|
||||
searchTypeDefault,
|
||||
initiallyToggled,
|
||||
}: {
|
||||
searchTypeDefault: string;
|
||||
initiallyToggled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<FunctionalWrapper
|
||||
initiallyToggled={initiallyToggled}
|
||||
content={(toggledSidebar, toggle) => (
|
||||
<SearchSection
|
||||
toggle={toggle}
|
||||
toggledSidebar={toggledSidebar}
|
||||
defaultSearchType={searchTypeDefault}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,213 +0,0 @@
|
||||
import {
|
||||
AuthTypeMetadata,
|
||||
getAuthTypeMetadataSS,
|
||||
getCurrentUserSS,
|
||||
} from "@/lib/userSS";
|
||||
import { redirect } from "next/navigation";
|
||||
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||
import { fetchSS } from "@/lib/utilsSS";
|
||||
import { CCPairBasicInfo, DocumentSet, Tag, User } from "@/lib/types";
|
||||
import { cookies } from "next/headers";
|
||||
import { SearchType } from "@/lib/search/interfaces";
|
||||
import { Persona } from "../admin/assistants/interfaces";
|
||||
import { unstable_noStore as noStore } from "next/cache";
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
import { personaComparator } from "../admin/assistants/lib";
|
||||
import { FullEmbeddingModelResponse } from "@/components/embedding/interfaces";
|
||||
import { ChatPopup } from "../chat/ChatPopup";
|
||||
import {
|
||||
FetchAssistantsResponse,
|
||||
fetchAssistantsSS,
|
||||
} from "@/lib/assistants/fetchAssistantsSS";
|
||||
import { ChatSession } from "../chat/interfaces";
|
||||
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants";
|
||||
import {
|
||||
AGENTIC_SEARCH_TYPE_COOKIE_NAME,
|
||||
NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN,
|
||||
DISABLE_LLM_DOC_RELEVANCE,
|
||||
} from "@/lib/constants";
|
||||
import WrappedSearch from "./WrappedSearch";
|
||||
import { SearchProvider } from "@/components/context/SearchContext";
|
||||
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
|
||||
import { LLMProviderDescriptor } from "../admin/configuration/llm/interfaces";
|
||||
import { headers } from "next/headers";
|
||||
import {
|
||||
hasCompletedWelcomeFlowSS,
|
||||
WelcomeModal,
|
||||
} from "@/components/initialSetup/welcome/WelcomeModalWrapper";
|
||||
|
||||
export default async function Home(props: {
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
}) {
|
||||
const searchParams = await props.searchParams;
|
||||
// Disable caching so we always get the up to date connector / document set / persona info
|
||||
// importantly, this prevents users from adding a connector, going back to the main page,
|
||||
// and then getting hit with a "No Connectors" popup
|
||||
noStore();
|
||||
const requestCookies = await cookies();
|
||||
const tasks = [
|
||||
getAuthTypeMetadataSS(),
|
||||
getCurrentUserSS(),
|
||||
fetchSS("/manage/indexing-status"),
|
||||
fetchSS("/manage/document-set"),
|
||||
fetchAssistantsSS(),
|
||||
fetchSS("/query/valid-tags"),
|
||||
fetchSS("/query/user-searches"),
|
||||
fetchLLMProvidersSS(),
|
||||
];
|
||||
|
||||
// catch cases where the backend is completely unreachable here
|
||||
// without try / catch, will just raise an exception and the page
|
||||
// will not render
|
||||
let results: (
|
||||
| User
|
||||
| Response
|
||||
| AuthTypeMetadata
|
||||
| FullEmbeddingModelResponse
|
||||
| FetchAssistantsResponse
|
||||
| LLMProviderDescriptor[]
|
||||
| null
|
||||
)[] = [null, null, null, null, null, null, null, null];
|
||||
try {
|
||||
results = await Promise.all(tasks);
|
||||
} catch (e) {
|
||||
console.log(`Some fetch failed for the main search page - ${e}`);
|
||||
}
|
||||
const authTypeMetadata = results[0] as AuthTypeMetadata | null;
|
||||
const user = results[1] as User | null;
|
||||
const ccPairsResponse = results[2] as Response | null;
|
||||
const documentSetsResponse = results[3] as Response | null;
|
||||
const [initialAssistantsList, assistantsFetchError] =
|
||||
results[4] as FetchAssistantsResponse;
|
||||
const tagsResponse = results[5] as Response | null;
|
||||
const queryResponse = results[6] as Response | null;
|
||||
const llmProviders = (results[7] || []) as LLMProviderDescriptor[];
|
||||
|
||||
const authDisabled = authTypeMetadata?.authType === "disabled";
|
||||
|
||||
if (!authDisabled && !user) {
|
||||
const headersList = await headers();
|
||||
const fullUrl = headersList.get("x-url") || "/search";
|
||||
const searchParamsString = new URLSearchParams(
|
||||
searchParams as unknown as Record<string, string>
|
||||
).toString();
|
||||
const redirectUrl = searchParamsString
|
||||
? `${fullUrl}?${searchParamsString}`
|
||||
: fullUrl;
|
||||
return redirect(`/auth/login?next=${encodeURIComponent(redirectUrl)}`);
|
||||
}
|
||||
|
||||
if (user && !user.is_verified && authTypeMetadata?.requiresVerification) {
|
||||
return redirect("/auth/waiting-on-verification");
|
||||
}
|
||||
|
||||
let ccPairs: CCPairBasicInfo[] = [];
|
||||
if (ccPairsResponse?.ok) {
|
||||
ccPairs = await ccPairsResponse.json();
|
||||
} else {
|
||||
console.log(`Failed to fetch connectors - ${ccPairsResponse?.status}`);
|
||||
}
|
||||
|
||||
let documentSets: DocumentSet[] = [];
|
||||
if (documentSetsResponse?.ok) {
|
||||
documentSets = await documentSetsResponse.json();
|
||||
} else {
|
||||
console.log(
|
||||
`Failed to fetch document sets - ${documentSetsResponse?.status}`
|
||||
);
|
||||
}
|
||||
|
||||
let querySessions: ChatSession[] = [];
|
||||
if (queryResponse?.ok) {
|
||||
querySessions = (await queryResponse.json()).sessions;
|
||||
} else {
|
||||
console.log(`Failed to fetch chat sessions - ${queryResponse?.text()}`);
|
||||
}
|
||||
|
||||
let assistants: Persona[] = initialAssistantsList;
|
||||
if (assistantsFetchError) {
|
||||
console.log(`Failed to fetch assistants - ${assistantsFetchError}`);
|
||||
} else {
|
||||
// remove those marked as hidden by an admin
|
||||
assistants = assistants.filter((assistant) => assistant.is_visible);
|
||||
// hide personas with no retrieval
|
||||
assistants = assistants.filter((assistant) => assistant.num_chunks !== 0);
|
||||
// sort them in priority order
|
||||
assistants.sort(personaComparator);
|
||||
}
|
||||
|
||||
let tags: Tag[] = [];
|
||||
if (tagsResponse?.ok) {
|
||||
tags = (await tagsResponse.json()).tags;
|
||||
} else {
|
||||
console.log(`Failed to fetch tags - ${tagsResponse?.status}`);
|
||||
}
|
||||
|
||||
// needs to be done in a non-client side component due to nextjs
|
||||
const storedSearchType = requestCookies.get("searchType")?.value as
|
||||
| string
|
||||
| undefined;
|
||||
const searchTypeDefault: SearchType =
|
||||
storedSearchType !== undefined &&
|
||||
SearchType.hasOwnProperty(storedSearchType)
|
||||
? (storedSearchType as SearchType)
|
||||
: SearchType.SEMANTIC; // default to semantic
|
||||
|
||||
const hasAnyConnectors = ccPairs.length > 0;
|
||||
|
||||
const shouldShowWelcomeModal =
|
||||
!llmProviders.length &&
|
||||
!hasCompletedWelcomeFlowSS(requestCookies) &&
|
||||
!hasAnyConnectors &&
|
||||
(!user || user.role === "admin");
|
||||
|
||||
const shouldDisplayNoSourcesModal =
|
||||
(!user || user.role === "admin") &&
|
||||
ccPairs.length === 0 &&
|
||||
!shouldShowWelcomeModal;
|
||||
|
||||
const sidebarToggled = requestCookies.get(SIDEBAR_TOGGLED_COOKIE_NAME);
|
||||
const agenticSearchToggle = requestCookies.get(
|
||||
AGENTIC_SEARCH_TYPE_COOKIE_NAME
|
||||
);
|
||||
|
||||
const toggleSidebar = sidebarToggled
|
||||
? sidebarToggled.value.toLocaleLowerCase() == "true" || false
|
||||
: NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN;
|
||||
|
||||
const agenticSearchEnabled = agenticSearchToggle
|
||||
? agenticSearchToggle.value.toLocaleLowerCase() == "true" || false
|
||||
: false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HealthCheckBanner />
|
||||
<InstantSSRAutoRefresh />
|
||||
{shouldShowWelcomeModal && (
|
||||
<WelcomeModal user={user} requestCookies={requestCookies} />
|
||||
)}
|
||||
{/* ChatPopup is a custom popup that displays a admin-specified message on initial user visit.
|
||||
Only used in the EE version of the app. */}
|
||||
<ChatPopup />
|
||||
<SearchProvider
|
||||
value={{
|
||||
querySessions,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
assistants,
|
||||
tags,
|
||||
agenticSearchEnabled,
|
||||
disabledAgentic: DISABLE_LLM_DOC_RELEVANCE,
|
||||
initiallyToggled: toggleSidebar,
|
||||
shouldShowWelcomeModal,
|
||||
shouldDisplayNoSources: shouldDisplayNoSourcesModal,
|
||||
}}
|
||||
>
|
||||
<WrappedSearch
|
||||
initiallyToggled={toggleSidebar}
|
||||
searchTypeDefault={searchTypeDefault}
|
||||
/>
|
||||
</SearchProvider>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
export function InternetSearchIcon({ url }: { url: string }) {
|
||||
return (
|
||||
<img
|
||||
className="rounded-full w-[18px] h-[18px]"
|
||||
src={`https://www.google.com/s2/favicons?sz=128&domain=${url}`}
|
||||
alt="favicon"
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
export function MetadataBadge({
|
||||
icon,
|
||||
value,
|
||||
flexNone,
|
||||
}: {
|
||||
icon?: React.FC<{ size?: number; className?: string }>;
|
||||
value: string | JSX.Element;
|
||||
flexNone?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@ -18,9 +20,13 @@ export function MetadataBadge({
|
||||
w-fit
|
||||
my-auto
|
||||
select-none
|
||||
`}
|
||||
${flexNone ? "flex-none" : ""}`}
|
||||
>
|
||||
{icon && icon({ size: 12, className: "mr-0.5 my-auto" })}
|
||||
{icon &&
|
||||
icon({
|
||||
size: 12,
|
||||
className: flexNone ? "flex-none" : "mr-0.5 my-auto",
|
||||
})}
|
||||
<div className="my-auto flex">{value}</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { FiX } from "react-icons/fi";
|
||||
import { IconProps, XIcon } from "./icons/icons";
|
||||
import { useRef } from "react";
|
||||
import { isEventWithinRef } from "@/lib/contains";
|
||||
import ReactDOM from "react-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ModalProps {
|
||||
icon?: ({ size, className }: IconProps) => JSX.Element;
|
||||
@ -18,6 +18,8 @@ interface ModalProps {
|
||||
hideDividerForTitle?: boolean;
|
||||
hideCloseButton?: boolean;
|
||||
noPadding?: boolean;
|
||||
height?: string;
|
||||
noScroll?: boolean;
|
||||
}
|
||||
|
||||
export function Modal({
|
||||
@ -28,9 +30,11 @@ export function Modal({
|
||||
width,
|
||||
titleSize,
|
||||
hideDividerForTitle,
|
||||
height,
|
||||
noPadding,
|
||||
icon,
|
||||
hideCloseButton,
|
||||
noScroll,
|
||||
}: ModalProps) {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
@ -56,8 +60,10 @@ export function Modal({
|
||||
const modalContent = (
|
||||
<div
|
||||
onMouseDown={handleMouseDown}
|
||||
className={`fixed inset-0 bg-black bg-opacity-25 backdrop-blur-sm h-full
|
||||
flex items-center justify-center z-[9999] transition-opacity duration-300 ease-in-out`}
|
||||
className={cn(
|
||||
`fixed inset-0 bg-black bg-opacity-25 backdrop-blur-sm h-full
|
||||
flex items-center justify-center z-[9999] transition-opacity duration-300 ease-in-out`
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={modalRef}
|
||||
@ -93,8 +99,7 @@ export function Modal({
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full flex flex-col h-full justify-stretch">
|
||||
<div className="w-full overflow-y-hidden flex flex-col h-full justify-stretch">
|
||||
{title && (
|
||||
<>
|
||||
<div className="flex mb-4">
|
||||
@ -110,7 +115,14 @@ export function Modal({
|
||||
{!hideDividerForTitle && <Separator />}
|
||||
</>
|
||||
)}
|
||||
<div className="max-h-[60vh] overflow-y-scroll">{children}</div>
|
||||
<div
|
||||
className={cn(
|
||||
noScroll ? "overflow-auto" : "overflow-x-hidden",
|
||||
height || "max-h-[60vh]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
65
web/src/components/SearchResultIcon.tsx
Normal file
65
web/src/components/SearchResultIcon.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import faviconFetch from "favicon-fetch";
|
||||
import { SourceIcon } from "./SourceIcon";
|
||||
|
||||
const CACHE_DURATION = 24 * 60 * 60 * 1000;
|
||||
|
||||
export async function getFaviconUrl(url: string): Promise<string | null> {
|
||||
const getCachedFavicon = () => {
|
||||
const cachedData = localStorage.getItem(`favicon_${url}`);
|
||||
if (cachedData) {
|
||||
const { favicon, timestamp } = JSON.parse(cachedData);
|
||||
if (Date.now() - timestamp < CACHE_DURATION) {
|
||||
return favicon;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const cachedFavicon = getCachedFavicon();
|
||||
if (cachedFavicon) {
|
||||
return cachedFavicon;
|
||||
}
|
||||
|
||||
const newFaviconUrl = await faviconFetch({ uri: url });
|
||||
if (newFaviconUrl) {
|
||||
localStorage.setItem(
|
||||
`favicon_${url}`,
|
||||
JSON.stringify({ favicon: newFaviconUrl, timestamp: Date.now() })
|
||||
);
|
||||
return newFaviconUrl;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function SearchResultIcon({ url }: { url: string }) {
|
||||
const [faviconUrl, setFaviconUrl] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getFaviconUrl(url).then((favicon) => {
|
||||
if (favicon) {
|
||||
setFaviconUrl(favicon);
|
||||
}
|
||||
});
|
||||
}, [url]);
|
||||
|
||||
if (!faviconUrl) {
|
||||
return <SourceIcon sourceType="web" iconSize={18} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-full w-[18px] h-[18px] overflow-hidden bg-gray-200">
|
||||
<img
|
||||
height={18}
|
||||
width={18}
|
||||
className="rounded-full w-full h-full object-cover"
|
||||
src={faviconUrl}
|
||||
alt="favicon"
|
||||
onError={(e) => {
|
||||
e.currentTarget.onerror = null;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -9,7 +9,7 @@ import { checkUserIsNoAuthUser, logout } from "@/lib/user";
|
||||
import { Popover } from "./popover/Popover";
|
||||
import { LOGOUT_DISABLED } from "@/lib/constants";
|
||||
import { SettingsContext } from "./settings/SettingsProvider";
|
||||
import { BellIcon, LightSettingsIcon } from "./icons/icons";
|
||||
import { BellIcon, LightSettingsIcon, UserIcon } from "./icons/icons";
|
||||
import { pageType } from "@/app/chat/sessionSidebar/types";
|
||||
import { NavigationItem, Notification } from "@/app/admin/settings/interfaces";
|
||||
import DynamicFaIcon, { preloadIcons } from "./icons/DynamicFaIcon";
|
||||
@ -56,7 +56,13 @@ const DropdownOption: React.FC<DropdownOptionProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
export function UserDropdown({ page }: { page?: pageType }) {
|
||||
export function UserDropdown({
|
||||
page,
|
||||
toggleUserSettings,
|
||||
}: {
|
||||
page?: pageType;
|
||||
toggleUserSettings?: () => void;
|
||||
}) {
|
||||
const { user, isCurator } = useUser();
|
||||
const [userInfoVisible, setUserInfoVisible] = useState(false);
|
||||
const userInfoRef = useRef<HTMLDivElement>(null);
|
||||
@ -238,6 +244,13 @@ export function UserDropdown({ page }: { page?: pageType }) {
|
||||
)
|
||||
)}
|
||||
|
||||
{toggleUserSettings && (
|
||||
<DropdownOption
|
||||
onClick={toggleUserSettings}
|
||||
icon={<UserIcon className="h-5 w-5 my-auto mr-2" />}
|
||||
label="User Settings"
|
||||
/>
|
||||
)}
|
||||
<DropdownOption
|
||||
onClick={() => {
|
||||
setUserInfoVisible(true);
|
||||
|
16
web/src/components/WebResultIcon.tsx
Normal file
16
web/src/components/WebResultIcon.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { SourceIcon } from "./SourceIcon";
|
||||
|
||||
export function WebResultIcon({ url }: { url: string }) {
|
||||
const hostname = new URL(url).hostname;
|
||||
return hostname == "https://docs.danswer.dev" ? (
|
||||
<img
|
||||
className="my-0 py-0"
|
||||
src={`https://www.google.com/s2/favicons?domain=${hostname}`}
|
||||
alt="favicon"
|
||||
height={18}
|
||||
width={18}
|
||||
/>
|
||||
) : (
|
||||
<SourceIcon sourceType="web" iconSize={18} />
|
||||
);
|
||||
}
|
@ -40,14 +40,7 @@ export function AdminSidebar({ collections }: { collections: Collection[] }) {
|
||||
<nav className="space-y-2">
|
||||
<div className="w-full justify-center mb-4 flex">
|
||||
<div className="w-52">
|
||||
<Link
|
||||
className="flex flex-col"
|
||||
href={
|
||||
settings && settings.default_page === "chat"
|
||||
? "/chat"
|
||||
: "/search"
|
||||
}
|
||||
>
|
||||
<Link className="flex flex-col" href="/chat">
|
||||
<div className="max-w-[200px] w-full flex gap-x-1 my-auto">
|
||||
<div className="flex-none mb-auto">
|
||||
<Logo />
|
||||
@ -73,7 +66,7 @@ export function AdminSidebar({ collections }: { collections: Collection[] }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-center">
|
||||
<Link href={settings.default_page == "chat" ? "/chat" : "/search"}>
|
||||
<Link href="/chat">
|
||||
<button className="text-sm flex items-center block w-52 py-2.5 flex px-2 text-left text-text-back-button bg-background-back-button hover:bg-opacity-80 cursor-pointer rounded">
|
||||
<BackIcon className="my-auto" size={18} />
|
||||
<p className="ml-1 break-words line-clamp-2 ellipsis leading-none">
|
||||
|
@ -13,7 +13,9 @@ export default function AssistantBanner({
|
||||
liveAssistant,
|
||||
allAssistants,
|
||||
onAssistantChange,
|
||||
mobile = false,
|
||||
}: {
|
||||
mobile?: boolean;
|
||||
recentAssistants: Persona[];
|
||||
liveAssistant: Persona | undefined;
|
||||
allAssistants: Persona[];
|
||||
@ -35,13 +37,15 @@ export default function AssistantBanner({
|
||||
)
|
||||
)
|
||||
// Take first 4
|
||||
.slice(0, 4)
|
||||
.slice(0, mobile ? 2 : 4)
|
||||
.map((assistant) => (
|
||||
<TooltipProvider key={assistant.id}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className="flex w-36 mx-3 py-1.5 scale-[1.] rounded-full border border-background-150 justify-center items-center gap-x-2 py-1 px-3 hover:bg-background-125 transition-colors cursor-pointer"
|
||||
className={`${
|
||||
mobile ? "w-full" : "w-36 mx-3"
|
||||
} flex py-1.5 scale-[1.] rounded-full border border-background-150 justify-center items-center gap-x-2 py-1 px-3 hover:bg-background-125 transition-colors cursor-pointer`}
|
||||
onClick={() => onAssistantChange(assistant)}
|
||||
>
|
||||
<AssistantIcon
|
||||
|
@ -1,77 +1,85 @@
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { AssistantTools } from "@/app/assistants/ToolsDisplay";
|
||||
import { Bubble } from "@/components/Bubble";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { getDisplayNameForModel } from "@/lib/hooks";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import React, { useState } from "react";
|
||||
import { FiBookmark } from "react-icons/fi";
|
||||
import { FiBookmark, FiImage, FiSearch } from "react-icons/fi";
|
||||
import { MdDragIndicator } from "react-icons/md";
|
||||
|
||||
import { Badge } from "../ui/badge";
|
||||
|
||||
export const AssistantCard = ({
|
||||
assistant,
|
||||
isSelected,
|
||||
onSelect,
|
||||
llmName,
|
||||
}: {
|
||||
assistant: Persona;
|
||||
isSelected: boolean;
|
||||
onSelect: (assistant: Persona) => void;
|
||||
llmName: string;
|
||||
}) => {
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const renderBadgeContent = (tool: { name: string }) => {
|
||||
switch (tool.name) {
|
||||
case "SearchTool":
|
||||
return (
|
||||
<>
|
||||
<FiSearch className="h-3 w-3 my-auto" />
|
||||
<span>Search</span>
|
||||
</>
|
||||
);
|
||||
case "ImageGenerationTool":
|
||||
return (
|
||||
<>
|
||||
<FiImage className="h-3 w-3 my-auto" />
|
||||
<span>Image Gen</span>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return tool.name;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => onSelect(assistant)}
|
||||
className={`
|
||||
p-4
|
||||
flex flex-col overflow-hidden w-full rounded-xl px-3 py-4
|
||||
cursor-pointer
|
||||
border
|
||||
${isSelected ? "bg-hover" : "hover:bg-hover-light"}
|
||||
shadow-md
|
||||
rounded-lg
|
||||
border-border
|
||||
grow
|
||||
flex items-center
|
||||
overflow-hidden
|
||||
${isSelected ? "bg-background-125" : "hover:bg-background-100"}
|
||||
`}
|
||||
onMouseEnter={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
>
|
||||
<div className="w-full">
|
||||
<div className="flex items-center mb-2">
|
||||
<AssistantIcon assistant={assistant} />
|
||||
<div className="ml-2 ellipsis truncate font-bold text-sm text-emphasis">
|
||||
{assistant.name}
|
||||
<div className="flex items-center gap-4">
|
||||
<AssistantIcon size="xs" assistant={assistant} />
|
||||
<div className="overflow-hidden text-ellipsis break-words flex-grow">
|
||||
<div className="flex items-start justify-start gap-2">
|
||||
<span className="line-clamp-1 text-sm text-black font-semibold leading-tight">
|
||||
{assistant.name}
|
||||
</span>
|
||||
{assistant.tools.map((tool, index) => (
|
||||
<Badge key={index} size="xs" variant="secondary" className="ml-1">
|
||||
<div className="flex items-center gap-1">
|
||||
{renderBadgeContent(tool)}
|
||||
</div>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-wrap text-subtle mb-2 mt-2 line-clamp-3 py-1">
|
||||
{assistant.description}
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col gap-y-1">
|
||||
{assistant.document_sets.length > 0 && (
|
||||
<div className="text-xs text-subtle flex flex-wrap gap-2">
|
||||
<p className="my-auto font-medium">Document Sets:</p>
|
||||
{assistant.document_sets.map((set) => (
|
||||
<Bubble key={set.id} isSelected={false}>
|
||||
<div className="flex flex-row gap-1">
|
||||
<FiBookmark className="mr-1 my-auto" />
|
||||
{set.name}
|
||||
</div>
|
||||
</Bubble>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-subtle">
|
||||
<span className="font-semibold">Default model:</span>{" "}
|
||||
{getDisplayNameForModel(
|
||||
assistant.llm_model_version_override || llmName
|
||||
)}
|
||||
</div>
|
||||
<AssistantTools hovered={hovering} assistant={assistant} />
|
||||
<span className="line-clamp-2 text-xs text-text-700">
|
||||
{assistant.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{assistant.document_sets.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{assistant.document_sets.map((set) => (
|
||||
<Bubble key={set.id} isSelected={false}>
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<FiBookmark className="text-text-500" />
|
||||
{set.name}
|
||||
</div>
|
||||
</Bubble>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
344
web/src/components/chat_search/AssistantSelector.tsx
Normal file
344
web/src/components/chat_search/AssistantSelector.tsx
Normal file
@ -0,0 +1,344 @@
|
||||
import React, { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useChatContext } from "@/components/context/ChatContext";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { FiChevronDown } from "react-icons/fi";
|
||||
import { destructureValue, getFinalLLM } from "@/lib/llm/utils";
|
||||
import { updateModelOverrideForChatSession } from "@/app/chat/lib";
|
||||
import { debounce } from "lodash";
|
||||
import { LlmList } from "@/components/llm/LLMList";
|
||||
import { checkPersonaRequiresImageGeneration } from "@/app/admin/assistants/lib";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { DraggableAssistantCard } from "@/components/assistants/AssistantCards";
|
||||
import { updateUserAssistantList } from "@/lib/assistants/updateAssistantPreferences";
|
||||
|
||||
import Text from "@/components/ui/text";
|
||||
import { LlmOverrideManager } from "@/lib/hooks";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { AssistantIcon } from "../assistants/AssistantIcon";
|
||||
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
||||
import { restrictToParentElement } from "@dnd-kit/modifiers";
|
||||
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "../ui/drawer";
|
||||
|
||||
const AssistantSelector = ({
|
||||
liveAssistant,
|
||||
onAssistantChange,
|
||||
chatSessionId,
|
||||
llmOverrideManager,
|
||||
isMobile,
|
||||
}: {
|
||||
liveAssistant: Persona;
|
||||
onAssistantChange: (assistant: Persona) => void;
|
||||
chatSessionId?: string;
|
||||
llmOverrideManager?: LlmOverrideManager;
|
||||
isMobile: boolean;
|
||||
}) => {
|
||||
const { finalAssistants } = useAssistants();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const { llmProviders } = useChatContext();
|
||||
const { user } = useUser();
|
||||
const [assistants, setAssistants] = useState<Persona[]>(finalAssistants);
|
||||
const [isTemperatureExpanded, setIsTemperatureExpanded] = useState(false);
|
||||
const [localTemperature, setLocalTemperature] = useState<number>(
|
||||
llmOverrideManager?.temperature || 0
|
||||
);
|
||||
|
||||
// Initialize selectedTab from localStorage
|
||||
const [selectedTab, setSelectedTab] = useState<number>(() => {
|
||||
const storedTab = localStorage.getItem("assistantSelectorSelectedTab");
|
||||
return storedTab !== null ? Number(storedTab) : 0;
|
||||
});
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
const handleDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = assistants.findIndex(
|
||||
(item) => item.id.toString() === active.id
|
||||
);
|
||||
const newIndex = assistants.findIndex(
|
||||
(item) => item.id.toString() === over.id
|
||||
);
|
||||
const updatedAssistants = arrayMove(assistants, oldIndex, newIndex);
|
||||
setAssistants(updatedAssistants);
|
||||
await updateUserAssistantList(updatedAssistants.map((a) => a.id));
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedSetTemperature = useCallback(
|
||||
(value: number) => {
|
||||
const debouncedFunction = debounce((value: number) => {
|
||||
llmOverrideManager?.setTemperature(value);
|
||||
}, 300);
|
||||
return debouncedFunction(value);
|
||||
},
|
||||
[llmOverrideManager]
|
||||
);
|
||||
|
||||
const handleTemperatureChange = (value: number) => {
|
||||
setLocalTemperature(value);
|
||||
debouncedSetTemperature(value);
|
||||
};
|
||||
|
||||
// Handle tab change and update localStorage
|
||||
const handleTabChange = (index: number) => {
|
||||
setSelectedTab(index);
|
||||
localStorage.setItem("assistantSelectorSelectedTab", index.toString());
|
||||
};
|
||||
|
||||
// Get the user's default model
|
||||
const userDefaultModel = user?.preferences.default_model;
|
||||
|
||||
const [_, currentLlm] = getFinalLLM(
|
||||
llmProviders,
|
||||
liveAssistant,
|
||||
llmOverrideManager?.llmOverride ?? null
|
||||
);
|
||||
|
||||
const requiresImageGeneration =
|
||||
checkPersonaRequiresImageGeneration(liveAssistant);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<Tab.Group selectedIndex={selectedTab} onChange={handleTabChange}>
|
||||
<Tab.List className="flex p-1 space-x-1 bg-gray-100 rounded-t-md">
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-full py-2.5 text-sm leading-5 font-medium rounded-md
|
||||
${
|
||||
selected
|
||||
? "bg-white text-gray-700 shadow"
|
||||
: "text-gray-500 hover:bg-white/[0.12] hover:text-gray-700"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Assistant
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-full py-2.5 text-sm leading-5 font-medium rounded-md
|
||||
${
|
||||
selected
|
||||
? "bg-white text-gray-700 shadow"
|
||||
: "text-gray-500 hover:bg-white/[0.12] hover:text-gray-700"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Model
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel className="p-3">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-center text-lg font-semibold text-gray-800">
|
||||
Choose an Assistant
|
||||
</h3>
|
||||
</div>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
modifiers={[restrictToVerticalAxis, restrictToParentElement]}
|
||||
>
|
||||
<SortableContext
|
||||
items={assistants.map((a) => a.id.toString())}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{assistants.map((assistant) => (
|
||||
<DraggableAssistantCard
|
||||
key={assistant.id.toString()}
|
||||
assistant={assistant}
|
||||
isSelected={liveAssistant.id === assistant.id}
|
||||
onSelect={(assistant) => {
|
||||
onAssistantChange(assistant);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
llmName={
|
||||
assistant.llm_model_version_override ??
|
||||
userDefaultModel ??
|
||||
currentLlm
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel className="p-3">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-center text-lg font-semibold text-gray-800 ">
|
||||
Choose a Model
|
||||
</h3>
|
||||
</div>
|
||||
<LlmList
|
||||
currentAssistant={liveAssistant}
|
||||
requiresImageGeneration={requiresImageGeneration}
|
||||
llmProviders={llmProviders}
|
||||
currentLlm={currentLlm}
|
||||
userDefault={userDefaultModel}
|
||||
includeUserDefault={true}
|
||||
onSelect={(value: string | null) => {
|
||||
if (value == null) return;
|
||||
const { modelName, name, provider } = destructureValue(value);
|
||||
llmOverrideManager?.setLlmOverride({
|
||||
name,
|
||||
provider,
|
||||
modelName,
|
||||
});
|
||||
if (chatSessionId) {
|
||||
updateModelOverrideForChatSession(chatSessionId, value);
|
||||
}
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<button
|
||||
className="flex items-center text-sm font-medium transition-colors duration-200"
|
||||
onClick={() => setIsTemperatureExpanded(!isTemperatureExpanded)}
|
||||
>
|
||||
<span className="mr-2 text-xs text-primary">
|
||||
{isTemperatureExpanded ? "▼" : "►"}
|
||||
</span>
|
||||
<span>Temperature</span>
|
||||
</button>
|
||||
|
||||
{isTemperatureExpanded && (
|
||||
<>
|
||||
<Text className="mt-2 mb-8">
|
||||
Adjust the temperature of the LLM. Higher temperatures will
|
||||
make the LLM generate more creative and diverse responses,
|
||||
while lower temperature will make the LLM generate more
|
||||
conservative and focused responses.
|
||||
</Text>
|
||||
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
type="range"
|
||||
onChange={(e) =>
|
||||
handleTemperatureChange(parseFloat(e.target.value))
|
||||
}
|
||||
className="w-full p-2 border border-border rounded-md"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.01"
|
||||
value={localTemperature}
|
||||
/>
|
||||
<div
|
||||
className="absolute text-sm"
|
||||
style={{
|
||||
left: `${(localTemperature || 0) * 50}%`,
|
||||
transform: `translateX(-${Math.min(
|
||||
Math.max((localTemperature || 0) * 50, 10),
|
||||
90
|
||||
)}%)`,
|
||||
top: "-1.5rem",
|
||||
}}
|
||||
>
|
||||
{localTemperature}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMobile) {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}
|
||||
}, [isMobile]);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto relative" ref={dropdownRef}>
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
// Get selectedTab from localStorage when opening
|
||||
const storedTab = localStorage.getItem(
|
||||
"assistantSelectorSelectedTab"
|
||||
);
|
||||
setSelectedTab(storedTab !== null ? Number(storedTab) : 0);
|
||||
}}
|
||||
className="flex items-center gap-x-2 justify-between px-6 py-3 text-sm font-medium text-white bg-black rounded-full shadow-lg hover:shadow-xl transition-all duration-300 cursor-pointer"
|
||||
>
|
||||
<div className="flex gap-x-2 items-center">
|
||||
<AssistantIcon assistant={liveAssistant} size="xs" />
|
||||
<span className="font-bold">{liveAssistant.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 text-xs">{currentLlm}</span>
|
||||
<FiChevronDown
|
||||
className={`w-5 h-5 text-white transition-transform duration-300 transform ${
|
||||
isOpen ? "rotate-180" : ""
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isMobile ? (
|
||||
<Drawer open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Assistant Selector</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
{content}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
) : (
|
||||
isOpen && (
|
||||
<div className="absolute z-10 w-96 mt-2 origin-top-center left-1/2 transform -translate-x-1/2 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssistantSelector;
|
@ -11,7 +11,8 @@ import { pageType } from "@/app/chat/sessionSidebar/types";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ChatBanner } from "@/app/chat/ChatBanner";
|
||||
import LogoType from "../header/LogoType";
|
||||
import { useUser } from "../user/UserProvider";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { LlmOverrideManager } from "@/lib/hooks";
|
||||
|
||||
export default function FunctionalHeader({
|
||||
page,
|
||||
@ -20,13 +21,23 @@ export default function FunctionalHeader({
|
||||
toggleSidebar = () => null,
|
||||
reset = () => null,
|
||||
sidebarToggled,
|
||||
liveAssistant,
|
||||
onAssistantChange,
|
||||
llmOverrideManager,
|
||||
documentSidebarToggled,
|
||||
toggleUserSettings,
|
||||
}: {
|
||||
reset?: () => void;
|
||||
page: pageType;
|
||||
sidebarToggled?: boolean;
|
||||
documentSidebarToggled?: boolean;
|
||||
currentChatSession?: ChatSession | null | undefined;
|
||||
setSharingModalVisible?: (value: SetStateAction<boolean>) => void;
|
||||
toggleSidebar?: () => void;
|
||||
liveAssistant?: Persona;
|
||||
onAssistantChange?: (assistant: Persona) => void;
|
||||
llmOverrideManager?: LlmOverrideManager;
|
||||
toggleUserSettings?: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
@ -63,14 +74,15 @@ export default function FunctionalHeader({
|
||||
router.push(newChatUrl);
|
||||
};
|
||||
return (
|
||||
<div className="left-0 bg-transparent sticky top-0 z-20 w-full relative flex">
|
||||
<div className="mt-2 mx-2.5 cursor-pointer text-text-700 relative flex w-full">
|
||||
<div className="left-0 sticky top-0 z-20 w-full relative flex">
|
||||
<div className="mt-2 cursor-pointer text-text-700 relative flex w-full">
|
||||
<LogoType
|
||||
assistantId={currentChatSession?.persona_id}
|
||||
page={page}
|
||||
toggleSidebar={toggleSidebar}
|
||||
handleNewChat={handleNewChat}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{ transition: "width 0.30s ease-out" }}
|
||||
className={`
|
||||
@ -85,6 +97,7 @@ export default function FunctionalHeader({
|
||||
${sidebarToggled ? "w-[250px]" : "w-[0px]"}
|
||||
`}
|
||||
/>
|
||||
|
||||
<div className="w-full mobile:-mx-20 desktop:px-4">
|
||||
<ChatBanner />
|
||||
</div>
|
||||
@ -108,7 +121,7 @@ export default function FunctionalHeader({
|
||||
)}
|
||||
|
||||
<div className="mobile:hidden flex my-auto">
|
||||
<UserDropdown />
|
||||
<UserDropdown page={page} toggleUserSettings={toggleUserSettings} />
|
||||
</div>
|
||||
<Link
|
||||
className="desktop:hidden my-auto"
|
||||
@ -124,12 +137,37 @@ export default function FunctionalHeader({
|
||||
<NewChatIcon size={20} />
|
||||
</div>
|
||||
</Link>
|
||||
<div
|
||||
style={{ transition: "width 0.30s ease-out" }}
|
||||
className={`
|
||||
mobile:hidden
|
||||
flex-none
|
||||
mx-auto
|
||||
overflow-y-hidden
|
||||
transition-all
|
||||
duration-300
|
||||
ease-in-out
|
||||
h-full
|
||||
${documentSidebarToggled ? "w-[400px]" : "w-[0px]"}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{page != "assistants" && (
|
||||
<div className="h-20 left-0 absolute top-0 z-10 w-full bg-gradient-to-b via-50% z-[-1] from-background via-background to-background/10 flex" />
|
||||
)}
|
||||
{page != "assistants" && (
|
||||
<div
|
||||
className={`
|
||||
h-20 absolute top-0 z-10 w-full sm:w-[90%] lg:w-[70%]
|
||||
bg-gradient-to-b via-50% z-[-1] from-background via-background to-background/10 flex
|
||||
transition-all duration-300 ease-in-out
|
||||
${
|
||||
documentSidebarToggled
|
||||
? "left-[200px] transform -translate-x-[calc(50%+100px)]"
|
||||
: "left-1/2 transform -translate-x-1/2"
|
||||
}
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
78
web/src/components/chat_search/sources/SourceCard.tsx
Normal file
78
web/src/components/chat_search/sources/SourceCard.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { WebResultIcon } from "@/components/WebResultIcon";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import { DanswerDocument } from "@/lib/search/interfaces";
|
||||
|
||||
export default function SourceCard({ doc }: { doc: DanswerDocument }) {
|
||||
return (
|
||||
<a
|
||||
key={doc.document_id}
|
||||
href={doc.link || undefined}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="flex flex-col gap-0.5 rounded-sm px-3 py-2.5 hover:bg-background-125 bg-background-100 w-[200px]"
|
||||
>
|
||||
<div className="line-clamp-1 font-semibold text-ellipsis text-text-900 flex h-6 items-center gap-2 text-sm">
|
||||
{doc.is_internet || doc.source_type === "web" ? (
|
||||
<WebResultIcon url={doc.link} />
|
||||
) : (
|
||||
<SourceIcon sourceType={doc.source_type} iconSize={18} />
|
||||
)}
|
||||
<p>
|
||||
{(doc.semantic_identifier || doc.document_id).slice(0, 12).trim()}
|
||||
{(doc.semantic_identifier || doc.document_id).length > 12 && (
|
||||
<span className="text-text-500">...</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="line-clamp-2 text-sm font-semibold"></div>
|
||||
<div className="line-clamp-2 text-sm font-normal leading-snug text-text-700">
|
||||
{doc.blurb}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
interface SeeMoreBlockProps {
|
||||
documentSelectionToggled: boolean;
|
||||
toggleDocumentSelection?: () => void;
|
||||
uniqueSources: DanswerDocument["source_type"][];
|
||||
}
|
||||
|
||||
export function SeeMoreBlock({
|
||||
documentSelectionToggled,
|
||||
toggleDocumentSelection,
|
||||
uniqueSources,
|
||||
}: SeeMoreBlockProps) {
|
||||
return (
|
||||
<div
|
||||
onClick={toggleDocumentSelection}
|
||||
className={`
|
||||
${documentSelectionToggled ? "border-border-100 border" : ""}
|
||||
cursor-pointer w-[150px] rounded-sm flex-none transition-all duration-500 hover:bg-background-125 bg-text-100 px-3 py-2.5
|
||||
`}
|
||||
>
|
||||
<div className="line-clamp-1 font-semibold text-ellipsis text-text-900 flex h-6 items-center justify-between text-sm">
|
||||
<p className="mr-0 flex-shrink-0">
|
||||
{documentSelectionToggled ? "Hide sources" : "See context"}
|
||||
</p>
|
||||
<div className="flex -space-x-3 flex-shrink-0 overflow-hidden">
|
||||
{uniqueSources.map((sourceType, ind) => (
|
||||
<div
|
||||
key={ind}
|
||||
className="inline-block bg-background-100 rounded-full p-0.5"
|
||||
style={{ zIndex: uniqueSources.length - ind }}
|
||||
>
|
||||
<div className="bg-background-100 rounded-full">
|
||||
<SourceIcon sourceType={sourceType} iconSize={20} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="line-clamp-2 text-sm font-semibold"></div>
|
||||
<div className="line-clamp-2 text-sm font-normal leading-snug text-text-700">
|
||||
See more
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,7 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
import { DocumentSet, Tag, User, ValidSources } from "@/lib/types";
|
||||
import {
|
||||
CCPairBasicInfo,
|
||||
DocumentSet,
|
||||
Tag,
|
||||
User,
|
||||
ValidSources,
|
||||
} from "@/lib/types";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
@ -12,6 +18,9 @@ import { personaComparator } from "@/app/admin/assistants/lib";
|
||||
interface ChatContextProps {
|
||||
chatSessions: ChatSession[];
|
||||
availableSources: ValidSources[];
|
||||
ccPairs: CCPairBasicInfo[];
|
||||
tags: Tag[];
|
||||
documentSets: DocumentSet[];
|
||||
availableDocumentSets: DocumentSet[];
|
||||
availableTags: Tag[];
|
||||
llmProviders: LLMProviderDescriptor[];
|
||||
|
@ -8,9 +8,9 @@ import { ChatSession } from "@/app/chat/interfaces";
|
||||
interface SearchContextProps {
|
||||
querySessions: ChatSession[];
|
||||
ccPairs: CCPairBasicInfo[];
|
||||
tags: Tag[];
|
||||
documentSets: DocumentSet[];
|
||||
assistants: Persona[];
|
||||
tags: Tag[];
|
||||
agenticSearchEnabled: boolean;
|
||||
disabledAgentic: boolean;
|
||||
initiallyToggled: boolean;
|
||||
|
@ -81,7 +81,9 @@ import cohereIcon from "../../../public/Cohere.svg";
|
||||
import voyageIcon from "../../../public/Voyage.png";
|
||||
import googleIcon from "../../../public/Google.webp";
|
||||
import xenforoIcon from "../../../public/Xenforo.svg";
|
||||
import { FaRobot } from "react-icons/fa";
|
||||
import { FaGithub, FaRobot } from "react-icons/fa";
|
||||
import { isConstructSignatureDeclaration } from "typescript";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface IconProps {
|
||||
size?: number;
|
||||
@ -474,13 +476,6 @@ export const XSquareIcon = ({
|
||||
return <XSquare size={size} className={className} />;
|
||||
};
|
||||
|
||||
export const GlobeIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSSBlue,
|
||||
}: IconProps) => {
|
||||
return <FiGlobe size={size} className={className} />;
|
||||
};
|
||||
|
||||
export const FileIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSSBlue,
|
||||
@ -1034,9 +1029,16 @@ export const GithubIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => (
|
||||
<LogoIcon size={size} className={className} src="/Github.png" />
|
||||
<FaGithub size={size} className={cn(className, "text-black")} />
|
||||
);
|
||||
|
||||
export const GlobeIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSSBlue,
|
||||
}: IconProps) => {
|
||||
return <FiGlobe size={size} className={className} />;
|
||||
};
|
||||
|
||||
export const GmailIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
@ -2698,3 +2700,28 @@ export const DownloadCSVIcon = ({
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
style={{ width: `${size}px`, height: `${size}px` }}
|
||||
className={`w-[${size}px] h-[${size}px] ` + className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="200"
|
||||
height="200"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M19.618 21.25c0-3.602-4.016-6.53-7.618-6.53c-3.602 0-7.618 2.928-7.618 6.53M12 11.456a4.353 4.353 0 1 0 0-8.706a4.353 4.353 0 0 0 0 8.706"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
@ -1,10 +1,15 @@
|
||||
import React from "react";
|
||||
import { getDisplayNameForModel } from "@/lib/hooks";
|
||||
import { checkLLMSupportsImageInput, structureValue } from "@/lib/llm/utils";
|
||||
import {
|
||||
checkLLMSupportsImageInput,
|
||||
destructureValue,
|
||||
structureValue,
|
||||
} from "@/lib/llm/utils";
|
||||
import {
|
||||
getProviderIcon,
|
||||
LLMProviderDescriptor,
|
||||
} from "@/app/admin/configuration/llm/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
|
||||
interface LlmListProps {
|
||||
llmProviders: LLMProviderDescriptor[];
|
||||
@ -14,15 +19,19 @@ interface LlmListProps {
|
||||
scrollable?: boolean;
|
||||
hideProviderIcon?: boolean;
|
||||
requiresImageGeneration?: boolean;
|
||||
includeUserDefault?: boolean;
|
||||
currentAssistant?: Persona;
|
||||
}
|
||||
|
||||
export const LlmList: React.FC<LlmListProps> = ({
|
||||
currentAssistant,
|
||||
llmProviders,
|
||||
currentLlm,
|
||||
onSelect,
|
||||
userDefault,
|
||||
scrollable,
|
||||
requiresImageGeneration,
|
||||
includeUserDefault = false,
|
||||
}) => {
|
||||
const llmOptionsByProvider: {
|
||||
[provider: string]: {
|
||||
@ -68,21 +77,6 @@ export const LlmList: React.FC<LlmListProps> = ({
|
||||
: "max-h-[300px]"
|
||||
} bg-background-175 flex flex-col gap-y-1 overflow-y-scroll`}
|
||||
>
|
||||
{userDefault && (
|
||||
<button
|
||||
type="button"
|
||||
key={-1}
|
||||
className={`w-full py-1.5 px-2 text-sm ${
|
||||
currentLlm == null
|
||||
? "bg-background-200"
|
||||
: "bg-background hover:bg-background-100"
|
||||
} text-left rounded`}
|
||||
onClick={() => onSelect(null)}
|
||||
>
|
||||
User Default (currently {getDisplayNameForModel(userDefault)})
|
||||
</button>
|
||||
)}
|
||||
|
||||
{llmOptions.map(({ name, icon, value }, index) => {
|
||||
if (!requiresImageGeneration || checkLLMSupportsImageInput(name)) {
|
||||
return (
|
||||
@ -98,6 +92,25 @@ export const LlmList: React.FC<LlmListProps> = ({
|
||||
>
|
||||
{icon({ size: 16 })}
|
||||
{getDisplayNameForModel(name)}
|
||||
{(() => {
|
||||
if (
|
||||
currentAssistant?.llm_model_version_override === name &&
|
||||
userDefault &&
|
||||
name === destructureValue(userDefault).modelName
|
||||
) {
|
||||
return " (assistant + user default)";
|
||||
} else if (
|
||||
currentAssistant?.llm_model_version_override === name
|
||||
) {
|
||||
return " (assistant)";
|
||||
} else if (
|
||||
userDefault &&
|
||||
name === destructureValue(userDefault).modelName
|
||||
) {
|
||||
return " (user default)";
|
||||
}
|
||||
return "";
|
||||
})()}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
DanswerDocument,
|
||||
DocumentRelevance,
|
||||
LoadedDanswerDocument,
|
||||
SearchDanswerDocument,
|
||||
} from "@/lib/search/interfaces";
|
||||
import { DocumentFeedbackBlock } from "./DocumentFeedbackBlock";
|
||||
@ -11,19 +12,21 @@ import { PopupSpec } from "../admin/connectors/Popup";
|
||||
import { DocumentUpdatedAtBadge } from "./DocumentUpdatedAtBadge";
|
||||
import { SourceIcon } from "../SourceIcon";
|
||||
import { MetadataBadge } from "../MetadataBadge";
|
||||
import { BookIcon, LightBulbIcon } from "../icons/icons";
|
||||
import { BookIcon, GlobeIcon, LightBulbIcon, SearchIcon } from "../icons/icons";
|
||||
|
||||
import { FaStar } from "react-icons/fa";
|
||||
import { FiTag } from "react-icons/fi";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
import { CustomTooltip, TooltipGroup } from "../tooltip/CustomTooltip";
|
||||
import { WarningCircle } from "@phosphor-icons/react";
|
||||
import { SearchResultIcon } from "../SearchResultIcon";
|
||||
|
||||
export const buildDocumentSummaryDisplay = (
|
||||
matchHighlights: string[],
|
||||
blurb: string
|
||||
) => {
|
||||
if (matchHighlights.length === 0) {
|
||||
if (!matchHighlights || matchHighlights.length === 0) {
|
||||
// console.log("no match highlights", matchHighlights);
|
||||
return blurb;
|
||||
}
|
||||
|
||||
@ -251,7 +254,11 @@ export const DocumentDisplay = ({
|
||||
>
|
||||
<CustomTooltip showTick line content="Toggle content">
|
||||
<LightBulbIcon
|
||||
className={`${settings?.isMobile && alternativeToggled ? "text-green-600" : "text-blue-600"} my-auto ml-2 h-4 w-4 cursor-pointer`}
|
||||
className={`${
|
||||
settings?.isMobile && alternativeToggled
|
||||
? "text-green-600"
|
||||
: "text-blue-600"
|
||||
} my-auto ml-2 h-4 w-4 cursor-pointer`}
|
||||
/>
|
||||
</CustomTooltip>
|
||||
</button>
|
||||
@ -308,7 +315,9 @@ export const AgenticDocumentDisplay = ({
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`collapsible ${!hide && "collapsible-closed overflow-y-auto border-transparent"}`}
|
||||
className={`collapsible ${
|
||||
!hide && "collapsible-closed overflow-y-auto border-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="flex relative">
|
||||
<a
|
||||
@ -380,3 +389,38 @@ export const AgenticDocumentDisplay = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function CompactDocumentCard({
|
||||
document,
|
||||
icon,
|
||||
url,
|
||||
}: {
|
||||
document: LoadedDanswerDocument;
|
||||
icon?: React.ReactNode;
|
||||
url?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="max-w-[250px] pb-0 pt-0 mt-0 flex gap-y-0 flex-col content-start items-start gap-0 ">
|
||||
<h3 className="text-sm font-semibold flex items-center gap-x-1 text-text-900 pt-0 mt-0 truncate w-full">
|
||||
{icon}
|
||||
{(document.semantic_identifier || document.document_id).slice(0, 40)}
|
||||
{(document.semantic_identifier || document.document_id).length > 40 &&
|
||||
"..."}
|
||||
</h3>
|
||||
{document.blurb && (
|
||||
<p className="text-xs mb-0 text-gray-600 line-clamp-2">
|
||||
{document.blurb}
|
||||
</p>
|
||||
)}
|
||||
{document.updated_at && (
|
||||
<div className=" flex mt-0 pt-0 items-center justify-between w-full ">
|
||||
{!isNaN(new Date(document.updated_at).getTime()) && (
|
||||
<span className="text-xs text-gray-500">
|
||||
Updated {new Date(document.updated_at).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,17 @@
|
||||
import { timeAgo } from "@/lib/time";
|
||||
import { MetadataBadge } from "../MetadataBadge";
|
||||
|
||||
export function DocumentUpdatedAtBadge({ updatedAt }: { updatedAt: string }) {
|
||||
return <MetadataBadge value={"Updated " + timeAgo(updatedAt)} />;
|
||||
export function DocumentUpdatedAtBadge({
|
||||
updatedAt,
|
||||
modal,
|
||||
}: {
|
||||
updatedAt: string;
|
||||
modal?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MetadataBadge
|
||||
flexNone={modal}
|
||||
value={(modal ? "" : "Updated ") + timeAgo(updatedAt)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,169 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { HoverableIcon } from "../Hoverable";
|
||||
import {
|
||||
DislikeFeedbackIcon,
|
||||
LikeFeedbackIcon,
|
||||
ToggleDown,
|
||||
} from "../icons/icons";
|
||||
import { FeedbackType } from "@/app/chat/types";
|
||||
import { searchState } from "./SearchSection";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
import { AnswerSection } from "./results/AnswerSection";
|
||||
import { Quote, SearchResponse } from "@/lib/search/interfaces";
|
||||
import { QuotesSection } from "./results/QuotesSection";
|
||||
|
||||
export default function SearchAnswer({
|
||||
searchAnswerExpanded,
|
||||
setSearchAnswerExpanded,
|
||||
isFetching,
|
||||
dedupedQuotes,
|
||||
searchResponse,
|
||||
setCurrentFeedback,
|
||||
searchState,
|
||||
}: {
|
||||
searchAnswerExpanded: boolean;
|
||||
setSearchAnswerExpanded: Dispatch<SetStateAction<boolean>>;
|
||||
isFetching: boolean;
|
||||
dedupedQuotes: Quote[];
|
||||
searchResponse: SearchResponse;
|
||||
searchState: searchState;
|
||||
setCurrentFeedback: Dispatch<SetStateAction<[FeedbackType, number] | null>>;
|
||||
}) {
|
||||
const [searchAnswerOverflowing, setSearchAnswerOverflowing] = useState(false);
|
||||
|
||||
const { quotes, answer, error } = searchResponse;
|
||||
const answerContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleFeedback = (feedbackType: FeedbackType, messageId: number) => {
|
||||
setCurrentFeedback([feedbackType, messageId]);
|
||||
};
|
||||
|
||||
const settings = useContext(SettingsContext);
|
||||
|
||||
useEffect(() => {
|
||||
const checkOverflow = () => {
|
||||
if (answerContainerRef.current) {
|
||||
const isOverflowing =
|
||||
answerContainerRef.current.scrollHeight >
|
||||
answerContainerRef.current.clientHeight;
|
||||
setSearchAnswerOverflowing(isOverflowing);
|
||||
}
|
||||
};
|
||||
|
||||
checkOverflow();
|
||||
window.addEventListener("resize", checkOverflow);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", checkOverflow);
|
||||
};
|
||||
}, [answer, quotes]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={answerContainerRef}
|
||||
className={`my-4 ${
|
||||
searchAnswerExpanded ? "min-h-[16rem]" : "h-[16rem]"
|
||||
} ${
|
||||
!searchAnswerExpanded && searchAnswerOverflowing && "overflow-y-hidden"
|
||||
} p-4 border-2 border-search-answer-border rounded-lg relative`}
|
||||
>
|
||||
<div>
|
||||
<div className="flex gap-x-2">
|
||||
<h2 className="text-emphasis font-bold my-auto mb-1">AI Answer</h2>
|
||||
|
||||
{searchState == "generating" && (
|
||||
<div key={"generating"} className="relative inline-block">
|
||||
<span className="loading-text">Generating Response...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchState == "citing" && (
|
||||
<div key={"citing"} className="relative inline-block">
|
||||
<span className="loading-text">Extracting Quotes...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchState == "searching" && (
|
||||
<div key={"Reading"} className="relative inline-block">
|
||||
<span className="loading-text">Searching...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchState == "reading" && (
|
||||
<div key={"Reading"} className="relative inline-block">
|
||||
<span className="loading-text">
|
||||
Reading{settings?.isMobile ? "" : " Documents"}
|
||||
...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchState == "analyzing" && (
|
||||
<div key={"Generating"} className="relative inline-block">
|
||||
<span className="loading-text">
|
||||
Running
|
||||
{settings?.isMobile ? "" : " Analysis"}...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`pt-1 h-auto border-t border-border w-full`}>
|
||||
<AnswerSection
|
||||
answer={answer}
|
||||
quotes={quotes}
|
||||
error={error}
|
||||
isFetching={isFetching}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
{quotes !== null && quotes.length > 0 && answer && (
|
||||
<QuotesSection quotes={dedupedQuotes} isFetching={isFetching} />
|
||||
)}
|
||||
|
||||
{searchResponse.messageId !== null && (
|
||||
<div className="absolute right-3 flex bottom-3">
|
||||
<HoverableIcon
|
||||
icon={<LikeFeedbackIcon />}
|
||||
onClick={() =>
|
||||
handleFeedback("like", searchResponse?.messageId as number)
|
||||
}
|
||||
/>
|
||||
<HoverableIcon
|
||||
icon={<DislikeFeedbackIcon />}
|
||||
onClick={() =>
|
||||
handleFeedback("dislike", searchResponse?.messageId as number)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!searchAnswerExpanded && searchAnswerOverflowing && (
|
||||
<div className="absolute bottom-0 left-0 w-full h-[100px] bg-gradient-to-b from-background/5 via-background/60 to-background/90"></div>
|
||||
)}
|
||||
|
||||
{!searchAnswerExpanded && searchAnswerOverflowing && (
|
||||
<div className="w-full h-12 absolute items-center content-center flex left-0 px-4 bottom-0">
|
||||
<button
|
||||
onClick={() => setSearchAnswerExpanded(true)}
|
||||
className="flex gap-x-1 items-center justify-center hover:bg-background-100 cursor-pointer max-w-sm text-sm mx-auto w-full bg-background border py-2 rounded-full"
|
||||
>
|
||||
Show more
|
||||
<ToggleDown />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -17,6 +17,12 @@ interface FullSearchBarProps {
|
||||
showingSidebar: boolean;
|
||||
}
|
||||
|
||||
import {
|
||||
TooltipProvider,
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useRef } from "react";
|
||||
import { SendIcon } from "../icons/icons";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
@ -28,61 +34,65 @@ import { CCPairBasicInfo, DocumentSet, Tag } from "@/lib/types";
|
||||
export const AnimatedToggle = ({
|
||||
isOn,
|
||||
handleToggle,
|
||||
direction = "top",
|
||||
}: {
|
||||
isOn: boolean;
|
||||
handleToggle: () => void;
|
||||
direction?: "bottom" | "top";
|
||||
}) => {
|
||||
const commandSymbol = KeyboardSymbol();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<CustomTooltip
|
||||
light
|
||||
large
|
||||
content={
|
||||
<div className="bg-white my-auto p-6 rounded-lg w-full">
|
||||
<h2 className="text-xl text-text-800 font-bold mb-2">
|
||||
Agentic Search
|
||||
</h2>
|
||||
<p className="text-text-700 text-sm mb-4">
|
||||
Our most powerful search, have an AI agent guide you to pinpoint
|
||||
exactly what you're looking for.
|
||||
</p>
|
||||
<Separator />
|
||||
<h2 className="text-xl text-text-800 font-bold mb-2">Fast Search</h2>
|
||||
<p className="text-text-700 text-sm mb-4">
|
||||
Get quality results immediately, best suited for instant access to
|
||||
your documents.
|
||||
</p>
|
||||
<p className="mt-2 flex text-xs">Shortcut: ({commandSymbol}/)</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="my-auto ml-auto flex justify-end items-center cursor-pointer"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<div ref={contentRef} className="flex items-center">
|
||||
{/* Toggle switch */}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={`
|
||||
w-10 h-6 flex items-center rounded-full p-1 transition-all duration-300 ease-in-out
|
||||
${isOn ? "bg-toggled-background" : "bg-untoggled-background"}
|
||||
`}
|
||||
ref={containerRef}
|
||||
className="my-auto ml-auto flex justify-end items-center cursor-pointer"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
bg-white w-4 h-4 rounded-full shadow-md transform transition-all duration-300 ease-in-out
|
||||
${isOn ? "translate-x-4" : ""}
|
||||
`}
|
||||
></div>
|
||||
<div ref={contentRef} className="flex items-center">
|
||||
<div
|
||||
className={`
|
||||
w-10 h-6 flex items-center rounded-full p-1 transition-all duration-300 ease-in-out
|
||||
${isOn ? "bg-toggled-background" : "bg-untoggled-background"}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
bg-white w-4 h-4 rounded-full shadow-md transform transition-all duration-300 ease-in-out
|
||||
${isOn ? "translate-x-4" : ""}
|
||||
`}
|
||||
></div>
|
||||
</div>
|
||||
<p className="ml-2 text-sm">Pro</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="ml-2 text-sm">Agentic</p>
|
||||
</div>
|
||||
</div>
|
||||
</CustomTooltip>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={direction} backgroundColor="bg-background-200">
|
||||
<div className="bg-white my-auto p-6 rounded-lg max-w-sm">
|
||||
<h2 className="text-xl text-text-800 font-bold mb-2">
|
||||
Agentic Search
|
||||
</h2>
|
||||
<p className="text-text-700 text-sm mb-4">
|
||||
Our most powerful search, have an AI agent guide you to pinpoint
|
||||
exactly what you're looking for.
|
||||
</p>
|
||||
<Separator />
|
||||
<h2 className="text-xl text-text-800 font-bold mb-2">
|
||||
Fast Search
|
||||
</h2>
|
||||
<p className="text-text-700 text-sm mb-4">
|
||||
Get quality results immediately, best suited for instant access to
|
||||
your documents.
|
||||
</p>
|
||||
<p className="mt-2 flex text-xs">Shortcut: ({commandSymbol}/)</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,301 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
DocumentRelevance,
|
||||
SearchDanswerDocument,
|
||||
SearchDefaultOverrides,
|
||||
SearchResponse,
|
||||
} from "@/lib/search/interfaces";
|
||||
import { usePopup } from "../admin/connectors/Popup";
|
||||
import { AlertIcon, MagnifyingIcon, UndoIcon } from "../icons/icons";
|
||||
import { AgenticDocumentDisplay, DocumentDisplay } from "./DocumentDisplay";
|
||||
import { searchState } from "./SearchSection";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipProvider,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import KeyboardSymbol from "@/lib/browserUtilities";
|
||||
import { DISABLE_LLM_DOC_RELEVANCE } from "@/lib/constants";
|
||||
|
||||
const getSelectedDocumentIds = (
|
||||
documents: SearchDanswerDocument[],
|
||||
selectedIndices: number[]
|
||||
) => {
|
||||
const selectedDocumentIds = new Set<string>();
|
||||
selectedIndices.forEach((ind) => {
|
||||
selectedDocumentIds.add(documents[ind].document_id);
|
||||
});
|
||||
return selectedDocumentIds;
|
||||
};
|
||||
|
||||
export const SearchResultsDisplay = ({
|
||||
agenticResults,
|
||||
searchResponse,
|
||||
contentEnriched,
|
||||
disabledAgentic,
|
||||
isFetching,
|
||||
defaultOverrides,
|
||||
performSweep,
|
||||
searchState,
|
||||
sweep,
|
||||
}: {
|
||||
searchState: searchState;
|
||||
disabledAgentic?: boolean;
|
||||
contentEnriched?: boolean;
|
||||
agenticResults?: boolean | null;
|
||||
performSweep: () => void;
|
||||
sweep?: boolean;
|
||||
searchResponse: SearchResponse | null;
|
||||
isFetching: boolean;
|
||||
defaultOverrides: SearchDefaultOverrides;
|
||||
comments: any;
|
||||
}) => {
|
||||
const commandSymbol = KeyboardSymbol();
|
||||
const { popup, setPopup } = usePopup();
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
switch (event.key.toLowerCase()) {
|
||||
case "o":
|
||||
event.preventDefault();
|
||||
|
||||
performSweep();
|
||||
if (agenticResults) {
|
||||
setShowAll((showAll) => !showAll);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [performSweep, agenticResults]);
|
||||
|
||||
if (!searchResponse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { answer, quotes, documents, error, messageId } = searchResponse;
|
||||
|
||||
if (isFetching && disabledAgentic) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="font-bold flex justify-between text-emphasis border-b mb-3 pb-1 border-border text-lg">
|
||||
<p>Results</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isFetching && !answer && !documents) {
|
||||
return null;
|
||||
}
|
||||
if (documents != null && documents.length == 0 && searchState == "input") {
|
||||
return (
|
||||
<div className="text-base gap-x-1.5 flex flex-col">
|
||||
<div className="flex gap-x-2 items-center font-semibold">
|
||||
<AlertIcon size={16} />
|
||||
No documents were found!
|
||||
</div>
|
||||
<p>
|
||||
Have you set up a connector? Your data may not have loaded properly.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
answer === null &&
|
||||
(documents === null || documents.length === 0) &&
|
||||
!isFetching
|
||||
) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
{error && (
|
||||
<div className="text-error text-sm">
|
||||
<div className="flex">
|
||||
<AlertIcon size={16} className="text-error my-auto mr-1" />
|
||||
<p className="italic">{error || "No documents were found!"}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const selectedDocumentIds = getSelectedDocumentIds(
|
||||
documents || [],
|
||||
searchResponse.selectedDocIndices || []
|
||||
);
|
||||
const relevantDocs = documents
|
||||
? documents.filter((doc) => {
|
||||
return (
|
||||
showAll ||
|
||||
(searchResponse &&
|
||||
searchResponse.additional_relevance &&
|
||||
searchResponse.additional_relevance[doc.document_id] &&
|
||||
searchResponse.additional_relevance[doc.document_id].relevant) ||
|
||||
doc.is_relevant
|
||||
);
|
||||
})
|
||||
: [];
|
||||
|
||||
const getUniqueDocuments = (
|
||||
documents: SearchDanswerDocument[]
|
||||
): SearchDanswerDocument[] => {
|
||||
const seenIds = new Set<string>();
|
||||
return documents.filter((doc) => {
|
||||
if (!seenIds.has(doc.document_id)) {
|
||||
seenIds.add(doc.document_id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const uniqueDocuments = getUniqueDocuments(documents || []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
|
||||
{documents && documents.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<div className="font-bold flex h-12 justify-between text-emphasis border-b mb-3 pb-1 border-border text-lg">
|
||||
<p>Results</p>
|
||||
{!DISABLE_LLM_DOC_RELEVANCE &&
|
||||
(contentEnriched || searchResponse.additional_relevance) && (
|
||||
<TooltipProvider delayDuration={1000}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => {
|
||||
performSweep();
|
||||
if (agenticResults) {
|
||||
setShowAll((showAll) => !showAll);
|
||||
}
|
||||
}}
|
||||
className={`flex items-center justify-center animate-fade-in-up rounded-lg p-1 text-xs transition-all duration-300 w-20 h-8 ${
|
||||
!sweep
|
||||
? "bg-background-agentic-toggled text-text-agentic-toggled"
|
||||
: "bg-background-agentic-untoggled text-text-agentic-untoggled"
|
||||
}`}
|
||||
style={{
|
||||
transform: sweep
|
||||
? "rotateZ(180deg)"
|
||||
: "rotateZ(0deg)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center ${
|
||||
sweep ? "rotate-180" : ""
|
||||
}`}
|
||||
>
|
||||
<span></span>
|
||||
{!sweep
|
||||
? agenticResults
|
||||
? "Show All"
|
||||
: "Focus"
|
||||
: agenticResults
|
||||
? "Focus"
|
||||
: "Show All"}
|
||||
|
||||
<span className="ml-1">
|
||||
{!sweep ? (
|
||||
<MagnifyingIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<UndoIcon className="h-4 w-4" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="flex">{commandSymbol}O</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{agenticResults &&
|
||||
relevantDocs &&
|
||||
contentEnriched &&
|
||||
relevantDocs.length == 0 &&
|
||||
!showAll && (
|
||||
<p className="flex text-lg font-bold">
|
||||
No high quality results found by agentic search.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{uniqueDocuments.map((document, ind) => {
|
||||
const relevance: DocumentRelevance | null =
|
||||
searchResponse.additional_relevance
|
||||
? searchResponse.additional_relevance[document.document_id]
|
||||
: null;
|
||||
|
||||
return agenticResults ? (
|
||||
<AgenticDocumentDisplay
|
||||
additional_relevance={relevance}
|
||||
contentEnriched={contentEnriched}
|
||||
index={ind}
|
||||
hide={showAll || relevance?.relevant || document.is_relevant}
|
||||
key={`${document.document_id}-${ind}`}
|
||||
document={document}
|
||||
documentRank={ind + 1}
|
||||
messageId={messageId}
|
||||
isSelected={selectedDocumentIds.has(document.document_id)}
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
) : (
|
||||
<DocumentDisplay
|
||||
additional_relevance={relevance}
|
||||
contentEnriched={contentEnriched}
|
||||
index={ind}
|
||||
hide={sweep && !document.is_relevant && !relevance?.relevant}
|
||||
key={`${document.document_id}-${ind}`}
|
||||
document={document}
|
||||
documentRank={ind + 1}
|
||||
messageId={messageId}
|
||||
isSelected={selectedDocumentIds.has(document.document_id)}
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="h-[100px]" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export function AgenticDisclaimer({
|
||||
forceNonAgentic,
|
||||
}: {
|
||||
forceNonAgentic: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="ml-auto mx-12 flex transition-all duration-300 animate-fade-in flex-col gap-y-2">
|
||||
<p className="text-sm">
|
||||
Please note that agentic quries can take substantially longer than
|
||||
non-agentic queries. You can click <i>non-agentic</i> to re-submit your
|
||||
query without agentic capabilities.
|
||||
</p>
|
||||
<button
|
||||
onClick={forceNonAgentic}
|
||||
className="p-2 bg-background-900 mr-auto text-text-200 rounded-lg text-xs my-auto"
|
||||
>
|
||||
Non-agentic
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,856 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||
import { FullSearchBar } from "./SearchBar";
|
||||
import { SearchResultsDisplay } from "./SearchResultsDisplay";
|
||||
import { SourceSelector } from "./filtering/Filters";
|
||||
import {
|
||||
Quote,
|
||||
SearchResponse,
|
||||
FlowType,
|
||||
SearchType,
|
||||
SearchDefaultOverrides,
|
||||
SearchRequestOverrides,
|
||||
ValidQuestionResponse,
|
||||
Relevance,
|
||||
SearchDanswerDocument,
|
||||
SourceMetadata,
|
||||
} from "@/lib/search/interfaces";
|
||||
import { searchRequestStreamed } from "@/lib/search/streamingQa";
|
||||
import { CancellationToken, cancellable } from "@/lib/search/cancellable";
|
||||
import { useFilters, useObjectState } from "@/lib/hooks";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { computeAvailableFilters } from "@/lib/filters";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
import { HistorySidebar } from "@/app/chat/sessionSidebar/HistorySidebar";
|
||||
import { ChatSession, SearchSession } from "@/app/chat/interfaces";
|
||||
import FunctionalHeader from "../chat_search/Header";
|
||||
import { useSidebarVisibility } from "../chat_search/hooks";
|
||||
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "../resizable/constants";
|
||||
import { AGENTIC_SEARCH_TYPE_COOKIE_NAME } from "@/lib/constants";
|
||||
import Cookies from "js-cookie";
|
||||
import FixedLogo from "@/app/chat/shared_chat_search/FixedLogo";
|
||||
import { usePopup } from "../admin/connectors/Popup";
|
||||
import { FeedbackType } from "@/app/chat/types";
|
||||
import { FeedbackModal } from "@/app/chat/modal/FeedbackModal";
|
||||
import { deleteChatSession, handleChatFeedback } from "@/app/chat/lib";
|
||||
import SearchAnswer from "./SearchAnswer";
|
||||
import { DeleteEntityModal } from "../modals/DeleteEntityModal";
|
||||
import { ApiKeyModal } from "../llm/ApiKeyModal";
|
||||
import { useSearchContext } from "../context/SearchContext";
|
||||
import { useUser } from "../user/UserProvider";
|
||||
import UnconfiguredProviderText from "../chat_search/UnconfiguredProviderText";
|
||||
import { DateRangePickerValue } from "@/app/ee/admin/performance/DateRangeSelector";
|
||||
import { Tag } from "@/lib/types";
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
export type searchState =
|
||||
| "input"
|
||||
| "searching"
|
||||
| "reading"
|
||||
| "analyzing"
|
||||
| "summarizing"
|
||||
| "generating"
|
||||
| "citing";
|
||||
|
||||
const SEARCH_DEFAULT_OVERRIDES_START: SearchDefaultOverrides = {
|
||||
forceDisplayQA: false,
|
||||
offset: 0,
|
||||
};
|
||||
|
||||
interface SearchSectionProps {
|
||||
toggle: () => void;
|
||||
defaultSearchType: SearchType;
|
||||
toggledSidebar: boolean;
|
||||
}
|
||||
|
||||
export const SearchSection = ({
|
||||
toggle,
|
||||
toggledSidebar,
|
||||
defaultSearchType,
|
||||
}: SearchSectionProps) => {
|
||||
const {
|
||||
querySessions,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
assistants,
|
||||
tags,
|
||||
shouldShowWelcomeModal,
|
||||
agenticSearchEnabled,
|
||||
disabledAgentic,
|
||||
shouldDisplayNoSources,
|
||||
} = useSearchContext();
|
||||
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [comments, setComments] = useState<any>(null);
|
||||
const [contentEnriched, setContentEnriched] = useState(false);
|
||||
|
||||
const [searchResponse, setSearchResponse] = useState<SearchResponse>({
|
||||
suggestedSearchType: null,
|
||||
suggestedFlowType: null,
|
||||
answer: null,
|
||||
quotes: null,
|
||||
documents: null,
|
||||
selectedDocIndices: null,
|
||||
error: null,
|
||||
messageId: null,
|
||||
});
|
||||
|
||||
const [showApiKeyModal, setShowApiKeyModal] = useState(true);
|
||||
|
||||
const [agentic, setAgentic] = useState(agenticSearchEnabled);
|
||||
|
||||
const toggleAgentic = useCallback(() => {
|
||||
Cookies.set(
|
||||
AGENTIC_SEARCH_TYPE_COOKIE_NAME,
|
||||
String(!agentic).toLocaleLowerCase()
|
||||
);
|
||||
setAgentic((agentic) => !agentic);
|
||||
}, [agentic]);
|
||||
|
||||
const toggleSidebar = useCallback(() => {
|
||||
Cookies.set(
|
||||
SIDEBAR_TOGGLED_COOKIE_NAME,
|
||||
String(!toggledSidebar).toLocaleLowerCase()
|
||||
),
|
||||
{
|
||||
path: "/",
|
||||
};
|
||||
toggle();
|
||||
}, [toggledSidebar, toggle]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
switch (event.key.toLowerCase()) {
|
||||
case "/":
|
||||
toggleAgentic();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [toggleAgentic]);
|
||||
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
|
||||
// Search Type
|
||||
const selectedSearchType = defaultSearchType;
|
||||
|
||||
// If knowledge assistant exists, use it. Otherwise, use first available assistant for search.
|
||||
const selectedPersona = assistants.find((assistant) => assistant.id === 0)
|
||||
? 0
|
||||
: assistants[0]?.id;
|
||||
|
||||
// Used for search state display
|
||||
const [analyzeStartTime, setAnalyzeStartTime] = useState<number>(0);
|
||||
|
||||
// Filters
|
||||
const filterManager = useFilters();
|
||||
const availableSources = ccPairs.map((ccPair) => ccPair.source);
|
||||
const [finalAvailableSources, finalAvailableDocumentSets] =
|
||||
computeAvailableFilters({
|
||||
selectedPersona: assistants.find(
|
||||
(assistant) => assistant.id === selectedPersona
|
||||
),
|
||||
availableSources: availableSources,
|
||||
availableDocumentSets: documentSets,
|
||||
});
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const existingSearchessionId = searchParams.get("searchId");
|
||||
|
||||
useEffect(() => {
|
||||
if (existingSearchessionId == null) {
|
||||
return;
|
||||
}
|
||||
function extractFirstMessageByType(
|
||||
chatSession: SearchSession,
|
||||
messageType: "user" | "assistant"
|
||||
): string | null {
|
||||
const userMessage = chatSession?.messages.find(
|
||||
(msg) => msg.message_type === messageType
|
||||
);
|
||||
return userMessage ? userMessage.message : null;
|
||||
}
|
||||
|
||||
async function initialSessionFetch() {
|
||||
const response = await fetch(
|
||||
`/api/query/search-session/${existingSearchessionId}`
|
||||
);
|
||||
const searchSession = (await response.json()) as SearchSession;
|
||||
const userMessage = extractFirstMessageByType(searchSession, "user");
|
||||
const assistantMessage = extractFirstMessageByType(
|
||||
searchSession,
|
||||
"assistant"
|
||||
);
|
||||
|
||||
if (userMessage) {
|
||||
setQuery(userMessage);
|
||||
const danswerDocs: SearchResponse = {
|
||||
documents: searchSession.documents,
|
||||
suggestedSearchType: null,
|
||||
answer: assistantMessage || "Search response not found",
|
||||
quotes: null,
|
||||
selectedDocIndices: null,
|
||||
error: null,
|
||||
messageId: searchSession.messages[0].message_id,
|
||||
suggestedFlowType: null,
|
||||
additional_relevance: undefined,
|
||||
};
|
||||
|
||||
setIsFetching(false);
|
||||
setFirstSearch(false);
|
||||
setSearchResponse(danswerDocs);
|
||||
setContentEnriched(true);
|
||||
}
|
||||
}
|
||||
initialSessionFetch();
|
||||
}, [existingSearchessionId]);
|
||||
|
||||
// Overrides for default behavior that only last a single query
|
||||
const [defaultOverrides, setDefaultOverrides] =
|
||||
useState<SearchDefaultOverrides>(SEARCH_DEFAULT_OVERRIDES_START);
|
||||
|
||||
const newSearchState = (
|
||||
currentSearchState: searchState,
|
||||
newSearchState: searchState
|
||||
) => {
|
||||
if (currentSearchState != "input") {
|
||||
return newSearchState;
|
||||
}
|
||||
return "input";
|
||||
};
|
||||
|
||||
// Helpers
|
||||
const initialSearchResponse: SearchResponse = {
|
||||
answer: null,
|
||||
quotes: null,
|
||||
documents: null,
|
||||
suggestedSearchType: null,
|
||||
suggestedFlowType: null,
|
||||
selectedDocIndices: null,
|
||||
error: null,
|
||||
messageId: null,
|
||||
additional_relevance: undefined,
|
||||
};
|
||||
// Streaming updates
|
||||
const updateCurrentAnswer = (answer: string) => {
|
||||
setSearchResponse((prevState) => ({
|
||||
...(prevState || initialSearchResponse),
|
||||
answer,
|
||||
}));
|
||||
|
||||
if (analyzeStartTime) {
|
||||
const elapsedTime = Date.now() - analyzeStartTime;
|
||||
const nextInterval = Math.ceil(elapsedTime / 1500) * 1500;
|
||||
setTimeout(() => {
|
||||
setSearchState((searchState) =>
|
||||
newSearchState(searchState, "generating")
|
||||
);
|
||||
}, nextInterval - elapsedTime);
|
||||
}
|
||||
};
|
||||
|
||||
const updateQuotes = (quotes: Quote[]) => {
|
||||
setSearchResponse((prevState) => ({
|
||||
...(prevState || initialSearchResponse),
|
||||
quotes,
|
||||
}));
|
||||
setSearchState((searchState) => "citing");
|
||||
};
|
||||
|
||||
const updateDocs = (documents: SearchDanswerDocument[]) => {
|
||||
if (agentic) {
|
||||
setTimeout(() => {
|
||||
setSearchState((searchState) => newSearchState(searchState, "reading"));
|
||||
}, 1500);
|
||||
|
||||
setTimeout(() => {
|
||||
setAnalyzeStartTime(Date.now());
|
||||
setSearchState((searchState) => {
|
||||
const newState = newSearchState(searchState, "analyzing");
|
||||
if (newState === "analyzing") {
|
||||
setAnalyzeStartTime(Date.now());
|
||||
}
|
||||
return newState;
|
||||
});
|
||||
}, 4500);
|
||||
}
|
||||
|
||||
setSearchResponse((prevState) => ({
|
||||
...(prevState || initialSearchResponse),
|
||||
documents,
|
||||
}));
|
||||
if (disabledAgentic) {
|
||||
setIsFetching(false);
|
||||
setSearchState((searchState) => "citing");
|
||||
}
|
||||
if (documents.length == 0) {
|
||||
setSearchState("input");
|
||||
}
|
||||
};
|
||||
const updateSuggestedSearchType = (suggestedSearchType: SearchType) =>
|
||||
setSearchResponse((prevState) => ({
|
||||
...(prevState || initialSearchResponse),
|
||||
suggestedSearchType,
|
||||
}));
|
||||
const updateSuggestedFlowType = (suggestedFlowType: FlowType) =>
|
||||
setSearchResponse((prevState) => ({
|
||||
...(prevState || initialSearchResponse),
|
||||
suggestedFlowType,
|
||||
}));
|
||||
const updateSelectedDocIndices = (docIndices: number[]) =>
|
||||
setSearchResponse((prevState) => ({
|
||||
...(prevState || initialSearchResponse),
|
||||
selectedDocIndices: docIndices,
|
||||
}));
|
||||
const updateError = (error: FlowType) => {
|
||||
resetInput(true);
|
||||
|
||||
setSearchResponse((prevState) => ({
|
||||
...(prevState || initialSearchResponse),
|
||||
error,
|
||||
}));
|
||||
};
|
||||
const updateMessageAndThreadId = (
|
||||
messageId: number,
|
||||
chat_session_id: string
|
||||
) => {
|
||||
setSearchResponse((prevState) => ({
|
||||
...(prevState || initialSearchResponse),
|
||||
messageId,
|
||||
}));
|
||||
router.refresh();
|
||||
setIsFetching(false);
|
||||
setSearchState((searchState) => "input");
|
||||
};
|
||||
|
||||
const updateDocumentRelevance = (relevance: Relevance) => {
|
||||
setSearchResponse((prevState) => ({
|
||||
...(prevState || initialSearchResponse),
|
||||
additional_relevance: relevance,
|
||||
}));
|
||||
|
||||
setContentEnriched(true);
|
||||
|
||||
setIsFetching(false);
|
||||
if (disabledAgentic) {
|
||||
setSearchState("input");
|
||||
} else {
|
||||
setSearchState("analyzing");
|
||||
}
|
||||
};
|
||||
|
||||
const updateComments = (comments: any) => {
|
||||
setComments(comments);
|
||||
};
|
||||
|
||||
const finishedSearching = () => {
|
||||
if (disabledAgentic) {
|
||||
setSearchState("input");
|
||||
}
|
||||
};
|
||||
const { user } = useUser();
|
||||
const [searchAnswerExpanded, setSearchAnswerExpanded] = useState(false);
|
||||
|
||||
const resetInput = (finalized?: boolean) => {
|
||||
setSweep(false);
|
||||
setFirstSearch(false);
|
||||
setComments(null);
|
||||
setSearchState(finalized ? "input" : "searching");
|
||||
setSearchAnswerExpanded(false);
|
||||
};
|
||||
|
||||
interface SearchDetails {
|
||||
query: string;
|
||||
sources: SourceMetadata[];
|
||||
agentic: boolean;
|
||||
documentSets: string[];
|
||||
timeRange: DateRangePickerValue | null;
|
||||
tags: Tag[];
|
||||
persona: Persona;
|
||||
}
|
||||
|
||||
const [previousSearch, setPreviousSearch] = useState<null | SearchDetails>(
|
||||
null
|
||||
);
|
||||
const [agenticResults, setAgenticResults] = useState<boolean | null>(null);
|
||||
const currentSearch = (overrideMessage?: string): SearchDetails => {
|
||||
return {
|
||||
query: overrideMessage || query,
|
||||
sources: filterManager.selectedSources,
|
||||
agentic: agentic!,
|
||||
documentSets: filterManager.selectedDocumentSets,
|
||||
timeRange: filterManager.timeRange,
|
||||
tags: filterManager.selectedTags,
|
||||
persona: assistants.find(
|
||||
(assistant) => assistant.id === selectedPersona
|
||||
) as Persona,
|
||||
};
|
||||
};
|
||||
const isSearchChanged = () => {
|
||||
return !isEqual(currentSearch(), previousSearch);
|
||||
};
|
||||
|
||||
let lastSearchCancellationToken = useRef<CancellationToken | null>(null);
|
||||
const onSearch = async ({
|
||||
searchType,
|
||||
agentic,
|
||||
offset,
|
||||
overrideMessage,
|
||||
}: SearchRequestOverrides = {}) => {
|
||||
if ((overrideMessage || query) == "") {
|
||||
return;
|
||||
}
|
||||
setAgenticResults(agentic!);
|
||||
resetInput();
|
||||
setContentEnriched(false);
|
||||
|
||||
if (lastSearchCancellationToken.current) {
|
||||
lastSearchCancellationToken.current.cancel();
|
||||
}
|
||||
lastSearchCancellationToken.current = new CancellationToken();
|
||||
|
||||
setIsFetching(true);
|
||||
setSearchResponse(initialSearchResponse);
|
||||
|
||||
setPreviousSearch(currentSearch(overrideMessage));
|
||||
|
||||
const searchFnArgs = {
|
||||
query: overrideMessage || query,
|
||||
sources: filterManager.selectedSources,
|
||||
agentic: agentic,
|
||||
documentSets: filterManager.selectedDocumentSets,
|
||||
timeRange: filterManager.timeRange,
|
||||
tags: filterManager.selectedTags,
|
||||
persona: assistants.find(
|
||||
(assistant) => assistant.id === selectedPersona
|
||||
) as Persona,
|
||||
updateCurrentAnswer: cancellable({
|
||||
cancellationToken: lastSearchCancellationToken.current,
|
||||
fn: updateCurrentAnswer,
|
||||
}),
|
||||
updateQuotes: cancellable({
|
||||
cancellationToken: lastSearchCancellationToken.current,
|
||||
fn: updateQuotes,
|
||||
}),
|
||||
updateDocs: cancellable({
|
||||
cancellationToken: lastSearchCancellationToken.current,
|
||||
fn: updateDocs,
|
||||
}),
|
||||
updateSuggestedSearchType: cancellable({
|
||||
cancellationToken: lastSearchCancellationToken.current,
|
||||
fn: updateSuggestedSearchType,
|
||||
}),
|
||||
updateSuggestedFlowType: cancellable({
|
||||
cancellationToken: lastSearchCancellationToken.current,
|
||||
fn: updateSuggestedFlowType,
|
||||
}),
|
||||
updateSelectedDocIndices: cancellable({
|
||||
cancellationToken: lastSearchCancellationToken.current,
|
||||
fn: updateSelectedDocIndices,
|
||||
}),
|
||||
updateError: cancellable({
|
||||
cancellationToken: lastSearchCancellationToken.current,
|
||||
fn: updateError,
|
||||
}),
|
||||
updateMessageAndThreadId: cancellable({
|
||||
cancellationToken: lastSearchCancellationToken.current,
|
||||
fn: updateMessageAndThreadId,
|
||||
}),
|
||||
updateDocStatus: cancellable({
|
||||
cancellationToken: lastSearchCancellationToken.current,
|
||||
fn: updateMessageAndThreadId,
|
||||
}),
|
||||
updateDocumentRelevance: cancellable({
|
||||
cancellationToken: lastSearchCancellationToken.current,
|
||||
fn: updateDocumentRelevance,
|
||||
}),
|
||||
updateComments: cancellable({
|
||||
cancellationToken: lastSearchCancellationToken.current,
|
||||
fn: updateComments,
|
||||
}),
|
||||
finishedSearching: cancellable({
|
||||
cancellationToken: lastSearchCancellationToken.current,
|
||||
fn: finishedSearching,
|
||||
}),
|
||||
selectedSearchType: searchType ?? selectedSearchType,
|
||||
offset: offset ?? defaultOverrides.offset,
|
||||
};
|
||||
|
||||
await Promise.all([searchRequestStreamed(searchFnArgs)]);
|
||||
};
|
||||
|
||||
// handle redirect if search page is disabled
|
||||
// NOTE: this must be done here, in a client component since
|
||||
// settings are passed in via Context and therefore aren't
|
||||
// available in server-side components
|
||||
const router = useRouter();
|
||||
const settings = useContext(SettingsContext);
|
||||
if (settings?.settings?.search_page_enabled === false) {
|
||||
router.push("/chat");
|
||||
}
|
||||
const sidebarElementRef = useRef<HTMLDivElement>(null);
|
||||
const innerSidebarElementRef = useRef<HTMLDivElement>(null);
|
||||
const [showDocSidebar, setShowDocSidebar] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
switch (event.key.toLowerCase()) {
|
||||
case "e":
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [router, toggleSidebar]);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings?.isMobile) {
|
||||
router.push("/chat");
|
||||
}
|
||||
}, [settings?.isMobile, router]);
|
||||
|
||||
const handleTransitionEnd = (e: React.TransitionEvent<HTMLDivElement>) => {
|
||||
if (e.propertyName === "opacity" && !firstSearch) {
|
||||
const target = e.target as HTMLDivElement;
|
||||
target.style.display = "none";
|
||||
}
|
||||
};
|
||||
const [sweep, setSweep] = useState(false);
|
||||
const performSweep = () => {
|
||||
setSweep((sweep) => !sweep);
|
||||
};
|
||||
const [firstSearch, setFirstSearch] = useState(true);
|
||||
const [searchState, setSearchState] = useState<searchState>("input");
|
||||
const [deletingChatSession, setDeletingChatSession] =
|
||||
useState<ChatSession | null>();
|
||||
|
||||
const showDeleteModal = (chatSession: ChatSession) => {
|
||||
setDeletingChatSession(chatSession);
|
||||
};
|
||||
// Used to maintain a "time out" for history sidebar so our existing refs can have time to process change
|
||||
const [untoggled, setUntoggled] = useState(false);
|
||||
|
||||
const explicitlyUntoggle = () => {
|
||||
setShowDocSidebar(false);
|
||||
|
||||
setUntoggled(true);
|
||||
setTimeout(() => {
|
||||
setUntoggled(false);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
useSidebarVisibility({
|
||||
toggledSidebar,
|
||||
sidebarElementRef,
|
||||
showDocSidebar,
|
||||
setShowDocSidebar,
|
||||
mobile: settings?.isMobile,
|
||||
});
|
||||
const { answer, quotes, documents, error, messageId } = searchResponse;
|
||||
|
||||
const dedupedQuotes: Quote[] = [];
|
||||
const seen = new Set<string>();
|
||||
if (quotes) {
|
||||
quotes.forEach((quote) => {
|
||||
if (!seen.has(quote.document_id)) {
|
||||
dedupedQuotes.push(quote);
|
||||
seen.add(quote.document_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
const [currentFeedback, setCurrentFeedback] = useState<
|
||||
[FeedbackType, number] | null
|
||||
>(null);
|
||||
|
||||
const onFeedback = async (
|
||||
messageId: number,
|
||||
feedbackType: FeedbackType,
|
||||
feedbackDetails: string,
|
||||
predefinedFeedback: string | undefined
|
||||
) => {
|
||||
const response = await handleChatFeedback(
|
||||
messageId,
|
||||
feedbackType,
|
||||
feedbackDetails,
|
||||
predefinedFeedback
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
setPopup({
|
||||
message: "Thanks for your feedback!",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
const responseJson = await response.json();
|
||||
const errorMsg = responseJson.detail || responseJson.message;
|
||||
setPopup({
|
||||
message: `Failed to submit feedback - ${errorMsg}`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const chatBannerPresent = settings?.enterpriseSettings?.custom_header_content;
|
||||
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
const shouldUseAgenticDisplay =
|
||||
agenticResults &&
|
||||
(searchResponse.documents || []).some(
|
||||
(document) =>
|
||||
searchResponse.additional_relevance &&
|
||||
searchResponse.additional_relevance[document.document_id] !== undefined
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex relative pr-[8px] h-full text-default">
|
||||
{popup}
|
||||
|
||||
{!shouldDisplayNoSources &&
|
||||
showApiKeyModal &&
|
||||
!shouldShowWelcomeModal && (
|
||||
<ApiKeyModal
|
||||
setPopup={setPopup}
|
||||
hide={() => setShowApiKeyModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deletingChatSession && (
|
||||
<DeleteEntityModal
|
||||
entityType="search"
|
||||
entityName={deletingChatSession.name}
|
||||
onClose={() => setDeletingChatSession(null)}
|
||||
onSubmit={async () => {
|
||||
const response = await deleteChatSession(deletingChatSession.id);
|
||||
if (response.ok) {
|
||||
setDeletingChatSession(null);
|
||||
// go back to the main page
|
||||
router.push("/search");
|
||||
} else {
|
||||
const responseJson = await response.json();
|
||||
setPopup({ message: responseJson.detail, type: "error" });
|
||||
}
|
||||
router.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{currentFeedback && (
|
||||
<FeedbackModal
|
||||
feedbackType={currentFeedback[0]}
|
||||
onClose={() => setCurrentFeedback(null)}
|
||||
onSubmit={({ message, predefinedFeedback }) => {
|
||||
onFeedback(
|
||||
currentFeedback[1],
|
||||
currentFeedback[0],
|
||||
message,
|
||||
predefinedFeedback
|
||||
);
|
||||
setCurrentFeedback(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={sidebarElementRef}
|
||||
className={`
|
||||
flex-none
|
||||
fixed
|
||||
left-0
|
||||
z-30
|
||||
bg-background-100
|
||||
h-screen
|
||||
transition-all
|
||||
bg-opacity-80
|
||||
duration-300
|
||||
ease-in-out
|
||||
${
|
||||
!untoggled && (showDocSidebar || toggledSidebar)
|
||||
? "opacity-100 w-[250px] translate-x-0"
|
||||
: "opacity-0 w-[200px] pointer-events-none -translate-x-10"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="w-full relative">
|
||||
<HistorySidebar
|
||||
showDeleteModal={showDeleteModal}
|
||||
explicitlyUntoggle={explicitlyUntoggle}
|
||||
reset={() => setQuery("")}
|
||||
page="search"
|
||||
ref={innerSidebarElementRef}
|
||||
toggleSidebar={toggleSidebar}
|
||||
toggled={toggledSidebar}
|
||||
existingChats={querySessions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute default-scrollbar h-screen overflow-y-auto overflow-x-hidden left-0 w-full top-0">
|
||||
<FunctionalHeader
|
||||
sidebarToggled={toggledSidebar}
|
||||
reset={() => setQuery("")}
|
||||
toggleSidebar={toggleSidebar}
|
||||
page="search"
|
||||
/>
|
||||
<div className="w-full flex">
|
||||
<div
|
||||
style={{ transition: "width 0.30s ease-out" }}
|
||||
className={`
|
||||
flex-none
|
||||
overflow-y-hidden
|
||||
bg-background-100
|
||||
h-full
|
||||
transition-all
|
||||
bg-opacity-80
|
||||
duration-300
|
||||
ease-in-out
|
||||
${toggledSidebar ? "w-[250px]" : "w-[0px]"}
|
||||
`}
|
||||
/>
|
||||
|
||||
{
|
||||
<div
|
||||
className={`desktop:px-24 w-full ${
|
||||
chatBannerPresent && "mt-10"
|
||||
} pt-10 relative max-w-[2000px] xl:max-w-[1430px] mx-auto`}
|
||||
>
|
||||
<div className="absolute z-10 mobile:px-4 mobile:max-w-searchbar-max mobile:w-[90%] top-12 desktop:left-4 hidden 2xl:block mobile:left-1/2 mobile:transform mobile:-translate-x-1/2 desktop:w-52 3xl:w-64">
|
||||
{!settings?.isMobile &&
|
||||
(ccPairs.length > 0 || documentSets.length > 0) && (
|
||||
<SourceSelector
|
||||
{...filterManager}
|
||||
showDocSidebar={toggledSidebar}
|
||||
availableDocumentSets={finalAvailableDocumentSets}
|
||||
existingSources={finalAvailableSources}
|
||||
availableTags={tags}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute left-0 hidden 2xl:block w-52 3xl:w-64"></div>
|
||||
<div className="max-w-searchbar-max w-[90%] mx-auto">
|
||||
{settings?.isMobile && (
|
||||
<div className="mt-6">
|
||||
{!(agenticResults && isFetching) || disabledAgentic ? (
|
||||
<SearchResultsDisplay
|
||||
searchState={searchState}
|
||||
disabledAgentic={disabledAgentic}
|
||||
contentEnriched={contentEnriched}
|
||||
comments={comments}
|
||||
sweep={sweep}
|
||||
agenticResults={agenticResults && !disabledAgentic}
|
||||
performSweep={performSweep}
|
||||
searchResponse={searchResponse}
|
||||
isFetching={isFetching}
|
||||
defaultOverrides={defaultOverrides}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`mobile:fixed mobile:left-1/2 mobile:transform mobile:-translate-x-1/2 mobile:max-w-search-bar-max mobile:w-[90%] mobile:z-100 mobile:bottom-12`}
|
||||
>
|
||||
<div
|
||||
className={`transition-all duration-500 ease-in-out overflow-hidden
|
||||
${
|
||||
firstSearch
|
||||
? "opacity-100 max-h-[500px]"
|
||||
: "opacity-0 max-h-0"
|
||||
}`}
|
||||
onTransitionEnd={handleTransitionEnd}
|
||||
>
|
||||
<div className="mt-48 mb-8 flex justify-center items-center">
|
||||
<div className="w-message-xs 2xl:w-message-sm 3xl:w-message">
|
||||
<div className="flex">
|
||||
<div className="text-3xl font-bold font-strong text-strong mx-auto">
|
||||
Unlock Knowledge
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UnconfiguredProviderText
|
||||
noSources={shouldDisplayNoSources}
|
||||
showConfigureAPIKey={() => setShowApiKeyModal(true)}
|
||||
/>
|
||||
|
||||
<FullSearchBar
|
||||
disabled={!isSearchChanged()}
|
||||
toggleAgentic={
|
||||
disabledAgentic ? undefined : toggleAgentic
|
||||
}
|
||||
showingSidebar={toggledSidebar}
|
||||
agentic={agentic}
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
onSearch={async (agentic?: boolean) => {
|
||||
setDefaultOverrides(SEARCH_DEFAULT_OVERRIDES_START);
|
||||
await onSearch({ agentic, offset: 0 });
|
||||
}}
|
||||
finalAvailableDocumentSets={finalAvailableDocumentSets}
|
||||
finalAvailableSources={finalAvailableSources}
|
||||
filterManager={filterManager}
|
||||
documentSets={documentSets}
|
||||
ccPairs={ccPairs}
|
||||
tags={tags}
|
||||
/>
|
||||
</div>
|
||||
{!firstSearch && (
|
||||
<SearchAnswer
|
||||
isFetching={isFetching}
|
||||
dedupedQuotes={dedupedQuotes}
|
||||
searchResponse={searchResponse}
|
||||
setSearchAnswerExpanded={setSearchAnswerExpanded}
|
||||
searchAnswerExpanded={searchAnswerExpanded}
|
||||
setCurrentFeedback={setCurrentFeedback}
|
||||
searchState={searchState}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!settings?.isMobile && (
|
||||
<div className="mt-6">
|
||||
{!(agenticResults && isFetching) || disabledAgentic ? (
|
||||
<SearchResultsDisplay
|
||||
searchState={searchState}
|
||||
disabledAgentic={disabledAgentic}
|
||||
contentEnriched={contentEnriched}
|
||||
comments={comments}
|
||||
sweep={sweep}
|
||||
agenticResults={
|
||||
shouldUseAgenticDisplay && !disabledAgentic
|
||||
}
|
||||
performSweep={performSweep}
|
||||
searchResponse={searchResponse}
|
||||
isFetching={isFetching}
|
||||
defaultOverrides={defaultOverrides}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FixedLogo backgroundToggled={toggledSidebar || showDocSidebar} />
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,43 +0,0 @@
|
||||
import { SearchType } from "@/lib/search/interfaces";
|
||||
|
||||
const defaultStyle =
|
||||
"py-1 px-2 border rounded border-gray-700 cursor-pointer font-bold ";
|
||||
|
||||
interface Props {
|
||||
selectedSearchType: SearchType;
|
||||
setSelectedSearchType: (searchType: SearchType) => void;
|
||||
}
|
||||
|
||||
export const SearchTypeSelector: React.FC<Props> = ({
|
||||
selectedSearchType,
|
||||
setSelectedSearchType,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex text-xs">
|
||||
<div
|
||||
className={
|
||||
defaultStyle +
|
||||
(selectedSearchType === SearchType.SEMANTIC
|
||||
? "bg-blue-500"
|
||||
: "bg-gray-800 hover:bg-gray-600")
|
||||
}
|
||||
onClick={() => setSelectedSearchType(SearchType.SEMANTIC)}
|
||||
>
|
||||
AI Search
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
defaultStyle +
|
||||
"ml-2 " +
|
||||
(selectedSearchType === SearchType.KEYWORD
|
||||
? "bg-blue-500"
|
||||
: "bg-gray-800 hover:bg-gray-600")
|
||||
}
|
||||
onClick={() => setSelectedSearchType(SearchType.KEYWORD)}
|
||||
>
|
||||
Keyword Search
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -33,6 +33,7 @@ import {
|
||||
getDateRangeString,
|
||||
getTimeAgoString,
|
||||
} from "@/lib/dateUtils";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
const SectionTitle = ({ children }: { children: string }) => (
|
||||
<div className="font-bold text-xs mt-2 flex">{children}</div>
|
||||
@ -53,6 +54,9 @@ export interface SourceSelectorProps {
|
||||
availableDocumentSets: DocumentSet[];
|
||||
existingSources: ValidSources[];
|
||||
availableTags: Tag[];
|
||||
toggleFilters?: () => void;
|
||||
filtersUntoggled?: boolean;
|
||||
tagsOnLeft?: boolean;
|
||||
}
|
||||
|
||||
export function SourceSelector({
|
||||
@ -68,6 +72,9 @@ export function SourceSelector({
|
||||
existingSources,
|
||||
availableTags,
|
||||
showDocSidebar,
|
||||
toggleFilters,
|
||||
filtersUntoggled,
|
||||
tagsOnLeft,
|
||||
}: SourceSelectorProps) {
|
||||
const handleSelect = (source: SourceMetadata) => {
|
||||
setSelectedSources((prev: SourceMetadata[]) => {
|
||||
@ -110,138 +117,155 @@ export function SourceSelector({
|
||||
showDocSidebar ? "4xl:block" : "!block"
|
||||
} duration-1000 flex ease-out transition-all transform origin-top-right`}
|
||||
>
|
||||
<div className="mb-4 pb-2 flex border-b border-border text-emphasis">
|
||||
<button
|
||||
onClick={() => toggleFilters && toggleFilters()}
|
||||
className="flex text-emphasis"
|
||||
>
|
||||
<h2 className="font-bold my-auto">Filters</h2>
|
||||
<FiFilter className="my-auto ml-2" size="16" />
|
||||
</div>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="cursor-pointer">
|
||||
<SectionTitle>Time Range</SectionTitle>
|
||||
<p className="text-sm text-default mt-2">
|
||||
{timeRange?.from
|
||||
? getDateRangeString(timeRange.from, timeRange.to)
|
||||
: "Since"}
|
||||
</p>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="bg-background border-border border rounded-md z-[200] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="range"
|
||||
selected={
|
||||
timeRange
|
||||
? { from: new Date(timeRange.from), to: new Date(timeRange.to) }
|
||||
: undefined
|
||||
}
|
||||
onSelect={(daterange) => {
|
||||
const initialDate = daterange?.from || new Date();
|
||||
const endDate = daterange?.to || new Date();
|
||||
setTimeRange({
|
||||
from: initialDate,
|
||||
to: endDate,
|
||||
selectValue: timeRange?.selectValue || "",
|
||||
});
|
||||
}}
|
||||
className="rounded-md "
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{availableTags.length > 0 && (
|
||||
</button>
|
||||
{!filtersUntoggled && (
|
||||
<>
|
||||
<div className="mt-4 mb-2">
|
||||
<SectionTitle>Tags</SectionTitle>
|
||||
</div>
|
||||
<TagFilter
|
||||
tags={availableTags}
|
||||
selectedTags={selectedTags}
|
||||
setSelectedTags={setSelectedTags}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{existingSources.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<div className="flex w-full gap-x-2 items-center">
|
||||
<div className="font-bold text-xs mt-2 flex items-center gap-x-2">
|
||||
<p>Sources</p>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allSourcesSelected}
|
||||
onChange={toggleAllSources}
|
||||
className="my-auto form-checkbox h-3 w-3 text-primary border-background-900 rounded"
|
||||
<Separator />
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="cursor-pointer">
|
||||
<SectionTitle>Time Range</SectionTitle>
|
||||
<p className="text-sm text-default mt-2">
|
||||
{getDateRangeString(timeRange?.from!, timeRange?.to!) ||
|
||||
"Select a time range"}
|
||||
</p>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="bg-background-search-filter border-border border rounded-md z-[200] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="range"
|
||||
selected={
|
||||
timeRange
|
||||
? {
|
||||
from: new Date(timeRange.from),
|
||||
to: new Date(timeRange.to),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onSelect={(daterange) => {
|
||||
const initialDate = daterange?.from || new Date();
|
||||
const endDate = daterange?.to || new Date();
|
||||
setTimeRange({
|
||||
from: initialDate,
|
||||
to: endDate,
|
||||
selectValue: timeRange?.selectValue || "",
|
||||
});
|
||||
}}
|
||||
className="rounded-md "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-1">
|
||||
{listSourceMetadata()
|
||||
.filter((source) => existingSources.includes(source.internalName))
|
||||
.map((source) => (
|
||||
<div
|
||||
key={source.internalName}
|
||||
className={
|
||||
"flex cursor-pointer w-full items-center " +
|
||||
"py-1.5 my-1.5 rounded-lg px-2 select-none " +
|
||||
(selectedSources
|
||||
.map((source) => source.internalName)
|
||||
.includes(source.internalName)
|
||||
? "bg-hover"
|
||||
: "hover:bg-hover-light")
|
||||
}
|
||||
onClick={() => handleSelect(source)}
|
||||
>
|
||||
<SourceIcon sourceType={source.internalName} iconSize={16} />
|
||||
<span className="ml-2 text-sm text-default">
|
||||
{source.displayName}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{availableDocumentSets.length > 0 && (
|
||||
<>
|
||||
<div className="mt-4">
|
||||
<SectionTitle>Knowledge Sets</SectionTitle>
|
||||
</div>
|
||||
<div className="px-1">
|
||||
{availableDocumentSets.map((documentSet) => (
|
||||
<div key={documentSet.name} className="my-1.5 flex">
|
||||
<div
|
||||
key={documentSet.name}
|
||||
className={
|
||||
"flex cursor-pointer w-full items-center " +
|
||||
"py-1.5 rounded-lg px-2 " +
|
||||
(selectedDocumentSets.includes(documentSet.name)
|
||||
? "bg-hover"
|
||||
: "hover:bg-hover-light")
|
||||
}
|
||||
onClick={() => handleDocumentSetSelect(documentSet.name)}
|
||||
>
|
||||
<HoverPopup
|
||||
mainContent={
|
||||
<div className="flex my-auto mr-2">
|
||||
<InfoIcon className={defaultTailwindCSS} />
|
||||
</div>
|
||||
}
|
||||
popupContent={
|
||||
<div className="text-sm w-64">
|
||||
<div className="flex font-medium">Description</div>
|
||||
<div className="mt-1">{documentSet.description}</div>
|
||||
</div>
|
||||
}
|
||||
classNameModifications="-ml-2"
|
||||
{availableTags.length > 0 && (
|
||||
<>
|
||||
<div className="mt-4 mb-2">
|
||||
<SectionTitle>Tags</SectionTitle>
|
||||
</div>
|
||||
<TagFilter
|
||||
showTagsOnLeft={true}
|
||||
tags={availableTags}
|
||||
selectedTags={selectedTags}
|
||||
setSelectedTags={setSelectedTags}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{existingSources.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<div className="flex w-full gap-x-2 items-center">
|
||||
<div className="font-bold text-xs mt-2 flex items-center gap-x-2">
|
||||
<p>Sources</p>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allSourcesSelected}
|
||||
onChange={toggleAllSources}
|
||||
className="my-auto form-checkbox h-3 w-3 text-primary border-background-900 rounded"
|
||||
/>
|
||||
<span className="text-sm">{documentSet.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="px-1">
|
||||
{listSourceMetadata()
|
||||
.filter((source) =>
|
||||
existingSources.includes(source.internalName)
|
||||
)
|
||||
.map((source) => (
|
||||
<div
|
||||
key={source.internalName}
|
||||
className={
|
||||
"flex cursor-pointer w-full items-center " +
|
||||
"py-1.5 my-1.5 rounded-lg px-2 select-none " +
|
||||
(selectedSources
|
||||
.map((source) => source.internalName)
|
||||
.includes(source.internalName)
|
||||
? "bg-hover"
|
||||
: "hover:bg-hover-light")
|
||||
}
|
||||
onClick={() => handleSelect(source)}
|
||||
>
|
||||
<SourceIcon
|
||||
sourceType={source.internalName}
|
||||
iconSize={16}
|
||||
/>
|
||||
<span className="ml-2 text-sm text-default">
|
||||
{source.displayName}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{availableDocumentSets.length > 0 && (
|
||||
<>
|
||||
<div className="mt-4">
|
||||
<SectionTitle>Knowledge Sets</SectionTitle>
|
||||
</div>
|
||||
<div className="px-1">
|
||||
{availableDocumentSets.map((documentSet) => (
|
||||
<div key={documentSet.name} className="my-1.5 flex">
|
||||
<div
|
||||
key={documentSet.name}
|
||||
className={
|
||||
"flex cursor-pointer w-full items-center " +
|
||||
"py-1.5 rounded-lg px-2 " +
|
||||
(selectedDocumentSets.includes(documentSet.name)
|
||||
? "bg-hover"
|
||||
: "hover:bg-hover-light")
|
||||
}
|
||||
onClick={() => handleDocumentSetSelect(documentSet.name)}
|
||||
>
|
||||
<HoverPopup
|
||||
mainContent={
|
||||
<div className="flex my-auto mr-2">
|
||||
<InfoIcon className={defaultTailwindCSS} />
|
||||
</div>
|
||||
}
|
||||
popupContent={
|
||||
<div className="text-sm w-64">
|
||||
<div className="flex font-medium">Description</div>
|
||||
<div className="mt-1">
|
||||
{documentSet.description}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
classNameModifications="-ml-2"
|
||||
/>
|
||||
<span className="text-sm">{documentSet.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
@ -6,13 +6,17 @@ import debounce from "lodash/debounce";
|
||||
import { getValidTags } from "@/lib/tags/tagUtils";
|
||||
|
||||
export function TagFilter({
|
||||
modal,
|
||||
tags,
|
||||
selectedTags,
|
||||
setSelectedTags,
|
||||
showTagsOnLeft = false,
|
||||
}: {
|
||||
modal?: boolean;
|
||||
tags: Tag[];
|
||||
selectedTags: Tag[];
|
||||
setSelectedTags: React.Dispatch<React.SetStateAction<Tag[]>>;
|
||||
showTagsOnLeft?: boolean;
|
||||
}) {
|
||||
const [filterValue, setFilterValue] = useState("");
|
||||
const [tagOptionsAreVisible, setTagOptionsAreVisible] = useState(false);
|
||||
@ -72,10 +76,12 @@ export function TagFilter({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative w-full ">
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="w-full border border-border py-0.5 px-2 rounded text-sm h-8"
|
||||
className={` border border-border py-0.5 px-2 rounded text-sm h-8 ${
|
||||
modal ? "w-[80vw]" : "w-full"
|
||||
}`}
|
||||
placeholder="Find a tag"
|
||||
value={filterValue}
|
||||
onChange={handleFilterChange}
|
||||
@ -106,7 +112,13 @@ export function TagFilter({
|
||||
</div>
|
||||
)}
|
||||
{tagOptionsAreVisible && (
|
||||
<div className="absolute top-0 right-0 transform translate-x-[105%] z-40">
|
||||
<div
|
||||
className={` absolute z-[100] ${
|
||||
showTagsOnLeft
|
||||
? "left-0 top-0 translate-y-[2rem]"
|
||||
: "right-0 translate-x-[105%] top-0"
|
||||
} z-40`}
|
||||
>
|
||||
<div
|
||||
ref={popupRef}
|
||||
className="p-2 border border-border rounded shadow-lg w-72 bg-background"
|
||||
|
@ -1,55 +1,71 @@
|
||||
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
|
||||
import { ReactNode } from "react";
|
||||
import { CompactDocumentCard } from "../DocumentDisplay";
|
||||
import { LoadedDanswerDocument } from "@/lib/search/interfaces";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
// NOTE: This is the preivous version of the citations which works just fine
|
||||
export function Citation({
|
||||
children,
|
||||
link,
|
||||
document,
|
||||
index,
|
||||
icon,
|
||||
url,
|
||||
}: {
|
||||
link?: string;
|
||||
children?: JSX.Element | string | null | ReactNode;
|
||||
index?: number;
|
||||
document: LoadedDanswerDocument;
|
||||
icon?: React.ReactNode;
|
||||
url?: string;
|
||||
}) {
|
||||
const innerText = children
|
||||
? children?.toString().split("[")[1].split("]")[0]
|
||||
: index;
|
||||
|
||||
if (link != "") {
|
||||
if (link) {
|
||||
return (
|
||||
<CustomTooltip
|
||||
citation
|
||||
content={<div className="inline-block p-0 m-0 truncate">{link}</div>}
|
||||
>
|
||||
<a
|
||||
onMouseDown={() => (link ? window.open(link, "_blank") : undefined)}
|
||||
className="cursor-pointer inline ml-1 align-middle"
|
||||
>
|
||||
<span className="group relative -top-1 text-sm text-gray-500 dark:text-gray-400 selection:bg-indigo-300 selection:text-black dark:selection:bg-indigo-900 dark:selection:text-white">
|
||||
<span
|
||||
className="inline-flex bg-background-200 group-hover:bg-background-300 items-center justify-center h-3.5 min-w-3.5 px-1 text-center text-xs rounded-full border-1 border-gray-400 ring-1 ring-gray-400 divide-gray-300 dark:divide-gray-700 dark:ring-gray-700 dark:border-gray-700 transition duration-150"
|
||||
data-number="3"
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
onMouseDown={() => window.open(link, "_blank")}
|
||||
className="inline-flex items-center ml-1 cursor-pointer transition-all duration-200 ease-in-out"
|
||||
>
|
||||
{innerText}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</CustomTooltip>
|
||||
<span className="relative min-w-[1.4rem] text-center no-underline -top-0.5 px-1.5 py-0.5 text-xs font-medium text-gray-700 bg-gray-100 rounded-full border border-gray-300 hover:bg-gray-200 hover:text-gray-900 shadow-sm no-underline">
|
||||
{innerText}
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent width="mb-2 max-w-lg" className="bg-background">
|
||||
<CompactDocumentCard url={url} icon={icon} document={document} />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<CustomTooltip content={<div>This doc doesn't have a link!</div>}>
|
||||
<div className="inline-block cursor-help leading-none inline ml-1 align-middle">
|
||||
<span className="group relative -top-1 text-gray-500 dark:text-gray-400 selection:bg-indigo-300 selection:text-black dark:selection:bg-indigo-900 dark:selection:text-white">
|
||||
<span
|
||||
className="inline-flex bg-background-200 group-hover:bg-background-300 items-center justify-center h-3.5 min-w-3.5 flex-none px-1 text-center text-xs rounded-full border-1 border-gray-400 ring-1 ring-gray-400 divide-gray-300 dark:divide-gray-700 dark:ring-gray-700 dark:border-gray-700 transition duration-150"
|
||||
data-number="3"
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
onMouseDown={() => window.open(link, "_blank")}
|
||||
className="inline-flex items-center ml-1 cursor-pointer transition-all duration-200 ease-in-out"
|
||||
>
|
||||
{innerText}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</CustomTooltip>
|
||||
<span className="relative min-w-[1.4rem] pchatno-underline -top-0.5 px-1.5 py-0.5 text-xs font-medium text-gray-700 bg-gray-100 rounded-full border border-gray-300 hover:bg-gray-200 hover:text-gray-900 shadow-sm no-underline">
|
||||
{innerText}
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent width="mb-2 max-w-lg" backgroundColor="bg-background">
|
||||
<CompactDocumentCard url={url} icon={icon} document={document} />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,6 @@ import {
|
||||
TriangleAlertIcon,
|
||||
} from "@/components/icons/icons";
|
||||
import { useState } from "react";
|
||||
import { Grid } from "react-loader-spinner";
|
||||
import { searchState } from "../SearchSection";
|
||||
|
||||
export type StatusOptions = "in-progress" | "failed" | "warning" | "success";
|
||||
|
||||
|
@ -43,11 +43,9 @@ export async function fetchSettingsSS(): Promise<CombinedSettings | null> {
|
||||
if (!results[0].ok) {
|
||||
if (results[0].status === 403 || results[0].status === 401) {
|
||||
settings = {
|
||||
auto_scroll: true,
|
||||
product_gating: GatingType.NONE,
|
||||
gpu_enabled: false,
|
||||
chat_page_enabled: true,
|
||||
search_page_enabled: true,
|
||||
default_page: "search",
|
||||
maximum_chat_retention_days: null,
|
||||
notifications: [],
|
||||
needs_reindexing: false,
|
||||
|
@ -131,7 +131,7 @@ export const CustomTooltip = ({
|
||||
transform -translate-x-1/2 text-sm
|
||||
${
|
||||
light
|
||||
? "text-gray-800 bg-background-200"
|
||||
? "text-text-800 bg-background-200"
|
||||
: "text-white bg-background-800"
|
||||
}
|
||||
rounded-lg shadow-lg`}
|
||||
|
@ -50,12 +50,13 @@ function Badge({
|
||||
...props
|
||||
}: BadgeProps & {
|
||||
icon?: React.ElementType;
|
||||
size?: "sm" | "md";
|
||||
size?: "sm" | "md" | "xs";
|
||||
circle?: boolean;
|
||||
}) {
|
||||
const sizeClasses = {
|
||||
sm: "px-2.5 py-0.5 text-xs",
|
||||
md: "px-3 py-1 text-sm",
|
||||
xs: "px-1.5 py-0.25 text-[.5rem]", // Made xs smaller
|
||||
};
|
||||
|
||||
return (
|
||||
@ -64,10 +65,20 @@ function Badge({
|
||||
{...props}
|
||||
>
|
||||
{Icon && (
|
||||
<Icon className={cn("mr-1", size === "sm" ? "h-3 w-3" : "h-4 w-4")} />
|
||||
<Icon
|
||||
className={cn(
|
||||
"mr-1",
|
||||
size === "sm" ? "h-3 w-3" : size === "xs" ? "h-2 w-2" : "h-4 w-4"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{circle && (
|
||||
<div className="h-2.5 w-2.5 mr-2 rounded-full bg-current opacity-80" />
|
||||
<div
|
||||
className={cn(
|
||||
"mr-2 rounded-full bg-current opacity-80",
|
||||
size === "xs" ? "h-2 w-2" : "h-2.5 w-2.5"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{props.children}
|
||||
</div>
|
||||
|
30
web/src/components/ui/checkbox.tsx
Normal file
30
web/src/components/ui/checkbox.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-neutral-200 border-neutral-900 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50 dark:border-neutral-800 dark:border-neutral-50 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=checked]:text-neutral-900",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox };
|
118
web/src/components/ui/drawer.tsx
Normal file
118
web/src/components/ui/drawer.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Drawer = ({
|
||||
shouldScaleBackground = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
||||
<DrawerPrimitive.Root
|
||||
shouldScaleBackground={shouldScaleBackground}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
Drawer.displayName = "Drawer";
|
||||
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger;
|
||||
|
||||
const DrawerPortal = DrawerPrimitive.Portal;
|
||||
|
||||
const DrawerClose = DrawerPrimitive.Close;
|
||||
|
||||
const DrawerOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
|
||||
|
||||
const DrawerContent = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-950",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-neutral-100 dark:bg-neutral-800" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
));
|
||||
DrawerContent.displayName = "DrawerContent";
|
||||
|
||||
const DrawerHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DrawerHeader.displayName = "DrawerHeader";
|
||||
|
||||
const DrawerFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DrawerFooter.displayName = "DrawerFooter";
|
||||
|
||||
const DrawerTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
|
||||
|
||||
const DrawerDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-neutral-500 dark:text-neutral-400", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
};
|
29
web/src/components/ui/switch.tsx
Normal file
29
web/src/components/ui/switch.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200 dark:focus-visible:ring-neutral-300 dark:focus-visible:ring-offset-neutral-950 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=unchecked]:bg-neutral-800",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 dark:bg-neutral-950"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { Switch };
|
@ -13,17 +13,19 @@ const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & {
|
||||
maxWidth?: string;
|
||||
width?: string;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
>(({ className, sideOffset = 4, maxWidth, backgroundColor, ...props }, ref) => (
|
||||
>(({ className, sideOffset = 4, width, backgroundColor, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
`z-50 overflow-hidden rounded-md border border-neutral-200 text-white ${
|
||||
backgroundColor || "bg-background-900"
|
||||
} px-2 py-1.5 text-sm shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50 max-w-sm`,
|
||||
}
|
||||
${width || "max-w-sm"}
|
||||
px-2 py-1.5 text-sm shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50 `,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
@ -12,6 +12,7 @@ interface UserContextType {
|
||||
isCurator: boolean;
|
||||
refreshUser: () => Promise<void>;
|
||||
isCloudSuperuser: boolean;
|
||||
updateUserAutoScroll: (autoScroll: boolean | null) => Promise<void>;
|
||||
}
|
||||
|
||||
const UserContext = createContext<UserContextType | undefined>(undefined);
|
||||
@ -55,6 +56,36 @@ export function UserProvider({
|
||||
setIsLoadingUser(false);
|
||||
}
|
||||
};
|
||||
const updateUserAutoScroll = async (autoScroll: boolean | null) => {
|
||||
try {
|
||||
const response = await fetch("/api/auto-scroll", {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ auto_scroll: autoScroll }),
|
||||
});
|
||||
setUpToDateUser((prevUser) => {
|
||||
if (prevUser) {
|
||||
return {
|
||||
...prevUser,
|
||||
preferences: {
|
||||
...prevUser.preferences,
|
||||
auto_scroll: autoScroll,
|
||||
},
|
||||
};
|
||||
}
|
||||
return prevUser;
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to update auto-scroll setting");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating auto-scroll setting:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshUser = async () => {
|
||||
await fetchUser();
|
||||
@ -66,6 +97,7 @@ export function UserProvider({
|
||||
user: upToDateUser,
|
||||
isLoadingUser,
|
||||
refreshUser,
|
||||
updateUserAutoScroll,
|
||||
isAdmin: upToDateUser?.role === UserRole.ADMIN,
|
||||
// Curator status applies for either global or basic curator
|
||||
isCurator:
|
||||
|
@ -19,6 +19,10 @@ export interface AnswerPiecePacket {
|
||||
answer_piece: string;
|
||||
}
|
||||
|
||||
export interface FinalContextDocs {
|
||||
final_context_docs: DanswerDocument[];
|
||||
}
|
||||
|
||||
export enum StreamStopReason {
|
||||
CONTEXT_LENGTH = "CONTEXT_LENGTH",
|
||||
CANCELLED = "CANCELLED",
|
||||
@ -62,6 +66,9 @@ export interface DanswerDocument {
|
||||
is_internet: boolean;
|
||||
validationState?: null | "good" | "bad";
|
||||
}
|
||||
export interface LoadedDanswerDocument extends DanswerDocument {
|
||||
icon: React.FC<{ size?: number; className?: string }>;
|
||||
}
|
||||
|
||||
export interface SearchDanswerDocument extends DanswerDocument {
|
||||
is_relevant: boolean;
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
DanswerDocument,
|
||||
DocumentInfoPacket,
|
||||
ErrorMessagePacket,
|
||||
FinalContextDocs,
|
||||
Quote,
|
||||
QuotesInfoPacket,
|
||||
RelevanceChunk,
|
||||
@ -91,6 +92,7 @@ export const searchRequestStreamed = async ({
|
||||
| DocumentInfoPacket
|
||||
| LLMRelevanceFilterPacket
|
||||
| BackendMessage
|
||||
| FinalContextDocs
|
||||
| RelevanceChunk
|
||||
>(decoder.decode(value, { stream: true }), previousPartialChunk);
|
||||
if (!completedChunks.length && !partialChunk) {
|
||||
|
@ -58,7 +58,7 @@ type SourceMap = {
|
||||
[K in ValidSources]: PartialSourceMetadata;
|
||||
};
|
||||
|
||||
const SOURCE_METADATA_MAP: SourceMap = {
|
||||
export const SOURCE_METADATA_MAP: SourceMap = {
|
||||
web: {
|
||||
icon: GlobeIcon,
|
||||
displayName: "Web",
|
||||
|
@ -9,6 +9,7 @@ interface UserPreferences {
|
||||
hidden_assistants: number[];
|
||||
default_model: string | null;
|
||||
recent_assistants: number[];
|
||||
auto_scroll: boolean | null;
|
||||
}
|
||||
|
||||
export enum UserStatus {
|
||||
|
@ -336,6 +336,7 @@ module.exports = {
|
||||
"tremor-full": "9999px",
|
||||
},
|
||||
fontSize: {
|
||||
"2xs": "0.625rem",
|
||||
"code-sm": "small",
|
||||
"tremor-label": ["0.75rem"],
|
||||
"tremor-default": ["0.875rem", { lineHeight: "1.25rem" }],
|
||||
|
Loading…
x
Reference in New Issue
Block a user