mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-06-19 04:20:57 +02: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)
|
return UserPreferences(**preferences_data)
|
||||||
except KvKeyNotFoundError:
|
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:
|
def fetch_no_auth_user(store: KeyValueStore) -> UserInfo:
|
||||||
|
@ -605,6 +605,7 @@ def stream_chat_message_objects(
|
|||||||
additional_headers=custom_tool_additional_headers,
|
additional_headers=custom_tool_additional_headers,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
tools: list[Tool] = []
|
tools: list[Tool] = []
|
||||||
for tool_list in tool_dict.values():
|
for tool_list in tool_dict.values():
|
||||||
tools.extend(tool_list)
|
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 specified, controls the assistants that are shown to the user + their order
|
||||||
# if not specified, all assistants are shown
|
# if not specified, all assistants are shown
|
||||||
|
auto_scroll: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
chosen_assistants: Mapped[list[int] | None] = mapped_column(
|
chosen_assistants: Mapped[list[int] | None] = mapped_column(
|
||||||
postgresql.JSONB(), nullable=True, default=None
|
postgresql.JSONB(), nullable=True, default=None
|
||||||
)
|
)
|
||||||
|
@ -5,7 +5,7 @@ personas:
|
|||||||
# this is for DanswerBot to use when tagged in a non-configured channel
|
# 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
|
# Careful setting specific IDs, this won't autoincrement the next ID value for postgres
|
||||||
- id: 0
|
- id: 0
|
||||||
name: "Knowledge"
|
name: "Search"
|
||||||
description: >
|
description: >
|
||||||
Assistant with access to documents from your Connected Sources.
|
Assistant with access to documents from your Connected Sources.
|
||||||
# Default Prompt objects attached to the persona, see prompts.yaml
|
# Default Prompt objects attached to the persona, see prompts.yaml
|
||||||
|
@ -45,6 +45,7 @@ class UserPreferences(BaseModel):
|
|||||||
visible_assistants: list[int] = []
|
visible_assistants: list[int] = []
|
||||||
recent_assistants: list[int] | None = None
|
recent_assistants: list[int] | None = None
|
||||||
default_model: str | None = None
|
default_model: str | None = None
|
||||||
|
auto_scroll: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
class UserInfo(BaseModel):
|
class UserInfo(BaseModel):
|
||||||
@ -79,6 +80,7 @@ class UserInfo(BaseModel):
|
|||||||
role=user.role,
|
role=user.role,
|
||||||
preferences=(
|
preferences=(
|
||||||
UserPreferences(
|
UserPreferences(
|
||||||
|
auto_scroll=user.auto_scroll,
|
||||||
chosen_assistants=user.chosen_assistants,
|
chosen_assistants=user.chosen_assistants,
|
||||||
default_model=user.default_model,
|
default_model=user.default_model,
|
||||||
hidden_assistants=user.hidden_assistants,
|
hidden_assistants=user.hidden_assistants,
|
||||||
@ -128,6 +130,10 @@ class HiddenUpdateRequest(BaseModel):
|
|||||||
hidden: bool
|
hidden: bool
|
||||||
|
|
||||||
|
|
||||||
|
class AutoScrollRequest(BaseModel):
|
||||||
|
auto_scroll: bool | None
|
||||||
|
|
||||||
|
|
||||||
class SlackBotCreationRequest(BaseModel):
|
class SlackBotCreationRequest(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
enabled: bool
|
enabled: bool
|
||||||
|
@ -52,6 +52,7 @@ from danswer.db.users import list_users
|
|||||||
from danswer.db.users import validate_user_role_update
|
from danswer.db.users import validate_user_role_update
|
||||||
from danswer.key_value_store.factory import get_kv_store
|
from danswer.key_value_store.factory import get_kv_store
|
||||||
from danswer.server.manage.models import AllUsersResponse
|
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 UserByEmail
|
||||||
from danswer.server.manage.models import UserInfo
|
from danswer.server.manage.models import UserInfo
|
||||||
from danswer.server.manage.models import UserPreferences
|
from danswer.server.manage.models import UserPreferences
|
||||||
@ -497,7 +498,6 @@ def verify_user_logged_in(
|
|||||||
return fetch_no_auth_user(store)
|
return fetch_no_auth_user(store)
|
||||||
|
|
||||||
raise BasicAuthenticationError(detail="User Not Authenticated")
|
raise BasicAuthenticationError(detail="User Not Authenticated")
|
||||||
|
|
||||||
if user.oidc_expiry and user.oidc_expiry < datetime.now(timezone.utc):
|
if user.oidc_expiry and user.oidc_expiry < datetime.now(timezone.utc):
|
||||||
raise BasicAuthenticationError(
|
raise BasicAuthenticationError(
|
||||||
detail="Access denied. User's OIDC token has expired.",
|
detail="Access denied. User's OIDC token has expired.",
|
||||||
@ -581,6 +581,30 @@ def update_user_recent_assistants(
|
|||||||
db_session.commit()
|
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")
|
@router.patch("/user/default-model")
|
||||||
def update_user_default_model(
|
def update_user_default_model(
|
||||||
request: ChosenDefaultModelRequest,
|
request: ChosenDefaultModelRequest,
|
||||||
|
@ -79,6 +79,7 @@ class CreateChatMessageRequest(ChunkContext):
|
|||||||
message: str
|
message: str
|
||||||
# Files that we should attach to this message
|
# Files that we should attach to this message
|
||||||
file_descriptors: list[FileDescriptor]
|
file_descriptors: list[FileDescriptor]
|
||||||
|
|
||||||
# If no prompt provided, uses the largest prompt of the chat session
|
# 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
|
# 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
|
# 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 APIRouter
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from fastapi import HTTPException
|
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@ -38,10 +37,6 @@ basic_router = APIRouter(prefix="/settings")
|
|||||||
def put_settings(
|
def put_settings(
|
||||||
settings: Settings, _: User | None = Depends(current_admin_user)
|
settings: Settings, _: User | None = Depends(current_admin_user)
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
|
||||||
settings.check_validity()
|
|
||||||
except ValueError as e:
|
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
|
||||||
store_settings(settings)
|
store_settings(settings)
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,33 +41,10 @@ class Notification(BaseModel):
|
|||||||
class Settings(BaseModel):
|
class Settings(BaseModel):
|
||||||
"""General settings"""
|
"""General settings"""
|
||||||
|
|
||||||
chat_page_enabled: bool = True
|
|
||||||
search_page_enabled: bool = True
|
|
||||||
default_page: PageType = PageType.SEARCH
|
|
||||||
maximum_chat_retention_days: int | None = None
|
maximum_chat_retention_days: int | None = None
|
||||||
gpu_enabled: bool | None = None
|
gpu_enabled: bool | None = None
|
||||||
product_gating: GatingType = GatingType.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):
|
class UserSettings(Settings):
|
||||||
notifications: list[Notification]
|
notifications: list[Notification]
|
||||||
|
@ -113,10 +113,6 @@ async def refresh_access_token(
|
|||||||
def put_settings(
|
def put_settings(
|
||||||
settings: EnterpriseSettings, _: User | None = Depends(current_admin_user)
|
settings: EnterpriseSettings, _: User | None = Depends(current_admin_user)
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
|
||||||
settings.check_validity()
|
|
||||||
except ValueError as e:
|
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
|
||||||
store_settings(settings)
|
store_settings(settings)
|
||||||
|
|
||||||
|
|
||||||
|
@ -157,7 +157,6 @@ def _seed_personas(db_session: Session, personas: list[CreatePersonaRequest]) ->
|
|||||||
def _seed_settings(settings: Settings) -> None:
|
def _seed_settings(settings: Settings) -> None:
|
||||||
logger.notice("Seeding Settings")
|
logger.notice("Seeding Settings")
|
||||||
try:
|
try:
|
||||||
settings.check_validity()
|
|
||||||
store_base_settings(settings)
|
store_base_settings(settings)
|
||||||
logger.notice("Successfully seeded Settings")
|
logger.notice("Successfully seeded Settings")
|
||||||
except ValueError as e:
|
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/react": "^2.2.0",
|
||||||
"@headlessui/tailwindcss": "^0.2.1",
|
"@headlessui/tailwindcss": "^0.2.1",
|
||||||
"@phosphor-icons/react": "^2.0.8",
|
"@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-popover": "^1.1.2",
|
||||||
"@radix-ui/react-select": "^2.1.2",
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^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-tabs": "^1.1.1",
|
||||||
"@radix-ui/react-tooltip": "^1.1.3",
|
"@radix-ui/react-tooltip": "^1.1.3",
|
||||||
"@sentry/nextjs": "^8.34.0",
|
"@sentry/nextjs": "^8.34.0",
|
||||||
@ -37,6 +39,7 @@
|
|||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"favicon-fetch": "^1.0.0",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@ -67,6 +70,7 @@
|
|||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "5.0.3",
|
"typescript": "5.0.3",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
|
"vaul": "^1.1.1",
|
||||||
"yup": "^1.4.0"
|
"yup": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -83,7 +83,7 @@ const EditRow = ({
|
|||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{!documentSet.is_up_to_date && (
|
{!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">
|
<div className="flex break-words break-keep whitespace-pre-wrap items-start">
|
||||||
<InfoIcon className="mr-2 mt-0.5" />
|
<InfoIcon className="mr-2 mt-0.5" />
|
||||||
Cannot update while syncing! Wait for the sync to finish, then
|
Cannot update while syncing! Wait for the sync to finish, then
|
||||||
|
@ -175,29 +175,6 @@ export function SettingsForm() {
|
|||||||
{ fieldName, newValue: checked },
|
{ 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);
|
updateSettingField(updates);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,42 +195,17 @@ export function SettingsForm() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{popup}
|
{popup}
|
||||||
<Title className="mb-4">Page Visibility</Title>
|
<Title className="mb-4">Workspace Settings</Title>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Search Page Enabled?"
|
label="Auto-scroll"
|
||||||
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."
|
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.search_page_enabled}
|
checked={settings.auto_scroll}
|
||||||
onChange={(e) =>
|
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 && (
|
{isEnterpriseEnabled && (
|
||||||
<>
|
<>
|
||||||
<Title className="mb-4">Chat Settings</Title>
|
<Title className="mb-4">Chat Settings</Title>
|
||||||
|
@ -5,14 +5,12 @@ export enum GatingType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
chat_page_enabled: boolean;
|
|
||||||
search_page_enabled: boolean;
|
|
||||||
default_page: "search" | "chat";
|
|
||||||
maximum_chat_retention_days: number | null;
|
maximum_chat_retention_days: number | null;
|
||||||
notifications: Notification[];
|
notifications: Notification[];
|
||||||
needs_reindexing: boolean;
|
needs_reindexing: boolean;
|
||||||
gpu_enabled: boolean;
|
gpu_enabled: boolean;
|
||||||
product_gating: GatingType;
|
product_gating: GatingType;
|
||||||
|
auto_scroll: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NotificationType {
|
export enum NotificationType {
|
||||||
@ -54,6 +52,7 @@ export interface EnterpriseSettings {
|
|||||||
custom_popup_header: string | null;
|
custom_popup_header: string | null;
|
||||||
custom_popup_content: string | null;
|
custom_popup_content: string | null;
|
||||||
enable_consent_screen: boolean | null;
|
enable_consent_screen: boolean | null;
|
||||||
|
auto_scroll: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CombinedSettings {
|
export interface CombinedSettings {
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
ChatFileType,
|
ChatFileType,
|
||||||
ChatSession,
|
ChatSession,
|
||||||
ChatSessionSharedStatus,
|
ChatSessionSharedStatus,
|
||||||
DocumentsResponse,
|
|
||||||
FileDescriptor,
|
FileDescriptor,
|
||||||
FileChatDisplay,
|
FileChatDisplay,
|
||||||
Message,
|
Message,
|
||||||
@ -60,7 +59,7 @@ import { useDocumentSelection } from "./useDocumentSelection";
|
|||||||
import { LlmOverride, useFilters, useLlmOverride } from "@/lib/hooks";
|
import { LlmOverride, useFilters, useLlmOverride } from "@/lib/hooks";
|
||||||
import { computeAvailableFilters } from "@/lib/filters";
|
import { computeAvailableFilters } from "@/lib/filters";
|
||||||
import { ChatState, FeedbackType, RegenerationState } from "./types";
|
import { ChatState, FeedbackType, RegenerationState } from "./types";
|
||||||
import { DocumentSidebar } from "./documentSidebar/DocumentSidebar";
|
import { ChatFilters } from "./documentSidebar/ChatFilters";
|
||||||
import { DanswerInitializingLoader } from "@/components/DanswerInitializingLoader";
|
import { DanswerInitializingLoader } from "@/components/DanswerInitializingLoader";
|
||||||
import { FeedbackModal } from "./modal/FeedbackModal";
|
import { FeedbackModal } from "./modal/FeedbackModal";
|
||||||
import { ShareChatSessionModal } from "./modal/ShareChatSessionModal";
|
import { ShareChatSessionModal } from "./modal/ShareChatSessionModal";
|
||||||
@ -71,6 +70,7 @@ import { StarterMessages } from "../../components/assistants/StarterMessage";
|
|||||||
import {
|
import {
|
||||||
AnswerPiecePacket,
|
AnswerPiecePacket,
|
||||||
DanswerDocument,
|
DanswerDocument,
|
||||||
|
FinalContextDocs,
|
||||||
StreamStopInfo,
|
StreamStopInfo,
|
||||||
StreamStopReason,
|
StreamStopReason,
|
||||||
} from "@/lib/search/interfaces";
|
} from "@/lib/search/interfaces";
|
||||||
@ -105,14 +105,9 @@ import BlurBackground from "./shared_chat_search/BlurBackground";
|
|||||||
import { NoAssistantModal } from "@/components/modals/NoAssistantModal";
|
import { NoAssistantModal } from "@/components/modals/NoAssistantModal";
|
||||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||||
import { Separator } from "@/components/ui/separator";
|
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 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_USER_MESSAGE_ID = -1;
|
||||||
const TEMP_ASSISTANT_MESSAGE_ID = -2;
|
const TEMP_ASSISTANT_MESSAGE_ID = -2;
|
||||||
@ -132,8 +127,9 @@ export function ChatPage({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
chatSessions,
|
chatSessions,
|
||||||
availableSources,
|
ccPairs,
|
||||||
availableDocumentSets,
|
tags,
|
||||||
|
documentSets,
|
||||||
llmProviders,
|
llmProviders,
|
||||||
folders,
|
folders,
|
||||||
openedFolders,
|
openedFolders,
|
||||||
@ -142,6 +138,36 @@ export function ChatPage({
|
|||||||
shouldShowWelcomeModal,
|
shouldShowWelcomeModal,
|
||||||
refreshChatSessions,
|
refreshChatSessions,
|
||||||
} = useChatContext();
|
} = 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
|
// handle redirect if chat page is disabled
|
||||||
// NOTE: this must be done here, in a client component since
|
// NOTE: this must be done here, in a client component since
|
||||||
@ -149,9 +175,11 @@ export function ChatPage({
|
|||||||
// available in server-side components
|
// available in server-side components
|
||||||
const settings = useContext(SettingsContext);
|
const settings = useContext(SettingsContext);
|
||||||
const enterpriseSettings = settings?.enterpriseSettings;
|
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();
|
const { assistants: availableAssistants, finalAssistants } = useAssistants();
|
||||||
|
|
||||||
@ -159,16 +187,13 @@ export function ChatPage({
|
|||||||
!shouldShowWelcomeModal
|
!shouldShowWelcomeModal
|
||||||
);
|
);
|
||||||
|
|
||||||
const { user, isAdmin, isLoadingUser, refreshUser } = useUser();
|
const { user, isAdmin, isLoadingUser } = useUser();
|
||||||
|
|
||||||
const slackChatId = searchParams.get("slackChatId");
|
const slackChatId = searchParams.get("slackChatId");
|
||||||
|
|
||||||
const existingChatIdRaw = searchParams.get("chatId");
|
const existingChatIdRaw = searchParams.get("chatId");
|
||||||
const [sendOnLoad, setSendOnLoad] = useState<string | null>(
|
const [sendOnLoad, setSendOnLoad] = useState<string | null>(
|
||||||
searchParams.get(SEARCH_PARAM_NAMES.SEND_ON_LOAD)
|
searchParams.get(SEARCH_PARAM_NAMES.SEND_ON_LOAD)
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentPersonaId = searchParams.get(SEARCH_PARAM_NAMES.PERSONA_ID);
|
|
||||||
const modelVersionFromSearchParams = searchParams.get(
|
const modelVersionFromSearchParams = searchParams.get(
|
||||||
SEARCH_PARAM_NAMES.STRUCTURED_MODEL
|
SEARCH_PARAM_NAMES.STRUCTURED_MODEL
|
||||||
);
|
);
|
||||||
@ -261,7 +286,7 @@ export function ChatPage({
|
|||||||
refreshRecentAssistants,
|
refreshRecentAssistants,
|
||||||
} = useAssistants();
|
} = useAssistants();
|
||||||
|
|
||||||
const liveAssistant =
|
const liveAssistant: Persona | undefined =
|
||||||
alternativeAssistant ||
|
alternativeAssistant ||
|
||||||
selectedAssistant ||
|
selectedAssistant ||
|
||||||
recentAssistants[0] ||
|
recentAssistants[0] ||
|
||||||
@ -269,8 +294,20 @@ export function ChatPage({
|
|||||||
availableAssistants[0];
|
availableAssistants[0];
|
||||||
|
|
||||||
const noAssistants = liveAssistant == null || liveAssistant == undefined;
|
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
|
// always set the model override for the chat session, when an assistant, llm provider, or user preference exists
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (noAssistants) return;
|
||||||
const personaDefault = getLLMProviderOverrideForPersona(
|
const personaDefault = getLLMProviderOverrideForPersona(
|
||||||
liveAssistant,
|
liveAssistant,
|
||||||
llmProviders
|
llmProviders
|
||||||
@ -357,9 +394,7 @@ export function ChatPage({
|
|||||||
textAreaRef.current?.focus();
|
textAreaRef.current?.focus();
|
||||||
|
|
||||||
// only clear things if we're going from one chat session to another
|
// only clear things if we're going from one chat session to another
|
||||||
const isChatSessionSwitch =
|
const isChatSessionSwitch = existingChatSessionId !== priorChatSessionId;
|
||||||
chatSessionIdRef.current !== null &&
|
|
||||||
existingChatSessionId !== priorChatSessionId;
|
|
||||||
if (isChatSessionSwitch) {
|
if (isChatSessionSwitch) {
|
||||||
// de-select documents
|
// de-select documents
|
||||||
clearSelectedDocuments();
|
clearSelectedDocuments();
|
||||||
@ -449,9 +484,9 @@ export function ChatPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shouldScrollToBottom) {
|
if (shouldScrollToBottom) {
|
||||||
if (!hasPerformedInitialScroll) {
|
if (!hasPerformedInitialScroll && autoScrollEnabled) {
|
||||||
clientScrollToBottom();
|
clientScrollToBottom();
|
||||||
} else if (isChatSessionSwitch) {
|
} else if (isChatSessionSwitch && autoScrollEnabled) {
|
||||||
clientScrollToBottom(true);
|
clientScrollToBottom(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -759,7 +794,7 @@ export function ChatPage({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchMaxTokens() {
|
async function fetchMaxTokens() {
|
||||||
const response = await fetch(
|
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) {
|
if (response.ok) {
|
||||||
const maxTokens = (await response.json()).max_tokens as number;
|
const maxTokens = (await response.json()).max_tokens as number;
|
||||||
@ -833,11 +868,13 @@ export function ChatPage({
|
|||||||
0
|
0
|
||||||
)}px`;
|
)}px`;
|
||||||
|
|
||||||
scrollableDivRef?.current.scrollBy({
|
if (autoScrollEnabled) {
|
||||||
left: 0,
|
scrollableDivRef?.current.scrollBy({
|
||||||
top: Math.max(heightDifference, 0),
|
left: 0,
|
||||||
behavior: "smooth",
|
top: Math.max(heightDifference, 0),
|
||||||
});
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
previousHeight.current = newHeight;
|
previousHeight.current = newHeight;
|
||||||
}
|
}
|
||||||
@ -884,6 +921,7 @@ export function ChatPage({
|
|||||||
endDivRef.current.scrollIntoView({
|
endDivRef.current.scrollIntoView({
|
||||||
behavior: fast ? "auto" : "smooth",
|
behavior: fast ? "auto" : "smooth",
|
||||||
});
|
});
|
||||||
|
|
||||||
setHasPerformedInitialScroll(true);
|
setHasPerformedInitialScroll(true);
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
@ -1035,7 +1073,9 @@ export function ChatPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setAlternativeGeneratingAssistant(alternativeAssistantOverride);
|
setAlternativeGeneratingAssistant(alternativeAssistantOverride);
|
||||||
|
|
||||||
clientScrollToBottom();
|
clientScrollToBottom();
|
||||||
|
|
||||||
let currChatSessionId: string;
|
let currChatSessionId: string;
|
||||||
const isNewSession = chatSessionIdRef.current === null;
|
const isNewSession = chatSessionIdRef.current === null;
|
||||||
const searchParamBasedChatSessionName =
|
const searchParamBasedChatSessionName =
|
||||||
@ -1281,8 +1321,8 @@ export function ChatPage({
|
|||||||
|
|
||||||
if (Object.hasOwn(packet, "answer_piece")) {
|
if (Object.hasOwn(packet, "answer_piece")) {
|
||||||
answer += (packet as AnswerPiecePacket).answer_piece;
|
answer += (packet as AnswerPiecePacket).answer_piece;
|
||||||
} else if (Object.hasOwn(packet, "top_documents")) {
|
} else if (Object.hasOwn(packet, "final_context_docs")) {
|
||||||
documents = (packet as DocumentsResponse).top_documents;
|
documents = (packet as FinalContextDocs).final_context_docs;
|
||||||
retrievalType = RetrievalType.Search;
|
retrievalType = RetrievalType.Search;
|
||||||
if (documents && documents.length > 0) {
|
if (documents && documents.length > 0) {
|
||||||
// point to the latest message (we don't know the messageId yet, which is why
|
// 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",
|
type: error ? "error" : "assistant",
|
||||||
retrievalType,
|
retrievalType,
|
||||||
query: finalMessage?.rephrased_query || query,
|
query: finalMessage?.rephrased_query || query,
|
||||||
documents:
|
documents: documents,
|
||||||
finalMessage?.context_docs?.top_documents || documents,
|
|
||||||
citations: finalMessage?.citations || {},
|
citations: finalMessage?.citations || {},
|
||||||
files: finalMessage?.files || aiMessageImages || [],
|
files: finalMessage?.files || aiMessageImages || [],
|
||||||
toolCall: finalMessage?.tool_call || toolCall,
|
toolCall: finalMessage?.tool_call || toolCall,
|
||||||
@ -1599,6 +1638,11 @@ export function ChatPage({
|
|||||||
mobile: settings?.isMobile,
|
mobile: settings?.isMobile,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const autoScrollEnabled =
|
||||||
|
user?.preferences?.auto_scroll == null
|
||||||
|
? settings?.enterpriseSettings?.auto_scroll || false
|
||||||
|
: user?.preferences?.auto_scroll!;
|
||||||
|
|
||||||
useScrollonStream({
|
useScrollonStream({
|
||||||
chatState: currentSessionChatState,
|
chatState: currentSessionChatState,
|
||||||
scrollableDivRef,
|
scrollableDivRef,
|
||||||
@ -1607,6 +1651,7 @@ export function ChatPage({
|
|||||||
debounceNumber,
|
debounceNumber,
|
||||||
waitForScrollRef,
|
waitForScrollRef,
|
||||||
mobile: settings?.isMobile,
|
mobile: settings?.isMobile,
|
||||||
|
enableAutoScroll: autoScrollEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Virtualization + Scrolling related effects and functions
|
// Virtualization + Scrolling related effects and functions
|
||||||
@ -1756,6 +1801,13 @@ export function ChatPage({
|
|||||||
liveAssistant
|
liveAssistant
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!retrievalEnabled) {
|
||||||
|
setDocumentSidebarToggled(false);
|
||||||
|
}
|
||||||
|
}, [retrievalEnabled]);
|
||||||
|
|
||||||
const [stackTraceModalContent, setStackTraceModalContent] = useState<
|
const [stackTraceModalContent, setStackTraceModalContent] = useState<
|
||||||
string | null
|
string | null
|
||||||
>(null);
|
>(null);
|
||||||
@ -1764,58 +1816,6 @@ export function ChatPage({
|
|||||||
const [settingsToggled, setSettingsToggled] = useState(false);
|
const [settingsToggled, setSettingsToggled] = useState(false);
|
||||||
|
|
||||||
const currentPersona = alternativeAssistant || liveAssistant;
|
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(() => {
|
useEffect(() => {
|
||||||
const handleSlackChatRedirect = async () => {
|
const handleSlackChatRedirect = async () => {
|
||||||
if (!slackChatId) return;
|
if (!slackChatId) return;
|
||||||
@ -1851,18 +1851,94 @@ export function ChatPage({
|
|||||||
|
|
||||||
handleSlackChatRedirect();
|
handleSlackChatRedirect();
|
||||||
}, [searchParams, router]);
|
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<HealthCheckBanner />
|
<HealthCheckBanner />
|
||||||
|
|
||||||
{showApiKeyModal && !shouldShowWelcomeModal ? (
|
{showApiKeyModal && !shouldShowWelcomeModal && (
|
||||||
<ApiKeyModal
|
<ApiKeyModal
|
||||||
hide={() => setShowApiKeyModal(false)}
|
hide={() => setShowApiKeyModal(false)}
|
||||||
setPopup={setPopup}
|
setPopup={setPopup}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
noAssistants && <NoAssistantModal isAdmin={isAdmin} />
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ChatPopup is a custom popup that displays a admin-specified message on initial user visit.
|
{/* 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
|
<SetDefaultModelModal
|
||||||
setPopup={setPopup}
|
setPopup={setPopup}
|
||||||
setLlmOverride={llmOverrideManager.setGlobalDefault}
|
setLlmOverride={llmOverrideManager.setGlobalDefault}
|
||||||
defaultModel={user?.preferences.default_model!}
|
defaultModel={user?.preferences.default_model!}
|
||||||
llmProviders={llmProviders}
|
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 && (
|
{deletingChatSession && (
|
||||||
<DeleteEntityModal
|
<DeleteEntityModal
|
||||||
entityType="chat"
|
entityType="chat"
|
||||||
@ -1996,6 +2102,50 @@ export function ChatPage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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
|
<BlurBackground
|
||||||
visible={!untoggled && (showDocSidebar || toggledSidebar)}
|
visible={!untoggled && (showDocSidebar || toggledSidebar)}
|
||||||
@ -2005,9 +2155,12 @@ export function ChatPage({
|
|||||||
ref={masterFlexboxRef}
|
ref={masterFlexboxRef}
|
||||||
className="flex h-full w-full overflow-x-hidden"
|
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 && (
|
{liveAssistant && (
|
||||||
<FunctionalHeader
|
<FunctionalHeader
|
||||||
|
toggleUserSettings={() => setUserSettingsToggled(true)}
|
||||||
|
liveAssistant={liveAssistant}
|
||||||
|
onAssistantChange={onAssistantChange}
|
||||||
sidebarToggled={toggledSidebar}
|
sidebarToggled={toggledSidebar}
|
||||||
reset={() => setMessage("")}
|
reset={() => setMessage("")}
|
||||||
page="chat"
|
page="chat"
|
||||||
@ -2018,6 +2171,8 @@ export function ChatPage({
|
|||||||
}
|
}
|
||||||
toggleSidebar={toggleSidebar}
|
toggleSidebar={toggleSidebar}
|
||||||
currentChatSession={selectedChatSession}
|
currentChatSession={selectedChatSession}
|
||||||
|
documentSidebarToggled={documentSidebarToggled}
|
||||||
|
llmOverrideManager={llmOverrideManager}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -2039,7 +2194,7 @@ export function ChatPage({
|
|||||||
duration-300
|
duration-300
|
||||||
ease-in-out
|
ease-in-out
|
||||||
h-full
|
h-full
|
||||||
${toggledSidebar ? "w-[250px]" : "w-[0px]"}
|
${toggledSidebar ? "w-[200px]" : "w-[0px]"}
|
||||||
`}
|
`}
|
||||||
></div>
|
></div>
|
||||||
)}
|
)}
|
||||||
@ -2049,9 +2204,55 @@ export function ChatPage({
|
|||||||
{...getRootProps()}
|
{...getRootProps()}
|
||||||
>
|
>
|
||||||
<div
|
<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}
|
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
|
{/* 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. */}
|
the top of the chat page. Oly used in the EE version of the app. */}
|
||||||
|
|
||||||
@ -2059,7 +2260,7 @@ export function ChatPage({
|
|||||||
!isFetchingChatMessages &&
|
!isFetchingChatMessages &&
|
||||||
currentSessionChatState == "input" &&
|
currentSessionChatState == "input" &&
|
||||||
!loadingError && (
|
!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} />
|
<ChatIntro selectedPersona={liveAssistant} />
|
||||||
|
|
||||||
<StarterMessages
|
<StarterMessages
|
||||||
@ -2081,6 +2282,7 @@ export function ChatPage({
|
|||||||
Recent Assistants
|
Recent Assistants
|
||||||
</div>
|
</div>
|
||||||
<AssistantBanner
|
<AssistantBanner
|
||||||
|
mobile={settings?.isMobile}
|
||||||
recentAssistants={recentAssistants}
|
recentAssistants={recentAssistants}
|
||||||
liveAssistant={liveAssistant}
|
liveAssistant={liveAssistant}
|
||||||
allAssistants={allAssistants}
|
allAssistants={allAssistants}
|
||||||
@ -2222,6 +2424,14 @@ export function ChatPage({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AIMessage
|
<AIMessage
|
||||||
|
index={i}
|
||||||
|
selectedMessageForDocDisplay={
|
||||||
|
selectedMessageForDocDisplay
|
||||||
|
}
|
||||||
|
documentSelectionToggled={
|
||||||
|
documentSidebarToggled &&
|
||||||
|
!filtersToggled
|
||||||
|
}
|
||||||
continueGenerating={
|
continueGenerating={
|
||||||
i == messageHistory.length - 1 &&
|
i == messageHistory.length - 1 &&
|
||||||
currentCanContinue()
|
currentCanContinue()
|
||||||
@ -2258,9 +2468,19 @@ export function ChatPage({
|
|||||||
}}
|
}}
|
||||||
isActive={messageHistory.length - 1 == i}
|
isActive={messageHistory.length - 1 == i}
|
||||||
selectedDocuments={selectedDocuments}
|
selectedDocuments={selectedDocuments}
|
||||||
toggleDocumentSelection={
|
toggleDocumentSelection={() => {
|
||||||
toggleDocumentSelectionAspects
|
if (
|
||||||
}
|
!documentSidebarToggled ||
|
||||||
|
(documentSidebarToggled &&
|
||||||
|
selectedMessageForDocDisplay ===
|
||||||
|
message.messageId)
|
||||||
|
) {
|
||||||
|
toggleDocumentSidebar();
|
||||||
|
}
|
||||||
|
setSelectedMessageForDocDisplay(
|
||||||
|
message.messageId
|
||||||
|
);
|
||||||
|
}}
|
||||||
docs={message.documents}
|
docs={message.documents}
|
||||||
currentPersona={liveAssistant}
|
currentPersona={liveAssistant}
|
||||||
alternativeAssistant={
|
alternativeAssistant={
|
||||||
@ -2268,7 +2488,6 @@ export function ChatPage({
|
|||||||
}
|
}
|
||||||
messageId={message.messageId}
|
messageId={message.messageId}
|
||||||
content={message.message}
|
content={message.message}
|
||||||
// content={message.message}
|
|
||||||
files={message.files}
|
files={message.files}
|
||||||
query={
|
query={
|
||||||
messageHistory[i]?.query || undefined
|
messageHistory[i]?.query || undefined
|
||||||
@ -2454,6 +2673,15 @@ export function ChatPage({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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*/}
|
{/* 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]" />
|
<div ref={endPaddingRef} className="h-[95px]" />
|
||||||
@ -2477,6 +2705,15 @@ export function ChatPage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ChatInputBar
|
<ChatInputBar
|
||||||
|
removeDocs={() => {
|
||||||
|
clearSelectedDocuments();
|
||||||
|
}}
|
||||||
|
removeFilters={() => {
|
||||||
|
filterManager.setSelectedSources([]);
|
||||||
|
filterManager.setSelectedTags([]);
|
||||||
|
filterManager.setSelectedDocumentSets([]);
|
||||||
|
setDocumentSidebarToggled(false);
|
||||||
|
}}
|
||||||
showConfigureAPIKey={() =>
|
showConfigureAPIKey={() =>
|
||||||
setShowApiKeyModal(true)
|
setShowApiKeyModal(true)
|
||||||
}
|
}
|
||||||
@ -2499,6 +2736,9 @@ export function ChatPage({
|
|||||||
llmOverrideManager={llmOverrideManager}
|
llmOverrideManager={llmOverrideManager}
|
||||||
files={currentMessageFiles}
|
files={currentMessageFiles}
|
||||||
setFiles={setCurrentMessageFiles}
|
setFiles={setCurrentMessageFiles}
|
||||||
|
toggleFilters={
|
||||||
|
retrievalEnabled ? toggleFilters : undefined
|
||||||
|
}
|
||||||
handleFileUpload={handleImageUpload}
|
handleFileUpload={handleImageUpload}
|
||||||
textAreaRef={textAreaRef}
|
textAreaRef={textAreaRef}
|
||||||
chatSessionId={chatSessionIdRef.current!}
|
chatSessionId={chatSessionIdRef.current!}
|
||||||
@ -2529,6 +2769,23 @@ export function ChatPage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
@ -2537,7 +2794,11 @@ export function ChatPage({
|
|||||||
<div
|
<div
|
||||||
style={{ transition: "width 0.30s ease-out" }}
|
style={{ transition: "width 0.30s ease-out" }}
|
||||||
className={`flex-none bg-transparent transition-all bg-opacity-80 duration-300 epase-in-out h-full
|
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">
|
<div className="my-auto">
|
||||||
<DanswerInitializingLoader />
|
<DanswerInitializingLoader />
|
||||||
@ -2548,20 +2809,8 @@ export function ChatPage({
|
|||||||
</div>
|
</div>
|
||||||
<FixedLogo backgroundToggled={toggledSidebar || showDocSidebar} />
|
<FixedLogo backgroundToggled={toggledSidebar || showDocSidebar} />
|
||||||
</div>
|
</div>
|
||||||
|
{/* Right Sidebar - DocumentSidebar */}
|
||||||
</div>
|
</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 { SourceIcon } from "@/components/SourceIcon";
|
||||||
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
|
||||||
import { DanswerDocument } from "@/lib/search/interfaces";
|
import { DanswerDocument } from "@/lib/search/interfaces";
|
||||||
import { FiInfo, FiRadio } from "react-icons/fi";
|
import { FiTag } from "react-icons/fi";
|
||||||
import { DocumentSelector } from "./DocumentSelector";
|
import { DocumentSelector } from "./DocumentSelector";
|
||||||
import {
|
import { buildDocumentSummaryDisplay } from "@/components/search/DocumentDisplay";
|
||||||
DocumentMetadataBlock,
|
import { DocumentUpdatedAtBadge } from "@/components/search/DocumentUpdatedAtBadge";
|
||||||
buildDocumentSummaryDisplay,
|
import { MetadataBadge } from "@/components/MetadataBadge";
|
||||||
} from "@/components/search/DocumentDisplay";
|
import { WebResultIcon } from "@/components/WebResultIcon";
|
||||||
import { InternetSearchIcon } from "@/components/InternetSearchIcon";
|
|
||||||
|
|
||||||
interface DocumentDisplayProps {
|
interface DocumentDisplayProps {
|
||||||
document: DanswerDocument;
|
document: DanswerDocument;
|
||||||
queryEventId: number | null;
|
modal?: boolean;
|
||||||
isAIPick: boolean;
|
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
handleSelect: (documentId: string) => void;
|
handleSelect: (documentId: string) => void;
|
||||||
setPopup: (popupSpec: PopupSpec | null) => void;
|
|
||||||
tokenLimitReached: boolean;
|
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({
|
export function ChatDocumentDisplay({
|
||||||
document,
|
document,
|
||||||
queryEventId,
|
modal,
|
||||||
isAIPick,
|
|
||||||
isSelected,
|
isSelected,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
setPopup,
|
|
||||||
tokenLimitReached,
|
tokenLimitReached,
|
||||||
}: DocumentDisplayProps) {
|
}: DocumentDisplayProps) {
|
||||||
const isInternet = document.is_internet;
|
const isInternet = document.is_internet;
|
||||||
// Consider reintroducing null scored docs in the future
|
|
||||||
|
|
||||||
if (document.score === null) {
|
if (document.score === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`opacity-100 ${modal ? "w-[90vw]" : "w-full"}`}>
|
||||||
key={document.semantic_identifier}
|
<div
|
||||||
className={`p-2 w-[325px] justify-start rounded-md ${
|
className={`flex relative flex-col gap-0.5 rounded-xl mx-2 my-1 ${
|
||||||
isSelected ? "bg-background-200" : "bg-background-125"
|
isSelected ? "bg-gray-200" : "hover:bg-background-125"
|
||||||
} text-sm mx-3`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex relative justify-start overflow-y-visible">
|
|
||||||
<a
|
<a
|
||||||
href={document.link}
|
href={document.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className={
|
rel="noopener noreferrer"
|
||||||
"rounded-lg flex font-bold flex-shrink truncate" +
|
className="cursor-pointer flex flex-col px-2 py-1.5"
|
||||||
(document.link ? "" : "pointer-events-none")
|
|
||||||
}
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
>
|
||||||
{isInternet ? (
|
<div className="line-clamp-1 mb-1 flex h-6 items-center gap-2 text-xs">
|
||||||
<InternetSearchIcon url={document.link} />
|
{document.is_internet || document.source_type === "web" ? (
|
||||||
) : (
|
<WebResultIcon url={document.link} />
|
||||||
<SourceIcon sourceType={document.source_type} iconSize={18} />
|
) : (
|
||||||
)}
|
<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
|
<div className="line-clamp-1 text-text-900 text-sm font-semibold">
|
||||||
className={`
|
{(document.semantic_identifier || document.document_id).length >
|
||||||
text-xs
|
(modal ? 30 : 40)
|
||||||
text-emphasis
|
? `${(document.semantic_identifier || document.document_id)
|
||||||
bg-hover
|
.slice(0, modal ? 30 : 40)
|
||||||
rounded
|
.trim()}...`
|
||||||
p-0.5
|
: document.semantic_identifier || document.document_id}
|
||||||
w-fit
|
|
||||||
my-auto
|
|
||||||
select-none
|
|
||||||
my-auto
|
|
||||||
mr-2`}
|
|
||||||
>
|
|
||||||
{Math.abs(document.score).toFixed(2)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<DocumentMetadataBlock modal={modal} document={document} />
|
||||||
|
<div className="line-clamp-3 pt-2 text-sm font-normal leading-snug text-gray-600">
|
||||||
{!isInternet && (
|
{buildDocumentSummaryDisplay(
|
||||||
<DocumentSelector
|
document.match_highlights,
|
||||||
isSelected={isSelected}
|
document.blurb
|
||||||
handleSelect={() => handleSelect(document.document_id)}
|
)}
|
||||||
isDisabled={tokenLimitReached && !isSelected}
|
</div>
|
||||||
/>
|
<div className="absolute top-2 right-2">
|
||||||
)}
|
{!isInternet && (
|
||||||
</div>
|
<DocumentSelector
|
||||||
<div>
|
isSelected={isSelected}
|
||||||
<div className="mt-1">
|
handleSelect={() => handleSelect(document.document_id)}
|
||||||
<DocumentMetadataBlock document={document} />
|
isDisabled={tokenLimitReached && !isSelected}
|
||||||
</div>
|
/>
|
||||||
</div>
|
)}
|
||||||
<p className="line-clamp-3 pl-1 pt-2 mb-1 text-start break-words">
|
</div>
|
||||||
{buildDocumentSummaryDisplay(document.match_highlights, document.blurb)}
|
</a>
|
||||||
test
|
|
||||||
</p>
|
|
||||||
<div className="mb-2">
|
|
||||||
{/*
|
|
||||||
// TODO: find a way to include this
|
|
||||||
{queryEventId && (
|
|
||||||
<DocumentFeedbackBlock
|
|
||||||
documentId={document.document_id}
|
|
||||||
queryId={queryEventId}
|
|
||||||
setPopup={setPopup}
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
</div>
|
</div>
|
||||||
</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 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 { ChatInputOption } from "./ChatInputOption";
|
||||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||||
import { InputPrompt } from "@/app/admin/prompt-library/interfaces";
|
import { InputPrompt } from "@/app/admin/prompt-library/interfaces";
|
||||||
import {
|
import { FilterManager, LlmOverrideManager } from "@/lib/hooks";
|
||||||
FilterManager,
|
|
||||||
getDisplayNameForModel,
|
|
||||||
LlmOverrideManager,
|
|
||||||
} from "@/lib/hooks";
|
|
||||||
import { SelectedFilterDisplay } from "./SelectedFilterDisplay";
|
import { SelectedFilterDisplay } from "./SelectedFilterDisplay";
|
||||||
import { useChatContext } from "@/components/context/ChatContext";
|
import { useChatContext } from "@/components/context/ChatContext";
|
||||||
import { getFinalLLM } from "@/lib/llm/utils";
|
import { getFinalLLM } from "@/lib/llm/utils";
|
||||||
@ -18,15 +14,10 @@ import {
|
|||||||
} from "../files/InputBarPreview";
|
} from "../files/InputBarPreview";
|
||||||
import {
|
import {
|
||||||
AssistantsIconSkeleton,
|
AssistantsIconSkeleton,
|
||||||
CpuIconSkeleton,
|
|
||||||
FileIcon,
|
FileIcon,
|
||||||
SendIcon,
|
SendIcon,
|
||||||
StopGeneratingIcon,
|
StopGeneratingIcon,
|
||||||
} from "@/components/icons/icons";
|
} 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 { DanswerDocument } from "@/lib/search/interfaces";
|
||||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||||
import {
|
import {
|
||||||
@ -40,10 +31,18 @@ import { SettingsContext } from "@/components/settings/SettingsProvider";
|
|||||||
import { ChatState } from "../types";
|
import { ChatState } from "../types";
|
||||||
import UnconfiguredProviderText from "@/components/chat_search/UnconfiguredProviderText";
|
import UnconfiguredProviderText from "@/components/chat_search/UnconfiguredProviderText";
|
||||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
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;
|
const MAX_INPUT_HEIGHT = 200;
|
||||||
|
|
||||||
export function ChatInputBar({
|
export function ChatInputBar({
|
||||||
|
removeFilters,
|
||||||
|
removeDocs,
|
||||||
openModelSettings,
|
openModelSettings,
|
||||||
showDocs,
|
showDocs,
|
||||||
showConfigureAPIKey,
|
showConfigureAPIKey,
|
||||||
@ -68,7 +67,10 @@ export function ChatInputBar({
|
|||||||
alternativeAssistant,
|
alternativeAssistant,
|
||||||
chatSessionId,
|
chatSessionId,
|
||||||
inputPrompts,
|
inputPrompts,
|
||||||
|
toggleFilters,
|
||||||
}: {
|
}: {
|
||||||
|
removeFilters: () => void;
|
||||||
|
removeDocs: () => void;
|
||||||
showConfigureAPIKey: () => void;
|
showConfigureAPIKey: () => void;
|
||||||
openModelSettings: () => void;
|
openModelSettings: () => void;
|
||||||
chatState: ChatState;
|
chatState: ChatState;
|
||||||
@ -90,6 +92,7 @@ export function ChatInputBar({
|
|||||||
handleFileUpload: (files: File[]) => void;
|
handleFileUpload: (files: File[]) => void;
|
||||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||||
chatSessionId?: string;
|
chatSessionId?: string;
|
||||||
|
toggleFilters?: () => void;
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const textarea = textAreaRef.current;
|
const textarea = textAreaRef.current;
|
||||||
@ -370,9 +373,9 @@ export function ChatInputBar({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
{/* <div>
|
||||||
<SelectedFilterDisplay filterManager={filterManager} />
|
<SelectedFilterDisplay filterManager={filterManager} />
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<UnconfiguredProviderText showConfigureAPIKey={showConfigureAPIKey} />
|
<UnconfiguredProviderText showConfigureAPIKey={showConfigureAPIKey} />
|
||||||
|
|
||||||
@ -429,16 +432,21 @@ export function ChatInputBar({
|
|||||||
)}
|
)}
|
||||||
{(selectedDocuments.length > 0 || files.length > 0) && (
|
{(selectedDocuments.length > 0 || files.length > 0) && (
|
||||||
<div className="flex gap-x-2 px-2 pt-2">
|
<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 && (
|
{selectedDocuments.length > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={showDocs}
|
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} />
|
<FileIcon size={20} />
|
||||||
<p className="text-xs">
|
<span className="text-sm whitespace-nowrap overflow-hidden text-ellipsis">
|
||||||
{selectedDocuments.length} selected
|
{selectedDocuments.length} selected
|
||||||
</p>
|
</span>
|
||||||
|
<XIcon
|
||||||
|
onClick={removeDocs}
|
||||||
|
size={16}
|
||||||
|
className="text-text-400 hover:text-text-600 ml-auto"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{files.map((file) => (
|
{files.map((file) => (
|
||||||
@ -529,72 +537,6 @@ export function ChatInputBar({
|
|||||||
suppressContentEditableWarning={true}
|
suppressContentEditableWarning={true}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center space-x-3 mr-12 px-4 pb-2">
|
<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
|
<ChatInputOption
|
||||||
flexPriority="stiff"
|
flexPriority="stiff"
|
||||||
name="File"
|
name="File"
|
||||||
@ -614,6 +556,14 @@ export function ChatInputBar({
|
|||||||
input.click();
|
input.click();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{toggleFilters && (
|
||||||
|
<ChatInputOption
|
||||||
|
flexPriority="stiff"
|
||||||
|
name="Filters"
|
||||||
|
Icon={FiSearch}
|
||||||
|
onClick={toggleFilters}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute bottom-2.5 mobile:right-4 desktop:right-10">
|
<div className="absolute bottom-2.5 mobile:right-4 desktop:right-10">
|
||||||
|
@ -2,6 +2,7 @@ import {
|
|||||||
AnswerPiecePacket,
|
AnswerPiecePacket,
|
||||||
DanswerDocument,
|
DanswerDocument,
|
||||||
Filters,
|
Filters,
|
||||||
|
FinalContextDocs,
|
||||||
StreamStopInfo,
|
StreamStopInfo,
|
||||||
} from "@/lib/search/interfaces";
|
} from "@/lib/search/interfaces";
|
||||||
import { handleSSEStream } from "@/lib/search/streamingUtils";
|
import { handleSSEStream } from "@/lib/search/streamingUtils";
|
||||||
@ -102,6 +103,7 @@ export type PacketType =
|
|||||||
| ToolCallMetadata
|
| ToolCallMetadata
|
||||||
| BackendMessage
|
| BackendMessage
|
||||||
| AnswerPiecePacket
|
| AnswerPiecePacket
|
||||||
|
| FinalContextDocs
|
||||||
| DocumentsResponse
|
| DocumentsResponse
|
||||||
| FileChatDisplay
|
| FileChatDisplay
|
||||||
| StreamingError
|
| StreamingError
|
||||||
@ -147,7 +149,6 @@ export async function* sendMessage({
|
|||||||
}): AsyncGenerator<PacketType, void, unknown> {
|
}): AsyncGenerator<PacketType, void, unknown> {
|
||||||
const documentsAreSelected =
|
const documentsAreSelected =
|
||||||
selectedDocumentIds && selectedDocumentIds.length > 0;
|
selectedDocumentIds && selectedDocumentIds.length > 0;
|
||||||
|
|
||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
alternate_assistant_id: alternateAssistantId,
|
alternate_assistant_id: alternateAssistantId,
|
||||||
chat_session_id: chatSessionId,
|
chat_session_id: chatSessionId,
|
||||||
@ -639,6 +640,7 @@ export async function useScrollonStream({
|
|||||||
endDivRef,
|
endDivRef,
|
||||||
debounceNumber,
|
debounceNumber,
|
||||||
mobile,
|
mobile,
|
||||||
|
enableAutoScroll,
|
||||||
}: {
|
}: {
|
||||||
chatState: ChatState;
|
chatState: ChatState;
|
||||||
scrollableDivRef: RefObject<HTMLDivElement>;
|
scrollableDivRef: RefObject<HTMLDivElement>;
|
||||||
@ -647,6 +649,7 @@ export async function useScrollonStream({
|
|||||||
endDivRef: RefObject<HTMLDivElement>;
|
endDivRef: RefObject<HTMLDivElement>;
|
||||||
debounceNumber: number;
|
debounceNumber: number;
|
||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
|
enableAutoScroll?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const mobileDistance = 900; // distance that should "engage" the scroll
|
const mobileDistance = 900; // distance that should "engage" the scroll
|
||||||
const desktopDistance = 500; // 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);
|
const previousScroll = useRef<number>(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!enableAutoScroll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (chatState != "input" && scrollableDivRef && scrollableDivRef.current) {
|
if (chatState != "input" && scrollableDivRef && scrollableDivRef.current) {
|
||||||
const newHeight: number = scrollableDivRef.current?.scrollTop!;
|
const newHeight: number = scrollableDivRef.current?.scrollTop!;
|
||||||
const heightDifference = newHeight - previousScroll.current;
|
const heightDifference = newHeight - previousScroll.current;
|
||||||
@ -716,7 +723,7 @@ export async function useScrollonStream({
|
|||||||
|
|
||||||
// scroll on end of stream if within distance
|
// scroll on end of stream if within distance
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scrollableDivRef?.current && chatState == "input") {
|
if (scrollableDivRef?.current && chatState == "input" && enableAutoScroll) {
|
||||||
if (scrollDist.current < distance - 50) {
|
if (scrollDist.current < distance - 50) {
|
||||||
scrollableDivRef?.current?.scrollBy({
|
scrollableDivRef?.current?.scrollBy({
|
||||||
left: 0,
|
left: 0,
|
||||||
|
@ -1,8 +1,50 @@
|
|||||||
import { Citation } from "@/components/search/results/Citation";
|
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 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) => {
|
export const MemoizedLink = memo((props: any) => {
|
||||||
const { node, ...rest } = props;
|
const { node, document, ...rest } = props;
|
||||||
const value = rest.children;
|
const value = rest.children;
|
||||||
|
|
||||||
if (value?.toString().startsWith("*")) {
|
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" />
|
<div className="flex-none bg-background-800 inline-block rounded-full h-3 w-3 ml-2" />
|
||||||
);
|
);
|
||||||
} else if (value?.toString().startsWith("[")) {
|
} 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 {
|
} else {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
@ -25,9 +76,16 @@ export const MemoizedLink = memo((props: any) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const MemoizedParagraph = memo(({ ...props }: any) => {
|
export const MemoizedParagraph = memo(
|
||||||
return <p {...props} className="text-default" />;
|
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";
|
MemoizedLink.displayName = "MemoizedLink";
|
||||||
MemoizedParagraph.displayName = "MemoizedParagraph";
|
MemoizedParagraph.displayName = "MemoizedParagraph";
|
||||||
|
@ -8,14 +8,22 @@ import {
|
|||||||
FiGlobe,
|
FiGlobe,
|
||||||
} from "react-icons/fi";
|
} from "react-icons/fi";
|
||||||
import { FeedbackType } from "../types";
|
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 ReactMarkdown from "react-markdown";
|
||||||
import {
|
import {
|
||||||
DanswerDocument,
|
DanswerDocument,
|
||||||
FilteredDanswerDocument,
|
FilteredDanswerDocument,
|
||||||
} from "@/lib/search/interfaces";
|
} from "@/lib/search/interfaces";
|
||||||
import { SearchSummary } from "./SearchSummary";
|
import { SearchSummary } from "./SearchSummary";
|
||||||
import { SourceIcon } from "@/components/SourceIcon";
|
|
||||||
import { SkippedSearch } from "./SkippedSearch";
|
import { SkippedSearch } from "./SkippedSearch";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { CopyButton } from "@/components/CopyButton";
|
import { CopyButton } from "@/components/CopyButton";
|
||||||
@ -36,8 +44,6 @@ import "prismjs/themes/prism-tomorrow.css";
|
|||||||
import "./custom-code-styles.css";
|
import "./custom-code-styles.css";
|
||||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
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 { LikeFeedback, DislikeFeedback } from "@/components/icons/icons";
|
||||||
import {
|
import {
|
||||||
@ -52,16 +58,18 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { useMouseTracking } from "./hooks";
|
import { useMouseTracking } from "./hooks";
|
||||||
import { InternetSearchIcon } from "@/components/InternetSearchIcon";
|
|
||||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||||
import GeneratingImageDisplay from "../tools/GeneratingImageDisplay";
|
import GeneratingImageDisplay from "../tools/GeneratingImageDisplay";
|
||||||
import RegenerateOption from "../RegenerateOption";
|
import RegenerateOption from "../RegenerateOption";
|
||||||
import { LlmOverride } from "@/lib/hooks";
|
import { LlmOverride } from "@/lib/hooks";
|
||||||
import { ContinueGenerating } from "./ContinueMessage";
|
import { ContinueGenerating } from "./ContinueMessage";
|
||||||
import { MemoizedLink, MemoizedParagraph } from "./MemoizedTextComponents";
|
import { MemoizedAnchor, MemoizedParagraph } from "./MemoizedTextComponents";
|
||||||
import { extractCodeText } from "./codeUtils";
|
import { extractCodeText } from "./codeUtils";
|
||||||
import ToolResult from "../../../components/tools/ToolResult";
|
import ToolResult from "../../../components/tools/ToolResult";
|
||||||
import CsvContent from "../../../components/tools/CSVContent";
|
import CsvContent from "../../../components/tools/CSVContent";
|
||||||
|
import SourceCard, {
|
||||||
|
SeeMoreBlock,
|
||||||
|
} from "@/components/chat_search/sources/SourceCard";
|
||||||
|
|
||||||
const TOOLS_WITH_CUSTOM_HANDLING = [
|
const TOOLS_WITH_CUSTOM_HANDLING = [
|
||||||
SEARCH_TOOL_NAME,
|
SEARCH_TOOL_NAME,
|
||||||
@ -155,6 +163,7 @@ function FileDisplay({
|
|||||||
export const AIMessage = ({
|
export const AIMessage = ({
|
||||||
regenerate,
|
regenerate,
|
||||||
overriddenModel,
|
overriddenModel,
|
||||||
|
selectedMessageForDocDisplay,
|
||||||
continueGenerating,
|
continueGenerating,
|
||||||
shared,
|
shared,
|
||||||
isActive,
|
isActive,
|
||||||
@ -162,6 +171,7 @@ export const AIMessage = ({
|
|||||||
alternativeAssistant,
|
alternativeAssistant,
|
||||||
docs,
|
docs,
|
||||||
messageId,
|
messageId,
|
||||||
|
documentSelectionToggled,
|
||||||
content,
|
content,
|
||||||
files,
|
files,
|
||||||
selectedDocuments,
|
selectedDocuments,
|
||||||
@ -178,7 +188,10 @@ export const AIMessage = ({
|
|||||||
currentPersona,
|
currentPersona,
|
||||||
otherMessagesCanSwitchTo,
|
otherMessagesCanSwitchTo,
|
||||||
onMessageSelection,
|
onMessageSelection,
|
||||||
|
index,
|
||||||
}: {
|
}: {
|
||||||
|
index?: number;
|
||||||
|
selectedMessageForDocDisplay?: number | null;
|
||||||
shared?: boolean;
|
shared?: boolean;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
continueGenerating?: () => void;
|
continueGenerating?: () => void;
|
||||||
@ -191,6 +204,7 @@ export const AIMessage = ({
|
|||||||
currentPersona: Persona;
|
currentPersona: Persona;
|
||||||
messageId: number | null;
|
messageId: number | null;
|
||||||
content: string | JSX.Element;
|
content: string | JSX.Element;
|
||||||
|
documentSelectionToggled?: boolean;
|
||||||
files?: FileDescriptor[];
|
files?: FileDescriptor[];
|
||||||
query?: string;
|
query?: string;
|
||||||
citedDocuments?: [string, DanswerDocument][] | null;
|
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
|
const currentMessageInd = messageId
|
||||||
? otherMessagesCanSwitchTo?.indexOf(messageId)
|
? otherMessagesCanSwitchTo?.indexOf(messageId)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const uniqueSources: ValidSources[] = Array.from(
|
const uniqueSources: ValidSources[] = Array.from(
|
||||||
new Set((docs || []).map((doc) => doc.source_type))
|
new Set((docs || []).map((doc) => doc.source_type))
|
||||||
).slice(0, 3);
|
).slice(0, 3);
|
||||||
|
|
||||||
const markdownComponents = useMemo(
|
const markdownComponents = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
a: MemoizedLink,
|
a: anchorCallback,
|
||||||
p: MemoizedParagraph,
|
p: paragraphCallback,
|
||||||
code: ({ node, className, children, ...props }: any) => {
|
code: ({ node, className, children }: any) => {
|
||||||
const codeText = extractCodeText(
|
const codeText = extractCodeText(
|
||||||
node,
|
node,
|
||||||
finalContent as string,
|
finalContent as string,
|
||||||
@ -312,7 +339,7 @@ export const AIMessage = ({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[finalContent]
|
[anchorCallback, paragraphCallback, finalContent]
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderedMarkdown = useMemo(() => {
|
const renderedMarkdown = useMemo(() => {
|
||||||
@ -333,12 +360,11 @@ export const AIMessage = ({
|
|||||||
onMessageSelection &&
|
onMessageSelection &&
|
||||||
otherMessagesCanSwitchTo &&
|
otherMessagesCanSwitchTo &&
|
||||||
otherMessagesCanSwitchTo.length > 1;
|
otherMessagesCanSwitchTo.length > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="danswer-ai-message"
|
id="danswer-ai-message"
|
||||||
ref={trackedElementRef}
|
ref={trackedElementRef}
|
||||||
className={"py-5 ml-4 px-5 relative flex "}
|
className={`py-5 ml-4 px-5 relative flex `}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`mx-auto ${
|
className={`mx-auto ${
|
||||||
@ -363,6 +389,7 @@ export const AIMessage = ({
|
|||||||
!retrievalDisabled && (
|
!retrievalDisabled && (
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
<SearchSummary
|
<SearchSummary
|
||||||
|
index={index || 0}
|
||||||
query={query}
|
query={query}
|
||||||
finished={toolCall?.tool_result != undefined}
|
finished={toolCall?.tool_result != undefined}
|
||||||
hasDocs={hasDocs || false}
|
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 ? (
|
{content || files ? (
|
||||||
<>
|
<>
|
||||||
<FileDisplay files={files || []} />
|
<FileDisplay files={files || []} />
|
||||||
@ -438,81 +490,6 @@ export const AIMessage = ({
|
|||||||
) : isComplete ? null : (
|
) : 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>
|
</div>
|
||||||
|
|
||||||
{handleFeedback &&
|
{handleFeedback &&
|
||||||
|
@ -41,6 +41,7 @@ export function ShowHideDocsButton({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SearchSummary({
|
export function SearchSummary({
|
||||||
|
index,
|
||||||
query,
|
query,
|
||||||
hasDocs,
|
hasDocs,
|
||||||
finished,
|
finished,
|
||||||
@ -48,6 +49,7 @@ export function SearchSummary({
|
|||||||
handleShowRetrieved,
|
handleShowRetrieved,
|
||||||
handleSearchQueryEdit,
|
handleSearchQueryEdit,
|
||||||
}: {
|
}: {
|
||||||
|
index: number;
|
||||||
finished: boolean;
|
finished: boolean;
|
||||||
query: string;
|
query: string;
|
||||||
hasDocs: boolean;
|
hasDocs: boolean;
|
||||||
@ -98,7 +100,14 @@ export function SearchSummary({
|
|||||||
!text-sm !line-clamp-1 !break-all px-0.5`}
|
!text-sm !line-clamp-1 !break-all px-0.5`}
|
||||||
ref={searchingForRef}
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -53,7 +53,7 @@ export const FeedbackModal = ({
|
|||||||
: predefinedNegativeFeedbackOptions;
|
: predefinedNegativeFeedbackOptions;
|
||||||
|
|
||||||
return (
|
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">
|
<h2 className="text-2xl text-emphasis font-bold mb-4 flex">
|
||||||
<div className="mr-1 my-auto">
|
<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 { Modal } from "@/components/Modal";
|
||||||
import Text from "@/components/ui/text";
|
import Text from "@/components/ui/text";
|
||||||
import { getDisplayNameForModel, LlmOverride } from "@/lib/hooks";
|
import { getDisplayNameForModel, LlmOverride } from "@/lib/hooks";
|
||||||
@ -9,6 +9,10 @@ import { setUserDefaultModel } from "@/lib/users/UserSettings";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||||
import { useUser } from "@/components/user/UserProvider";
|
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({
|
export function SetDefaultModelModal({
|
||||||
setPopup,
|
setPopup,
|
||||||
@ -23,7 +27,7 @@ export function SetDefaultModelModal({
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
defaultModel: string | null;
|
defaultModel: string | null;
|
||||||
}) {
|
}) {
|
||||||
const { refreshUser } = useUser();
|
const { refreshUser, user, updateUserAutoScroll } = useUser();
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const messageRef = useRef<HTMLDivElement>(null);
|
const messageRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -121,16 +125,41 @@ export function SetDefaultModelModal({
|
|||||||
const defaultProvider = llmProviders.find(
|
const defaultProvider = llmProviders.find(
|
||||||
(llmProvider) => llmProvider.is_default_provider
|
(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 (
|
return (
|
||||||
<Modal onOutsideClick={onClose} width="rounded-lg bg-white max-w-xl">
|
<Modal onOutsideClick={onClose} width="rounded-lg bg-white max-w-xl">
|
||||||
<>
|
<>
|
||||||
<div className="flex mb-4">
|
<div className="flex mb-4">
|
||||||
<h2 className="text-2xl text-emphasis font-bold flex my-auto">
|
<h2 className="text-2xl text-emphasis font-bold flex my-auto">
|
||||||
Set Default Model
|
User settings
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</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">
|
<Text className="mb-4">
|
||||||
Choose a Large Language Model (LLM) to serve as the default for
|
Choose a Large Language Model (LLM) to serve as the default for
|
||||||
assistants that don't have a default model assigned.
|
assistants that don't have a default model assigned.
|
||||||
|
@ -32,6 +32,7 @@ export default async function Page(props: {
|
|||||||
defaultAssistantId,
|
defaultAssistantId,
|
||||||
shouldShowWelcomeModal,
|
shouldShowWelcomeModal,
|
||||||
userInputPrompts,
|
userInputPrompts,
|
||||||
|
ccPairs,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -44,6 +45,9 @@ export default async function Page(props: {
|
|||||||
value={{
|
value={{
|
||||||
chatSessions,
|
chatSessions,
|
||||||
availableSources,
|
availableSources,
|
||||||
|
ccPairs,
|
||||||
|
documentSets,
|
||||||
|
tags,
|
||||||
availableDocumentSets: documentSets,
|
availableDocumentSets: documentSets,
|
||||||
availableTags: tags,
|
availableTags: tags,
|
||||||
llmProviders,
|
llmProviders,
|
||||||
|
@ -113,7 +113,7 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
|||||||
{page == "chat" && (
|
{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">
|
<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
|
<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={
|
href={
|
||||||
`/${page}` +
|
`/${page}` +
|
||||||
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA &&
|
(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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
href={
|
href="/chat"
|
||||||
settings && settings.default_page === "chat" ? "/chat" : "/search"
|
|
||||||
}
|
|
||||||
className="fixed cursor-pointer flex z-40 left-2.5 top-2"
|
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">
|
<div className="max-w-[200px] mobile:hidden flex items-center gap-x-1 my-auto">
|
||||||
@ -49,7 +47,7 @@ export default function FixedLogo({
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="mobile:hidden fixed left-2.5 bottom-4">
|
<div className="mobile:hidden fixed left-2.5 bottom-4">
|
||||||
<FiSidebar className="text-text-mobile-sidebar" />
|
{/* <FiSidebar className="text-text-mobile-sidebar" /> */}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,90 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { ReactNode, useContext, useEffect, useState } from "react";
|
import React, { ReactNode, useEffect, useState } from "react";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { 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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function FunctionalWrapper({
|
export default function FunctionalWrapper({
|
||||||
initiallyToggled,
|
initiallyToggled,
|
||||||
@ -128,12 +45,6 @@ export default function FunctionalWrapper({
|
|||||||
window.removeEventListener("keydown", handleKeyDown);
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [router]);
|
}, [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);
|
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
|
||||||
|
|
||||||
@ -145,24 +56,7 @@ export default function FunctionalWrapper({
|
|||||||
|
|
||||||
return (
|
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">
|
<div className="overscroll-y-contain overflow-y-scroll overscroll-contain left-0 top-0 w-full h-svh">
|
||||||
{content(toggledSidebar, toggle)}
|
{content(toggledSidebar, toggle)}
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
|
auto_scroll: enterpriseSettings?.auto_scroll || false,
|
||||||
application_name: enterpriseSettings?.application_name || null,
|
application_name: enterpriseSettings?.application_name || null,
|
||||||
use_custom_logo: enterpriseSettings?.use_custom_logo || false,
|
use_custom_logo: enterpriseSettings?.use_custom_logo || false,
|
||||||
use_custom_logotype: enterpriseSettings?.use_custom_logotype || false,
|
use_custom_logotype: enterpriseSettings?.use_custom_logotype || false,
|
||||||
@ -71,6 +72,7 @@ export function WhitelabelingForm() {
|
|||||||
enterpriseSettings?.enable_consent_screen || false,
|
enterpriseSettings?.enable_consent_screen || false,
|
||||||
}}
|
}}
|
||||||
validationSchema={Yup.object().shape({
|
validationSchema={Yup.object().shape({
|
||||||
|
auto_scroll: Yup.boolean().nullable(),
|
||||||
application_name: Yup.string().nullable(),
|
application_name: Yup.string().nullable(),
|
||||||
use_custom_logo: Yup.boolean().required(),
|
use_custom_logo: Yup.boolean().required(),
|
||||||
use_custom_logotype: Yup.boolean().required(),
|
use_custom_logotype: Yup.boolean().required(),
|
||||||
|
@ -1,15 +1,5 @@
|
|||||||
import { fetchSettingsSS } from "@/components/settings/lib";
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const settings = await fetchSettingsSS();
|
redirect("/chat");
|
||||||
if (!settings) {
|
|
||||||
redirect("/search");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.settings.default_page === "search") {
|
|
||||||
redirect("/search");
|
|
||||||
} else {
|
|
||||||
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({
|
export function MetadataBadge({
|
||||||
icon,
|
icon,
|
||||||
value,
|
value,
|
||||||
|
flexNone,
|
||||||
}: {
|
}: {
|
||||||
icon?: React.FC<{ size?: number; className?: string }>;
|
icon?: React.FC<{ size?: number; className?: string }>;
|
||||||
value: string | JSX.Element;
|
value: string | JSX.Element;
|
||||||
|
flexNone?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -18,9 +20,13 @@ export function MetadataBadge({
|
|||||||
w-fit
|
w-fit
|
||||||
my-auto
|
my-auto
|
||||||
select-none
|
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 className="my-auto flex">{value}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { FiX } from "react-icons/fi";
|
|
||||||
import { IconProps, XIcon } from "./icons/icons";
|
import { IconProps, XIcon } from "./icons/icons";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { isEventWithinRef } from "@/lib/contains";
|
import { isEventWithinRef } from "@/lib/contains";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
icon?: ({ size, className }: IconProps) => JSX.Element;
|
icon?: ({ size, className }: IconProps) => JSX.Element;
|
||||||
@ -18,6 +18,8 @@ interface ModalProps {
|
|||||||
hideDividerForTitle?: boolean;
|
hideDividerForTitle?: boolean;
|
||||||
hideCloseButton?: boolean;
|
hideCloseButton?: boolean;
|
||||||
noPadding?: boolean;
|
noPadding?: boolean;
|
||||||
|
height?: string;
|
||||||
|
noScroll?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Modal({
|
export function Modal({
|
||||||
@ -28,9 +30,11 @@ export function Modal({
|
|||||||
width,
|
width,
|
||||||
titleSize,
|
titleSize,
|
||||||
hideDividerForTitle,
|
hideDividerForTitle,
|
||||||
|
height,
|
||||||
noPadding,
|
noPadding,
|
||||||
icon,
|
icon,
|
||||||
hideCloseButton,
|
hideCloseButton,
|
||||||
|
noScroll,
|
||||||
}: ModalProps) {
|
}: ModalProps) {
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
@ -56,8 +60,10 @@ export function Modal({
|
|||||||
const modalContent = (
|
const modalContent = (
|
||||||
<div
|
<div
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
className={`fixed inset-0 bg-black bg-opacity-25 backdrop-blur-sm h-full
|
className={cn(
|
||||||
flex items-center justify-center z-[9999] transition-opacity duration-300 ease-in-out`}
|
`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
|
<div
|
||||||
ref={modalRef}
|
ref={modalRef}
|
||||||
@ -93,8 +99,7 @@ export function Modal({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="w-full overflow-y-hidden flex flex-col h-full justify-stretch">
|
||||||
<div className="w-full flex flex-col h-full justify-stretch">
|
|
||||||
{title && (
|
{title && (
|
||||||
<>
|
<>
|
||||||
<div className="flex mb-4">
|
<div className="flex mb-4">
|
||||||
@ -110,7 +115,14 @@ export function Modal({
|
|||||||
{!hideDividerForTitle && <Separator />}
|
{!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>
|
</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 { Popover } from "./popover/Popover";
|
||||||
import { LOGOUT_DISABLED } from "@/lib/constants";
|
import { LOGOUT_DISABLED } from "@/lib/constants";
|
||||||
import { SettingsContext } from "./settings/SettingsProvider";
|
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 { pageType } from "@/app/chat/sessionSidebar/types";
|
||||||
import { NavigationItem, Notification } from "@/app/admin/settings/interfaces";
|
import { NavigationItem, Notification } from "@/app/admin/settings/interfaces";
|
||||||
import DynamicFaIcon, { preloadIcons } from "./icons/DynamicFaIcon";
|
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 { user, isCurator } = useUser();
|
||||||
const [userInfoVisible, setUserInfoVisible] = useState(false);
|
const [userInfoVisible, setUserInfoVisible] = useState(false);
|
||||||
const userInfoRef = useRef<HTMLDivElement>(null);
|
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
|
<DropdownOption
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUserInfoVisible(true);
|
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">
|
<nav className="space-y-2">
|
||||||
<div className="w-full justify-center mb-4 flex">
|
<div className="w-full justify-center mb-4 flex">
|
||||||
<div className="w-52">
|
<div className="w-52">
|
||||||
<Link
|
<Link className="flex flex-col" href="/chat">
|
||||||
className="flex flex-col"
|
|
||||||
href={
|
|
||||||
settings && settings.default_page === "chat"
|
|
||||||
? "/chat"
|
|
||||||
: "/search"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="max-w-[200px] w-full flex gap-x-1 my-auto">
|
<div className="max-w-[200px] w-full flex gap-x-1 my-auto">
|
||||||
<div className="flex-none mb-auto">
|
<div className="flex-none mb-auto">
|
||||||
<Logo />
|
<Logo />
|
||||||
@ -73,7 +66,7 @@ export function AdminSidebar({ collections }: { collections: Collection[] }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-center">
|
<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">
|
<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} />
|
<BackIcon className="my-auto" size={18} />
|
||||||
<p className="ml-1 break-words line-clamp-2 ellipsis leading-none">
|
<p className="ml-1 break-words line-clamp-2 ellipsis leading-none">
|
||||||
|
@ -13,7 +13,9 @@ export default function AssistantBanner({
|
|||||||
liveAssistant,
|
liveAssistant,
|
||||||
allAssistants,
|
allAssistants,
|
||||||
onAssistantChange,
|
onAssistantChange,
|
||||||
|
mobile = false,
|
||||||
}: {
|
}: {
|
||||||
|
mobile?: boolean;
|
||||||
recentAssistants: Persona[];
|
recentAssistants: Persona[];
|
||||||
liveAssistant: Persona | undefined;
|
liveAssistant: Persona | undefined;
|
||||||
allAssistants: Persona[];
|
allAssistants: Persona[];
|
||||||
@ -35,13 +37,15 @@ export default function AssistantBanner({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
// Take first 4
|
// Take first 4
|
||||||
.slice(0, 4)
|
.slice(0, mobile ? 2 : 4)
|
||||||
.map((assistant) => (
|
.map((assistant) => (
|
||||||
<TooltipProvider key={assistant.id}>
|
<TooltipProvider key={assistant.id}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div
|
<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)}
|
onClick={() => onAssistantChange(assistant)}
|
||||||
>
|
>
|
||||||
<AssistantIcon
|
<AssistantIcon
|
||||||
|
@ -1,77 +1,85 @@
|
|||||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||||
import { AssistantTools } from "@/app/assistants/ToolsDisplay";
|
|
||||||
import { Bubble } from "@/components/Bubble";
|
import { Bubble } from "@/components/Bubble";
|
||||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||||
import { getDisplayNameForModel } from "@/lib/hooks";
|
|
||||||
import { useSortable } from "@dnd-kit/sortable";
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
import React, { useState } from "react";
|
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 { MdDragIndicator } from "react-icons/md";
|
||||||
|
|
||||||
|
import { Badge } from "../ui/badge";
|
||||||
|
|
||||||
export const AssistantCard = ({
|
export const AssistantCard = ({
|
||||||
assistant,
|
assistant,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
llmName,
|
|
||||||
}: {
|
}: {
|
||||||
assistant: Persona;
|
assistant: Persona;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onSelect: (assistant: Persona) => void;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={() => onSelect(assistant)}
|
onClick={() => onSelect(assistant)}
|
||||||
className={`
|
className={`
|
||||||
p-4
|
flex flex-col overflow-hidden w-full rounded-xl px-3 py-4
|
||||||
cursor-pointer
|
cursor-pointer
|
||||||
border
|
${isSelected ? "bg-background-125" : "hover:bg-background-100"}
|
||||||
${isSelected ? "bg-hover" : "hover:bg-hover-light"}
|
|
||||||
shadow-md
|
|
||||||
rounded-lg
|
|
||||||
border-border
|
|
||||||
grow
|
|
||||||
flex items-center
|
|
||||||
overflow-hidden
|
|
||||||
`}
|
`}
|
||||||
onMouseEnter={() => setHovering(true)}
|
|
||||||
onMouseLeave={() => setHovering(false)}
|
|
||||||
>
|
>
|
||||||
<div className="w-full">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex items-center mb-2">
|
<AssistantIcon size="xs" assistant={assistant} />
|
||||||
<AssistantIcon assistant={assistant} />
|
<div className="overflow-hidden text-ellipsis break-words flex-grow">
|
||||||
<div className="ml-2 ellipsis truncate font-bold text-sm text-emphasis">
|
<div className="flex items-start justify-start gap-2">
|
||||||
{assistant.name}
|
<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>
|
<span className="line-clamp-2 text-xs text-text-700">
|
||||||
|
{assistant.description}
|
||||||
<div className="text-xs text-wrap text-subtle mb-2 mt-2 line-clamp-3 py-1">
|
</span>
|
||||||
{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} />
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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 { useRouter } from "next/navigation";
|
||||||
import { ChatBanner } from "@/app/chat/ChatBanner";
|
import { ChatBanner } from "@/app/chat/ChatBanner";
|
||||||
import LogoType from "../header/LogoType";
|
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({
|
export default function FunctionalHeader({
|
||||||
page,
|
page,
|
||||||
@ -20,13 +21,23 @@ export default function FunctionalHeader({
|
|||||||
toggleSidebar = () => null,
|
toggleSidebar = () => null,
|
||||||
reset = () => null,
|
reset = () => null,
|
||||||
sidebarToggled,
|
sidebarToggled,
|
||||||
|
liveAssistant,
|
||||||
|
onAssistantChange,
|
||||||
|
llmOverrideManager,
|
||||||
|
documentSidebarToggled,
|
||||||
|
toggleUserSettings,
|
||||||
}: {
|
}: {
|
||||||
reset?: () => void;
|
reset?: () => void;
|
||||||
page: pageType;
|
page: pageType;
|
||||||
sidebarToggled?: boolean;
|
sidebarToggled?: boolean;
|
||||||
|
documentSidebarToggled?: boolean;
|
||||||
currentChatSession?: ChatSession | null | undefined;
|
currentChatSession?: ChatSession | null | undefined;
|
||||||
setSharingModalVisible?: (value: SetStateAction<boolean>) => void;
|
setSharingModalVisible?: (value: SetStateAction<boolean>) => void;
|
||||||
toggleSidebar?: () => void;
|
toggleSidebar?: () => void;
|
||||||
|
liveAssistant?: Persona;
|
||||||
|
onAssistantChange?: (assistant: Persona) => void;
|
||||||
|
llmOverrideManager?: LlmOverrideManager;
|
||||||
|
toggleUserSettings?: () => void;
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
@ -63,14 +74,15 @@ export default function FunctionalHeader({
|
|||||||
router.push(newChatUrl);
|
router.push(newChatUrl);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="left-0 bg-transparent sticky top-0 z-20 w-full relative flex">
|
<div className="left-0 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="mt-2 cursor-pointer text-text-700 relative flex w-full">
|
||||||
<LogoType
|
<LogoType
|
||||||
assistantId={currentChatSession?.persona_id}
|
assistantId={currentChatSession?.persona_id}
|
||||||
page={page}
|
page={page}
|
||||||
toggleSidebar={toggleSidebar}
|
toggleSidebar={toggleSidebar}
|
||||||
handleNewChat={handleNewChat}
|
handleNewChat={handleNewChat}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{ transition: "width 0.30s ease-out" }}
|
style={{ transition: "width 0.30s ease-out" }}
|
||||||
className={`
|
className={`
|
||||||
@ -85,6 +97,7 @@ export default function FunctionalHeader({
|
|||||||
${sidebarToggled ? "w-[250px]" : "w-[0px]"}
|
${sidebarToggled ? "w-[250px]" : "w-[0px]"}
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="w-full mobile:-mx-20 desktop:px-4">
|
<div className="w-full mobile:-mx-20 desktop:px-4">
|
||||||
<ChatBanner />
|
<ChatBanner />
|
||||||
</div>
|
</div>
|
||||||
@ -108,7 +121,7 @@ export default function FunctionalHeader({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mobile:hidden flex my-auto">
|
<div className="mobile:hidden flex my-auto">
|
||||||
<UserDropdown />
|
<UserDropdown page={page} toggleUserSettings={toggleUserSettings} />
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
className="desktop:hidden my-auto"
|
className="desktop:hidden my-auto"
|
||||||
@ -124,12 +137,37 @@ export default function FunctionalHeader({
|
|||||||
<NewChatIcon size={20} />
|
<NewChatIcon size={20} />
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</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>
|
||||||
</div>
|
|
||||||
|
|
||||||
{page != "assistants" && (
|
{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" />
|
<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>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import React, { createContext, useContext, useState } from "react";
|
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 { ChatSession } from "@/app/chat/interfaces";
|
||||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||||
@ -12,6 +18,9 @@ import { personaComparator } from "@/app/admin/assistants/lib";
|
|||||||
interface ChatContextProps {
|
interface ChatContextProps {
|
||||||
chatSessions: ChatSession[];
|
chatSessions: ChatSession[];
|
||||||
availableSources: ValidSources[];
|
availableSources: ValidSources[];
|
||||||
|
ccPairs: CCPairBasicInfo[];
|
||||||
|
tags: Tag[];
|
||||||
|
documentSets: DocumentSet[];
|
||||||
availableDocumentSets: DocumentSet[];
|
availableDocumentSets: DocumentSet[];
|
||||||
availableTags: Tag[];
|
availableTags: Tag[];
|
||||||
llmProviders: LLMProviderDescriptor[];
|
llmProviders: LLMProviderDescriptor[];
|
||||||
|
@ -8,9 +8,9 @@ import { ChatSession } from "@/app/chat/interfaces";
|
|||||||
interface SearchContextProps {
|
interface SearchContextProps {
|
||||||
querySessions: ChatSession[];
|
querySessions: ChatSession[];
|
||||||
ccPairs: CCPairBasicInfo[];
|
ccPairs: CCPairBasicInfo[];
|
||||||
|
tags: Tag[];
|
||||||
documentSets: DocumentSet[];
|
documentSets: DocumentSet[];
|
||||||
assistants: Persona[];
|
assistants: Persona[];
|
||||||
tags: Tag[];
|
|
||||||
agenticSearchEnabled: boolean;
|
agenticSearchEnabled: boolean;
|
||||||
disabledAgentic: boolean;
|
disabledAgentic: boolean;
|
||||||
initiallyToggled: boolean;
|
initiallyToggled: boolean;
|
||||||
|
@ -81,7 +81,9 @@ import cohereIcon from "../../../public/Cohere.svg";
|
|||||||
import voyageIcon from "../../../public/Voyage.png";
|
import voyageIcon from "../../../public/Voyage.png";
|
||||||
import googleIcon from "../../../public/Google.webp";
|
import googleIcon from "../../../public/Google.webp";
|
||||||
import xenforoIcon from "../../../public/Xenforo.svg";
|
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 {
|
export interface IconProps {
|
||||||
size?: number;
|
size?: number;
|
||||||
@ -474,13 +476,6 @@ export const XSquareIcon = ({
|
|||||||
return <XSquare size={size} className={className} />;
|
return <XSquare size={size} className={className} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GlobeIcon = ({
|
|
||||||
size = 16,
|
|
||||||
className = defaultTailwindCSSBlue,
|
|
||||||
}: IconProps) => {
|
|
||||||
return <FiGlobe size={size} className={className} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FileIcon = ({
|
export const FileIcon = ({
|
||||||
size = 16,
|
size = 16,
|
||||||
className = defaultTailwindCSSBlue,
|
className = defaultTailwindCSSBlue,
|
||||||
@ -1034,9 +1029,16 @@ export const GithubIcon = ({
|
|||||||
size = 16,
|
size = 16,
|
||||||
className = defaultTailwindCSS,
|
className = defaultTailwindCSS,
|
||||||
}: IconProps) => (
|
}: 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 = ({
|
export const GmailIcon = ({
|
||||||
size = 16,
|
size = 16,
|
||||||
className = defaultTailwindCSS,
|
className = defaultTailwindCSS,
|
||||||
@ -2698,3 +2700,28 @@ export const DownloadCSVIcon = ({
|
|||||||
</svg>
|
</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 React from "react";
|
||||||
import { getDisplayNameForModel } from "@/lib/hooks";
|
import { getDisplayNameForModel } from "@/lib/hooks";
|
||||||
import { checkLLMSupportsImageInput, structureValue } from "@/lib/llm/utils";
|
import {
|
||||||
|
checkLLMSupportsImageInput,
|
||||||
|
destructureValue,
|
||||||
|
structureValue,
|
||||||
|
} from "@/lib/llm/utils";
|
||||||
import {
|
import {
|
||||||
getProviderIcon,
|
getProviderIcon,
|
||||||
LLMProviderDescriptor,
|
LLMProviderDescriptor,
|
||||||
} from "@/app/admin/configuration/llm/interfaces";
|
} from "@/app/admin/configuration/llm/interfaces";
|
||||||
|
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||||
|
|
||||||
interface LlmListProps {
|
interface LlmListProps {
|
||||||
llmProviders: LLMProviderDescriptor[];
|
llmProviders: LLMProviderDescriptor[];
|
||||||
@ -14,15 +19,19 @@ interface LlmListProps {
|
|||||||
scrollable?: boolean;
|
scrollable?: boolean;
|
||||||
hideProviderIcon?: boolean;
|
hideProviderIcon?: boolean;
|
||||||
requiresImageGeneration?: boolean;
|
requiresImageGeneration?: boolean;
|
||||||
|
includeUserDefault?: boolean;
|
||||||
|
currentAssistant?: Persona;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LlmList: React.FC<LlmListProps> = ({
|
export const LlmList: React.FC<LlmListProps> = ({
|
||||||
|
currentAssistant,
|
||||||
llmProviders,
|
llmProviders,
|
||||||
currentLlm,
|
currentLlm,
|
||||||
onSelect,
|
onSelect,
|
||||||
userDefault,
|
userDefault,
|
||||||
scrollable,
|
scrollable,
|
||||||
requiresImageGeneration,
|
requiresImageGeneration,
|
||||||
|
includeUserDefault = false,
|
||||||
}) => {
|
}) => {
|
||||||
const llmOptionsByProvider: {
|
const llmOptionsByProvider: {
|
||||||
[provider: string]: {
|
[provider: string]: {
|
||||||
@ -68,21 +77,6 @@ export const LlmList: React.FC<LlmListProps> = ({
|
|||||||
: "max-h-[300px]"
|
: "max-h-[300px]"
|
||||||
} bg-background-175 flex flex-col gap-y-1 overflow-y-scroll`}
|
} 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) => {
|
{llmOptions.map(({ name, icon, value }, index) => {
|
||||||
if (!requiresImageGeneration || checkLLMSupportsImageInput(name)) {
|
if (!requiresImageGeneration || checkLLMSupportsImageInput(name)) {
|
||||||
return (
|
return (
|
||||||
@ -98,6 +92,25 @@ export const LlmList: React.FC<LlmListProps> = ({
|
|||||||
>
|
>
|
||||||
{icon({ size: 16 })}
|
{icon({ size: 16 })}
|
||||||
{getDisplayNameForModel(name)}
|
{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>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
DanswerDocument,
|
DanswerDocument,
|
||||||
DocumentRelevance,
|
DocumentRelevance,
|
||||||
|
LoadedDanswerDocument,
|
||||||
SearchDanswerDocument,
|
SearchDanswerDocument,
|
||||||
} from "@/lib/search/interfaces";
|
} from "@/lib/search/interfaces";
|
||||||
import { DocumentFeedbackBlock } from "./DocumentFeedbackBlock";
|
import { DocumentFeedbackBlock } from "./DocumentFeedbackBlock";
|
||||||
@ -11,19 +12,21 @@ import { PopupSpec } from "../admin/connectors/Popup";
|
|||||||
import { DocumentUpdatedAtBadge } from "./DocumentUpdatedAtBadge";
|
import { DocumentUpdatedAtBadge } from "./DocumentUpdatedAtBadge";
|
||||||
import { SourceIcon } from "../SourceIcon";
|
import { SourceIcon } from "../SourceIcon";
|
||||||
import { MetadataBadge } from "../MetadataBadge";
|
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 { FaStar } from "react-icons/fa";
|
||||||
import { FiTag } from "react-icons/fi";
|
import { FiTag } from "react-icons/fi";
|
||||||
import { SettingsContext } from "../settings/SettingsProvider";
|
import { SettingsContext } from "../settings/SettingsProvider";
|
||||||
import { CustomTooltip, TooltipGroup } from "../tooltip/CustomTooltip";
|
import { CustomTooltip, TooltipGroup } from "../tooltip/CustomTooltip";
|
||||||
import { WarningCircle } from "@phosphor-icons/react";
|
import { WarningCircle } from "@phosphor-icons/react";
|
||||||
|
import { SearchResultIcon } from "../SearchResultIcon";
|
||||||
|
|
||||||
export const buildDocumentSummaryDisplay = (
|
export const buildDocumentSummaryDisplay = (
|
||||||
matchHighlights: string[],
|
matchHighlights: string[],
|
||||||
blurb: string
|
blurb: string
|
||||||
) => {
|
) => {
|
||||||
if (matchHighlights.length === 0) {
|
if (!matchHighlights || matchHighlights.length === 0) {
|
||||||
|
// console.log("no match highlights", matchHighlights);
|
||||||
return blurb;
|
return blurb;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,7 +254,11 @@ export const DocumentDisplay = ({
|
|||||||
>
|
>
|
||||||
<CustomTooltip showTick line content="Toggle content">
|
<CustomTooltip showTick line content="Toggle content">
|
||||||
<LightBulbIcon
|
<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>
|
</CustomTooltip>
|
||||||
</button>
|
</button>
|
||||||
@ -308,7 +315,9 @@ export const AgenticDocumentDisplay = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<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">
|
<div className="flex relative">
|
||||||
<a
|
<a
|
||||||
@ -380,3 +389,38 @@ export const AgenticDocumentDisplay = ({
|
|||||||
</div>
|
</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 { timeAgo } from "@/lib/time";
|
||||||
import { MetadataBadge } from "../MetadataBadge";
|
import { MetadataBadge } from "../MetadataBadge";
|
||||||
|
|
||||||
export function DocumentUpdatedAtBadge({ updatedAt }: { updatedAt: string }) {
|
export function DocumentUpdatedAtBadge({
|
||||||
return <MetadataBadge value={"Updated " + timeAgo(updatedAt)} />;
|
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;
|
showingSidebar: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import {
|
||||||
|
TooltipProvider,
|
||||||
|
Tooltip,
|
||||||
|
TooltipTrigger,
|
||||||
|
TooltipContent,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { SendIcon } from "../icons/icons";
|
import { SendIcon } from "../icons/icons";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
@ -28,61 +34,65 @@ import { CCPairBasicInfo, DocumentSet, Tag } from "@/lib/types";
|
|||||||
export const AnimatedToggle = ({
|
export const AnimatedToggle = ({
|
||||||
isOn,
|
isOn,
|
||||||
handleToggle,
|
handleToggle,
|
||||||
|
direction = "top",
|
||||||
}: {
|
}: {
|
||||||
isOn: boolean;
|
isOn: boolean;
|
||||||
handleToggle: () => void;
|
handleToggle: () => void;
|
||||||
|
direction?: "bottom" | "top";
|
||||||
}) => {
|
}) => {
|
||||||
const commandSymbol = KeyboardSymbol();
|
const commandSymbol = KeyboardSymbol();
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomTooltip
|
<TooltipProvider>
|
||||||
light
|
<Tooltip>
|
||||||
large
|
<TooltipTrigger asChild>
|
||||||
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 */}
|
|
||||||
<div
|
<div
|
||||||
className={`
|
ref={containerRef}
|
||||||
w-10 h-6 flex items-center rounded-full p-1 transition-all duration-300 ease-in-out
|
className="my-auto ml-auto flex justify-end items-center cursor-pointer"
|
||||||
${isOn ? "bg-toggled-background" : "bg-untoggled-background"}
|
onClick={handleToggle}
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
<div
|
<div ref={contentRef} className="flex items-center">
|
||||||
className={`
|
<div
|
||||||
bg-white w-4 h-4 rounded-full shadow-md transform transition-all duration-300 ease-in-out
|
className={`
|
||||||
${isOn ? "translate-x-4" : ""}
|
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>
|
`}
|
||||||
|
>
|
||||||
|
<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>
|
</div>
|
||||||
<p className="ml-2 text-sm">Agentic</p>
|
</TooltipTrigger>
|
||||||
</div>
|
<TooltipContent side={direction} backgroundColor="bg-background-200">
|
||||||
</div>
|
<div className="bg-white my-auto p-6 rounded-lg max-w-sm">
|
||||||
</CustomTooltip>
|
<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,
|
getDateRangeString,
|
||||||
getTimeAgoString,
|
getTimeAgoString,
|
||||||
} from "@/lib/dateUtils";
|
} from "@/lib/dateUtils";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
const SectionTitle = ({ children }: { children: string }) => (
|
const SectionTitle = ({ children }: { children: string }) => (
|
||||||
<div className="font-bold text-xs mt-2 flex">{children}</div>
|
<div className="font-bold text-xs mt-2 flex">{children}</div>
|
||||||
@ -53,6 +54,9 @@ export interface SourceSelectorProps {
|
|||||||
availableDocumentSets: DocumentSet[];
|
availableDocumentSets: DocumentSet[];
|
||||||
existingSources: ValidSources[];
|
existingSources: ValidSources[];
|
||||||
availableTags: Tag[];
|
availableTags: Tag[];
|
||||||
|
toggleFilters?: () => void;
|
||||||
|
filtersUntoggled?: boolean;
|
||||||
|
tagsOnLeft?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SourceSelector({
|
export function SourceSelector({
|
||||||
@ -68,6 +72,9 @@ export function SourceSelector({
|
|||||||
existingSources,
|
existingSources,
|
||||||
availableTags,
|
availableTags,
|
||||||
showDocSidebar,
|
showDocSidebar,
|
||||||
|
toggleFilters,
|
||||||
|
filtersUntoggled,
|
||||||
|
tagsOnLeft,
|
||||||
}: SourceSelectorProps) {
|
}: SourceSelectorProps) {
|
||||||
const handleSelect = (source: SourceMetadata) => {
|
const handleSelect = (source: SourceMetadata) => {
|
||||||
setSelectedSources((prev: SourceMetadata[]) => {
|
setSelectedSources((prev: SourceMetadata[]) => {
|
||||||
@ -110,138 +117,155 @@ export function SourceSelector({
|
|||||||
showDocSidebar ? "4xl:block" : "!block"
|
showDocSidebar ? "4xl:block" : "!block"
|
||||||
} duration-1000 flex ease-out transition-all transform origin-top-right`}
|
} 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>
|
<h2 className="font-bold my-auto">Filters</h2>
|
||||||
<FiFilter className="my-auto ml-2" size="16" />
|
<FiFilter className="my-auto ml-2" size="16" />
|
||||||
</div>
|
</button>
|
||||||
|
{!filtersUntoggled && (
|
||||||
<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 && (
|
|
||||||
<>
|
<>
|
||||||
<div className="mt-4 mb-2">
|
<Separator />
|
||||||
<SectionTitle>Tags</SectionTitle>
|
<Popover>
|
||||||
</div>
|
<PopoverTrigger asChild>
|
||||||
<TagFilter
|
<div className="cursor-pointer">
|
||||||
tags={availableTags}
|
<SectionTitle>Time Range</SectionTitle>
|
||||||
selectedTags={selectedTags}
|
<p className="text-sm text-default mt-2">
|
||||||
setSelectedTags={setSelectedTags}
|
{getDateRangeString(timeRange?.from!, timeRange?.to!) ||
|
||||||
/>
|
"Select a time range"}
|
||||||
</>
|
</p>
|
||||||
)}
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
{existingSources.length > 0 && (
|
<PopoverContent
|
||||||
<div className="mt-4">
|
className="bg-background-search-filter border-border border rounded-md z-[200] p-0"
|
||||||
<div className="flex w-full gap-x-2 items-center">
|
align="start"
|
||||||
<div className="font-bold text-xs mt-2 flex items-center gap-x-2">
|
>
|
||||||
<p>Sources</p>
|
<Calendar
|
||||||
<input
|
mode="range"
|
||||||
type="checkbox"
|
selected={
|
||||||
checked={allSourcesSelected}
|
timeRange
|
||||||
onChange={toggleAllSources}
|
? {
|
||||||
className="my-auto form-checkbox h-3 w-3 text-primary border-background-900 rounded"
|
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>
|
</PopoverContent>
|
||||||
</div>
|
</Popover>
|
||||||
<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 && (
|
{availableTags.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="mt-4">
|
<div className="mt-4 mb-2">
|
||||||
<SectionTitle>Knowledge Sets</SectionTitle>
|
<SectionTitle>Tags</SectionTitle>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-1">
|
<TagFilter
|
||||||
{availableDocumentSets.map((documentSet) => (
|
showTagsOnLeft={true}
|
||||||
<div key={documentSet.name} className="my-1.5 flex">
|
tags={availableTags}
|
||||||
<div
|
selectedTags={selectedTags}
|
||||||
key={documentSet.name}
|
setSelectedTags={setSelectedTags}
|
||||||
className={
|
/>
|
||||||
"flex cursor-pointer w-full items-center " +
|
</>
|
||||||
"py-1.5 rounded-lg px-2 " +
|
)}
|
||||||
(selectedDocumentSets.includes(documentSet.name)
|
|
||||||
? "bg-hover"
|
{existingSources.length > 0 && (
|
||||||
: "hover:bg-hover-light")
|
<div className="mt-4">
|
||||||
}
|
<div className="flex w-full gap-x-2 items-center">
|
||||||
onClick={() => handleDocumentSetSelect(documentSet.name)}
|
<div className="font-bold text-xs mt-2 flex items-center gap-x-2">
|
||||||
>
|
<p>Sources</p>
|
||||||
<HoverPopup
|
<input
|
||||||
mainContent={
|
type="checkbox"
|
||||||
<div className="flex my-auto mr-2">
|
checked={allSourcesSelected}
|
||||||
<InfoIcon className={defaultTailwindCSS} />
|
onChange={toggleAllSources}
|
||||||
</div>
|
className="my-auto form-checkbox h-3 w-3 text-primary border-background-900 rounded"
|
||||||
}
|
|
||||||
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>
|
||||||
))}
|
<div className="px-1">
|
||||||
</div>
|
{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>
|
</div>
|
||||||
|
@ -6,13 +6,17 @@ import debounce from "lodash/debounce";
|
|||||||
import { getValidTags } from "@/lib/tags/tagUtils";
|
import { getValidTags } from "@/lib/tags/tagUtils";
|
||||||
|
|
||||||
export function TagFilter({
|
export function TagFilter({
|
||||||
|
modal,
|
||||||
tags,
|
tags,
|
||||||
selectedTags,
|
selectedTags,
|
||||||
setSelectedTags,
|
setSelectedTags,
|
||||||
|
showTagsOnLeft = false,
|
||||||
}: {
|
}: {
|
||||||
|
modal?: boolean;
|
||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
selectedTags: Tag[];
|
selectedTags: Tag[];
|
||||||
setSelectedTags: React.Dispatch<React.SetStateAction<Tag[]>>;
|
setSelectedTags: React.Dispatch<React.SetStateAction<Tag[]>>;
|
||||||
|
showTagsOnLeft?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [filterValue, setFilterValue] = useState("");
|
const [filterValue, setFilterValue] = useState("");
|
||||||
const [tagOptionsAreVisible, setTagOptionsAreVisible] = useState(false);
|
const [tagOptionsAreVisible, setTagOptionsAreVisible] = useState(false);
|
||||||
@ -72,10 +76,12 @@ export function TagFilter({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative w-full ">
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
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"
|
placeholder="Find a tag"
|
||||||
value={filterValue}
|
value={filterValue}
|
||||||
onChange={handleFilterChange}
|
onChange={handleFilterChange}
|
||||||
@ -106,7 +112,13 @@ export function TagFilter({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{tagOptionsAreVisible && (
|
{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
|
<div
|
||||||
ref={popupRef}
|
ref={popupRef}
|
||||||
className="p-2 border border-border rounded shadow-lg w-72 bg-background"
|
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 { 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({
|
export function Citation({
|
||||||
children,
|
children,
|
||||||
link,
|
link,
|
||||||
|
document,
|
||||||
index,
|
index,
|
||||||
|
icon,
|
||||||
|
url,
|
||||||
}: {
|
}: {
|
||||||
link?: string;
|
link?: string;
|
||||||
children?: JSX.Element | string | null | ReactNode;
|
children?: JSX.Element | string | null | ReactNode;
|
||||||
index?: number;
|
index?: number;
|
||||||
|
document: LoadedDanswerDocument;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
url?: string;
|
||||||
}) {
|
}) {
|
||||||
const innerText = children
|
const innerText = children
|
||||||
? children?.toString().split("[")[1].split("]")[0]
|
? children?.toString().split("[")[1].split("]")[0]
|
||||||
: index;
|
: index;
|
||||||
|
|
||||||
if (link != "") {
|
if (link) {
|
||||||
return (
|
return (
|
||||||
<CustomTooltip
|
<TooltipProvider delayDuration={0}>
|
||||||
citation
|
<Tooltip>
|
||||||
content={<div className="inline-block p-0 m-0 truncate">{link}</div>}
|
<TooltipTrigger asChild>
|
||||||
>
|
<div
|
||||||
<a
|
onMouseDown={() => window.open(link, "_blank")}
|
||||||
onMouseDown={() => (link ? window.open(link, "_blank") : undefined)}
|
className="inline-flex items-center ml-1 cursor-pointer transition-all duration-200 ease-in-out"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
{innerText}
|
<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">
|
||||||
</span>
|
{innerText}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</div>
|
||||||
</CustomTooltip>
|
</TooltipTrigger>
|
||||||
|
<TooltipContent width="mb-2 max-w-lg" className="bg-background">
|
||||||
|
<CompactDocumentCard url={url} icon={icon} document={document} />
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<CustomTooltip content={<div>This doc doesn't have a link!</div>}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<div className="inline-block cursor-help leading-none inline ml-1 align-middle">
|
<Tooltip>
|
||||||
<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">
|
<TooltipTrigger asChild>
|
||||||
<span
|
<div
|
||||||
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"
|
onMouseDown={() => window.open(link, "_blank")}
|
||||||
data-number="3"
|
className="inline-flex items-center ml-1 cursor-pointer transition-all duration-200 ease-in-out"
|
||||||
>
|
>
|
||||||
{innerText}
|
<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">
|
||||||
</span>
|
{innerText}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CustomTooltip>
|
</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,
|
TriangleAlertIcon,
|
||||||
} from "@/components/icons/icons";
|
} from "@/components/icons/icons";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Grid } from "react-loader-spinner";
|
|
||||||
import { searchState } from "../SearchSection";
|
|
||||||
|
|
||||||
export type StatusOptions = "in-progress" | "failed" | "warning" | "success";
|
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].ok) {
|
||||||
if (results[0].status === 403 || results[0].status === 401) {
|
if (results[0].status === 403 || results[0].status === 401) {
|
||||||
settings = {
|
settings = {
|
||||||
|
auto_scroll: true,
|
||||||
product_gating: GatingType.NONE,
|
product_gating: GatingType.NONE,
|
||||||
gpu_enabled: false,
|
gpu_enabled: false,
|
||||||
chat_page_enabled: true,
|
|
||||||
search_page_enabled: true,
|
|
||||||
default_page: "search",
|
|
||||||
maximum_chat_retention_days: null,
|
maximum_chat_retention_days: null,
|
||||||
notifications: [],
|
notifications: [],
|
||||||
needs_reindexing: false,
|
needs_reindexing: false,
|
||||||
|
@ -131,7 +131,7 @@ export const CustomTooltip = ({
|
|||||||
transform -translate-x-1/2 text-sm
|
transform -translate-x-1/2 text-sm
|
||||||
${
|
${
|
||||||
light
|
light
|
||||||
? "text-gray-800 bg-background-200"
|
? "text-text-800 bg-background-200"
|
||||||
: "text-white bg-background-800"
|
: "text-white bg-background-800"
|
||||||
}
|
}
|
||||||
rounded-lg shadow-lg`}
|
rounded-lg shadow-lg`}
|
||||||
|
@ -50,12 +50,13 @@ function Badge({
|
|||||||
...props
|
...props
|
||||||
}: BadgeProps & {
|
}: BadgeProps & {
|
||||||
icon?: React.ElementType;
|
icon?: React.ElementType;
|
||||||
size?: "sm" | "md";
|
size?: "sm" | "md" | "xs";
|
||||||
circle?: boolean;
|
circle?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: "px-2.5 py-0.5 text-xs",
|
sm: "px-2.5 py-0.5 text-xs",
|
||||||
md: "px-3 py-1 text-sm",
|
md: "px-3 py-1 text-sm",
|
||||||
|
xs: "px-1.5 py-0.25 text-[.5rem]", // Made xs smaller
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -64,10 +65,20 @@ function Badge({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{Icon && (
|
{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 && (
|
{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}
|
{props.children}
|
||||||
</div>
|
</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<
|
const TooltipContent = React.forwardRef<
|
||||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & {
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & {
|
||||||
maxWidth?: string;
|
width?: string;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
}
|
}
|
||||||
>(({ className, sideOffset = 4, maxWidth, backgroundColor, ...props }, ref) => (
|
>(({ className, sideOffset = 4, width, backgroundColor, ...props }, ref) => (
|
||||||
<TooltipPrimitive.Content
|
<TooltipPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
`z-50 overflow-hidden rounded-md border border-neutral-200 text-white ${
|
`z-50 overflow-hidden rounded-md border border-neutral-200 text-white ${
|
||||||
backgroundColor || "bg-background-900"
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -12,6 +12,7 @@ interface UserContextType {
|
|||||||
isCurator: boolean;
|
isCurator: boolean;
|
||||||
refreshUser: () => Promise<void>;
|
refreshUser: () => Promise<void>;
|
||||||
isCloudSuperuser: boolean;
|
isCloudSuperuser: boolean;
|
||||||
|
updateUserAutoScroll: (autoScroll: boolean | null) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserContext = createContext<UserContextType | undefined>(undefined);
|
const UserContext = createContext<UserContextType | undefined>(undefined);
|
||||||
@ -55,6 +56,36 @@ export function UserProvider({
|
|||||||
setIsLoadingUser(false);
|
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 () => {
|
const refreshUser = async () => {
|
||||||
await fetchUser();
|
await fetchUser();
|
||||||
@ -66,6 +97,7 @@ export function UserProvider({
|
|||||||
user: upToDateUser,
|
user: upToDateUser,
|
||||||
isLoadingUser,
|
isLoadingUser,
|
||||||
refreshUser,
|
refreshUser,
|
||||||
|
updateUserAutoScroll,
|
||||||
isAdmin: upToDateUser?.role === UserRole.ADMIN,
|
isAdmin: upToDateUser?.role === UserRole.ADMIN,
|
||||||
// Curator status applies for either global or basic curator
|
// Curator status applies for either global or basic curator
|
||||||
isCurator:
|
isCurator:
|
||||||
|
@ -19,6 +19,10 @@ export interface AnswerPiecePacket {
|
|||||||
answer_piece: string;
|
answer_piece: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FinalContextDocs {
|
||||||
|
final_context_docs: DanswerDocument[];
|
||||||
|
}
|
||||||
|
|
||||||
export enum StreamStopReason {
|
export enum StreamStopReason {
|
||||||
CONTEXT_LENGTH = "CONTEXT_LENGTH",
|
CONTEXT_LENGTH = "CONTEXT_LENGTH",
|
||||||
CANCELLED = "CANCELLED",
|
CANCELLED = "CANCELLED",
|
||||||
@ -62,6 +66,9 @@ export interface DanswerDocument {
|
|||||||
is_internet: boolean;
|
is_internet: boolean;
|
||||||
validationState?: null | "good" | "bad";
|
validationState?: null | "good" | "bad";
|
||||||
}
|
}
|
||||||
|
export interface LoadedDanswerDocument extends DanswerDocument {
|
||||||
|
icon: React.FC<{ size?: number; className?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SearchDanswerDocument extends DanswerDocument {
|
export interface SearchDanswerDocument extends DanswerDocument {
|
||||||
is_relevant: boolean;
|
is_relevant: boolean;
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
DanswerDocument,
|
DanswerDocument,
|
||||||
DocumentInfoPacket,
|
DocumentInfoPacket,
|
||||||
ErrorMessagePacket,
|
ErrorMessagePacket,
|
||||||
|
FinalContextDocs,
|
||||||
Quote,
|
Quote,
|
||||||
QuotesInfoPacket,
|
QuotesInfoPacket,
|
||||||
RelevanceChunk,
|
RelevanceChunk,
|
||||||
@ -91,6 +92,7 @@ export const searchRequestStreamed = async ({
|
|||||||
| DocumentInfoPacket
|
| DocumentInfoPacket
|
||||||
| LLMRelevanceFilterPacket
|
| LLMRelevanceFilterPacket
|
||||||
| BackendMessage
|
| BackendMessage
|
||||||
|
| FinalContextDocs
|
||||||
| RelevanceChunk
|
| RelevanceChunk
|
||||||
>(decoder.decode(value, { stream: true }), previousPartialChunk);
|
>(decoder.decode(value, { stream: true }), previousPartialChunk);
|
||||||
if (!completedChunks.length && !partialChunk) {
|
if (!completedChunks.length && !partialChunk) {
|
||||||
|
@ -58,7 +58,7 @@ type SourceMap = {
|
|||||||
[K in ValidSources]: PartialSourceMetadata;
|
[K in ValidSources]: PartialSourceMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SOURCE_METADATA_MAP: SourceMap = {
|
export const SOURCE_METADATA_MAP: SourceMap = {
|
||||||
web: {
|
web: {
|
||||||
icon: GlobeIcon,
|
icon: GlobeIcon,
|
||||||
displayName: "Web",
|
displayName: "Web",
|
||||||
|
@ -9,6 +9,7 @@ interface UserPreferences {
|
|||||||
hidden_assistants: number[];
|
hidden_assistants: number[];
|
||||||
default_model: string | null;
|
default_model: string | null;
|
||||||
recent_assistants: number[];
|
recent_assistants: number[];
|
||||||
|
auto_scroll: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UserStatus {
|
export enum UserStatus {
|
||||||
|
@ -336,6 +336,7 @@ module.exports = {
|
|||||||
"tremor-full": "9999px",
|
"tremor-full": "9999px",
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
|
"2xs": "0.625rem",
|
||||||
"code-sm": "small",
|
"code-sm": "small",
|
||||||
"tremor-label": ["0.75rem"],
|
"tremor-label": ["0.75rem"],
|
||||||
"tremor-default": ["0.875rem", { lineHeight: "1.25rem" }],
|
"tremor-default": ["0.875rem", { lineHeight: "1.25rem" }],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user