mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-17 07:45:47 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d38e8cf595 | ||
|
|
1f1ab566ec | ||
|
|
b7ece296e0 | ||
|
|
d7063e0a1d | ||
|
|
ee073f6d30 | ||
|
|
2e524816a0 | ||
|
|
47ef0c8658 | ||
|
|
806de92feb | ||
|
|
da39f32fea | ||
|
|
2a87837ce1 | ||
|
|
7491cdd0f0 | ||
|
|
aabd698295 | ||
|
|
4b725e4d1a | ||
|
|
34d2d92fa8 | ||
|
|
3a3b2a2f8d |
@@ -1,52 +0,0 @@
|
||||
"""max_length_for_instruction_system_prompt
|
||||
|
||||
Revision ID: e995bdf0d6f7
|
||||
Revises: 8e1ac4f39a9f
|
||||
Create Date: 2025-04-01 18:32:45.123456
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "e995bdf0d6f7"
|
||||
down_revision = "8e1ac4f39a9f"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Alter system_prompt and task_prompt columns to have a maximum length of 8000 characters
|
||||
op.alter_column(
|
||||
"prompt",
|
||||
"system_prompt",
|
||||
existing_type=sa.Text(),
|
||||
type_=sa.String(8000),
|
||||
existing_nullable=False,
|
||||
)
|
||||
op.alter_column(
|
||||
"prompt",
|
||||
"task_prompt",
|
||||
existing_type=sa.Text(),
|
||||
type_=sa.String(8000),
|
||||
existing_nullable=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Revert system_prompt and task_prompt columns back to Text type
|
||||
op.alter_column(
|
||||
"prompt",
|
||||
"system_prompt",
|
||||
existing_type=sa.String(8000),
|
||||
type_=sa.Text(),
|
||||
existing_nullable=False,
|
||||
)
|
||||
op.alter_column(
|
||||
"prompt",
|
||||
"task_prompt",
|
||||
existing_type=sa.String(8000),
|
||||
type_=sa.Text(),
|
||||
existing_nullable=False,
|
||||
)
|
||||
@@ -58,6 +58,7 @@ def _get_objects_access_for_user_email_from_salesforce(
|
||||
f"Time taken to get Salesforce user ID: {end_time - start_time} seconds"
|
||||
)
|
||||
if user_id is None:
|
||||
logger.warning(f"User '{user_email}' not found in Salesforce")
|
||||
return None
|
||||
|
||||
# This is the only query that is not cached in the function
|
||||
@@ -65,6 +66,7 @@ def _get_objects_access_for_user_email_from_salesforce(
|
||||
object_id_to_access = get_objects_access_for_user_id(
|
||||
salesforce_client, user_id, list(object_ids)
|
||||
)
|
||||
logger.debug(f"Object ID to access: {object_id_to_access}")
|
||||
return object_id_to_access
|
||||
|
||||
|
||||
|
||||
@@ -36,9 +36,6 @@ from onyx.utils.logger import setup_logger
|
||||
logger = setup_logger()
|
||||
router = APIRouter(prefix="/auth/saml")
|
||||
|
||||
# Define non-authenticated user roles that should be re-created during SAML login
|
||||
NON_AUTHENTICATED_ROLES = {UserRole.SLACK_USER, UserRole.EXT_PERM_USER}
|
||||
|
||||
|
||||
async def upsert_saml_user(email: str) -> User:
|
||||
logger.debug(f"Attempting to upsert SAML user with email: {email}")
|
||||
@@ -54,7 +51,7 @@ async def upsert_saml_user(email: str) -> User:
|
||||
try:
|
||||
user = await user_manager.get_by_email(email)
|
||||
# If user has a non-authenticated role, treat as non-existent
|
||||
if user.role in NON_AUTHENTICATED_ROLES:
|
||||
if not user.role.is_web_login():
|
||||
raise exceptions.UserNotExists()
|
||||
return user
|
||||
except exceptions.UserNotExists:
|
||||
|
||||
@@ -23,6 +23,7 @@ from onyx.utils.url import add_url_params
|
||||
from onyx.utils.variable_functionality import fetch_versioned_implementation
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
|
||||
|
||||
HTML_EMAIL_TEMPLATE = """\
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
@@ -56,6 +56,7 @@ from httpx_oauth.oauth2 import OAuth2Token
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ee.onyx.configs.app_configs import ANONYMOUS_USER_COOKIE_NAME
|
||||
from onyx.auth.api_key import get_hashed_api_key_from_request
|
||||
from onyx.auth.email_utils import send_forgot_password_email
|
||||
from onyx.auth.email_utils import send_user_verification_email
|
||||
@@ -513,6 +514,25 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
|
||||
return user
|
||||
|
||||
async def on_after_login(
|
||||
self,
|
||||
user: User,
|
||||
request: Optional[Request] = None,
|
||||
response: Optional[Response] = None,
|
||||
) -> None:
|
||||
try:
|
||||
if response and request and ANONYMOUS_USER_COOKIE_NAME in request.cookies:
|
||||
response.delete_cookie(
|
||||
ANONYMOUS_USER_COOKIE_NAME,
|
||||
# Ensure cookie deletion doesn't override other cookies by setting the same path/domain
|
||||
path="/",
|
||||
domain=None,
|
||||
secure=WEB_DOMAIN.startswith("https"),
|
||||
)
|
||||
logger.debug(f"Deleted anonymous user cookie for user {user.email}")
|
||||
except Exception:
|
||||
logger.exception("Error deleting anonymous user cookie")
|
||||
|
||||
async def on_after_register(
|
||||
self, user: User, request: Optional[Request] = None
|
||||
) -> None:
|
||||
@@ -1302,6 +1322,7 @@ def get_oauth_router(
|
||||
# Login user
|
||||
response = await backend.login(strategy, user)
|
||||
await user_manager.on_after_login(user, request, response)
|
||||
|
||||
# Prepare redirect response
|
||||
if tenant_id is None:
|
||||
# Use URL utility to add parameters
|
||||
@@ -1311,9 +1332,14 @@ def get_oauth_router(
|
||||
# No parameters to add
|
||||
redirect_response = RedirectResponse(next_url, status_code=302)
|
||||
|
||||
# Copy headers and other attributes from 'response' to 'redirect_response'
|
||||
# Copy headers from auth response to redirect response, with special handling for Set-Cookie
|
||||
for header_name, header_value in response.headers.items():
|
||||
redirect_response.headers[header_name] = header_value
|
||||
# FastAPI can have multiple Set-Cookie headers as a list
|
||||
if header_name.lower() == "set-cookie" and isinstance(header_value, list):
|
||||
for cookie_value in header_value:
|
||||
redirect_response.headers.append(header_name, cookie_value)
|
||||
else:
|
||||
redirect_response.headers[header_name] = header_value
|
||||
|
||||
if hasattr(response, "body"):
|
||||
redirect_response.body = response.body
|
||||
|
||||
@@ -886,11 +886,8 @@ def monitor_ccpair_permissions_taskset(
|
||||
record_type=RecordType.PERMISSION_SYNC_PROGRESS,
|
||||
data={
|
||||
"cc_pair_id": cc_pair_id,
|
||||
"id": payload.id if payload else None,
|
||||
"total_docs": initial if initial is not None else 0,
|
||||
"remaining_docs": remaining,
|
||||
"synced_docs": (initial - remaining) if initial is not None else 0,
|
||||
"is_complete": remaining == 0,
|
||||
"total_docs_synced": initial if initial is not None else 0,
|
||||
"remaining_docs_to_sync": remaining,
|
||||
},
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
@@ -906,6 +903,13 @@ def monitor_ccpair_permissions_taskset(
|
||||
f"num_synced={initial}"
|
||||
)
|
||||
|
||||
# Add telemetry for permission syncing complete
|
||||
optional_telemetry(
|
||||
record_type=RecordType.PERMISSION_SYNC_COMPLETE,
|
||||
data={"cc_pair_id": cc_pair_id},
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
|
||||
update_sync_record_status(
|
||||
db_session=db_session,
|
||||
entity_id=cc_pair_id,
|
||||
|
||||
@@ -56,7 +56,6 @@ from onyx.indexing.indexing_pipeline import build_indexing_pipeline
|
||||
from onyx.natural_language_processing.search_nlp_models import (
|
||||
InformationContentClassificationModel,
|
||||
)
|
||||
from onyx.redis.redis_connector import RedisConnector
|
||||
from onyx.utils.logger import setup_logger
|
||||
from onyx.utils.logger import TaskAttemptSingleton
|
||||
from onyx.utils.telemetry import create_milestone_and_report
|
||||
@@ -578,11 +577,8 @@ def _run_indexing(
|
||||
data={
|
||||
"index_attempt_id": index_attempt_id,
|
||||
"cc_pair_id": ctx.cc_pair_id,
|
||||
"connector_id": ctx.connector_id,
|
||||
"credential_id": ctx.credential_id,
|
||||
"total_docs_indexed": document_count,
|
||||
"total_chunks": chunk_count,
|
||||
"batch_num": batch_num,
|
||||
"current_docs_indexed": document_count,
|
||||
"current_chunks_indexed": chunk_count,
|
||||
"source": ctx.source.value,
|
||||
},
|
||||
tenant_id=tenant_id,
|
||||
@@ -603,26 +599,15 @@ def _run_indexing(
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
# Add telemetry for completed indexing
|
||||
redis_connector = RedisConnector(tenant_id, ctx.cc_pair_id)
|
||||
redis_connector_index = redis_connector.new_index(
|
||||
index_attempt_start.search_settings_id
|
||||
)
|
||||
final_progress = redis_connector_index.get_progress() or 0
|
||||
|
||||
optional_telemetry(
|
||||
record_type=RecordType.INDEXING_COMPLETE,
|
||||
data={
|
||||
"index_attempt_id": index_attempt_id,
|
||||
"cc_pair_id": ctx.cc_pair_id,
|
||||
"connector_id": ctx.connector_id,
|
||||
"credential_id": ctx.credential_id,
|
||||
"total_docs_indexed": document_count,
|
||||
"total_chunks": chunk_count,
|
||||
"batch_count": batch_num,
|
||||
"time_elapsed_seconds": time.monotonic() - start_time,
|
||||
"source": ctx.source.value,
|
||||
"redis_progress": final_progress,
|
||||
},
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
|
||||
@@ -25,6 +25,8 @@ class SalesforceObject:
|
||||
)
|
||||
|
||||
|
||||
# te
|
||||
|
||||
# This defines the base path for all data files relative to this file
|
||||
# AKA BE CAREFUL WHEN MOVING THIS FILE
|
||||
BASE_DATA_PATH = os.path.join(os.path.dirname(__file__), "data")
|
||||
|
||||
@@ -217,7 +217,6 @@ def mark_attempt_in_progress(
|
||||
"index_attempt_id": index_attempt.id,
|
||||
"status": IndexingStatus.IN_PROGRESS.value,
|
||||
"cc_pair_id": index_attempt.connector_credential_pair_id,
|
||||
"search_settings_id": index_attempt.search_settings_id,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
@@ -246,9 +245,6 @@ def mark_attempt_succeeded(
|
||||
"index_attempt_id": index_attempt_id,
|
||||
"status": IndexingStatus.SUCCESS.value,
|
||||
"cc_pair_id": attempt.connector_credential_pair_id,
|
||||
"search_settings_id": attempt.search_settings_id,
|
||||
"total_docs_indexed": attempt.total_docs_indexed,
|
||||
"new_docs_indexed": attempt.new_docs_indexed,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
@@ -277,9 +273,6 @@ def mark_attempt_partially_succeeded(
|
||||
"index_attempt_id": index_attempt_id,
|
||||
"status": IndexingStatus.COMPLETED_WITH_ERRORS.value,
|
||||
"cc_pair_id": attempt.connector_credential_pair_id,
|
||||
"search_settings_id": attempt.search_settings_id,
|
||||
"total_docs_indexed": attempt.total_docs_indexed,
|
||||
"new_docs_indexed": attempt.new_docs_indexed,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
@@ -312,10 +305,6 @@ def mark_attempt_canceled(
|
||||
"index_attempt_id": index_attempt_id,
|
||||
"status": IndexingStatus.CANCELED.value,
|
||||
"cc_pair_id": attempt.connector_credential_pair_id,
|
||||
"search_settings_id": attempt.search_settings_id,
|
||||
"reason": reason,
|
||||
"total_docs_indexed": attempt.total_docs_indexed,
|
||||
"new_docs_indexed": attempt.new_docs_indexed,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
@@ -350,10 +339,6 @@ def mark_attempt_failed(
|
||||
"index_attempt_id": index_attempt_id,
|
||||
"status": IndexingStatus.FAILED.value,
|
||||
"cc_pair_id": attempt.connector_credential_pair_id,
|
||||
"search_settings_id": attempt.search_settings_id,
|
||||
"reason": failure_reason,
|
||||
"total_docs_indexed": attempt.total_docs_indexed,
|
||||
"new_docs_indexed": attempt.new_docs_indexed,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
|
||||
@@ -5,6 +5,7 @@ from datetime import timezone
|
||||
from onyx.configs.constants import INDEX_SEPARATOR
|
||||
from onyx.context.search.models import IndexFilters
|
||||
from onyx.document_index.interfaces import VespaChunkRequest
|
||||
from onyx.document_index.vespa_constants import ACCESS_CONTROL_LIST
|
||||
from onyx.document_index.vespa_constants import CHUNK_ID
|
||||
from onyx.document_index.vespa_constants import DOC_UPDATED_AT
|
||||
from onyx.document_index.vespa_constants import DOCUMENT_ID
|
||||
@@ -74,8 +75,10 @@ def build_vespa_filters(
|
||||
filter_str += f'({TENANT_ID} contains "{filters.tenant_id}") and '
|
||||
|
||||
# ACL filters
|
||||
# if filters.access_control_list is not None:
|
||||
# filter_str += _build_or_filters(ACCESS_CONTROL_LIST, filters.access_control_list)
|
||||
if filters.access_control_list is not None:
|
||||
filter_str += _build_or_filters(
|
||||
ACCESS_CONTROL_LIST, filters.access_control_list
|
||||
)
|
||||
|
||||
# Source type filters
|
||||
source_strs = (
|
||||
|
||||
@@ -39,6 +39,7 @@ class RecordType(str, Enum):
|
||||
INDEXING_PROGRESS = "indexing_progress"
|
||||
INDEXING_COMPLETE = "indexing_complete"
|
||||
PERMISSION_SYNC_PROGRESS = "permission_sync_progress"
|
||||
PERMISSION_SYNC_COMPLETE = "permission_sync_complete"
|
||||
INDEX_ATTEMPT_STATUS = "index_attempt_status"
|
||||
|
||||
|
||||
|
||||
@@ -887,6 +887,7 @@ def main() -> None:
|
||||
type=int,
|
||||
help="Maximum number of documents to delete (for delete-all-documents)",
|
||||
)
|
||||
parser.add_argument("--link", help="Document link (for get_acls filter)")
|
||||
|
||||
args = parser.parse_args()
|
||||
vespa_debug = VespaDebugging(args.tenant_id)
|
||||
@@ -924,7 +925,11 @@ def main() -> None:
|
||||
elif args.action == "get_acls":
|
||||
if args.cc_pair_id is None:
|
||||
parser.error("--cc-pair-id is required for get_acls action")
|
||||
vespa_debug.acls(args.cc_pair_id, args.n)
|
||||
|
||||
if args.link is None:
|
||||
vespa_debug.acls(args.cc_pair_id, args.n)
|
||||
else:
|
||||
vespa_debug.acls_by_link(args.cc_pair_id, args.link)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -313,3 +313,29 @@ class UserManager:
|
||||
)
|
||||
response.raise_for_status()
|
||||
return UserInfo(**response.json())
|
||||
|
||||
@staticmethod
|
||||
def invite_users(
|
||||
user_performing_action: DATestUser,
|
||||
emails: list[str],
|
||||
) -> int:
|
||||
response = requests.put(
|
||||
url=f"{API_SERVER_URL}/manage/admin/users",
|
||||
json={"emails": emails},
|
||||
headers=user_performing_action.headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
@staticmethod
|
||||
def remove_invited_user(
|
||||
user_performing_action: DATestUser,
|
||||
user_email: str,
|
||||
) -> int:
|
||||
response = requests.patch(
|
||||
url=f"{API_SERVER_URL}/manage/admin/remove-invited-user",
|
||||
json={"user_email": user_email},
|
||||
headers=user_performing_action.headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
@@ -22,6 +22,7 @@ from onyx.document_index.document_index_utils import get_multipass_config
|
||||
from onyx.document_index.vespa.index import DOCUMENT_ID_ENDPOINT
|
||||
from onyx.document_index.vespa.index import VespaIndex
|
||||
from onyx.indexing.models import IndexingSetting
|
||||
from onyx.redis.redis_pool import get_redis_client
|
||||
from onyx.setup import setup_postgres
|
||||
from onyx.setup import setup_vespa
|
||||
from onyx.utils.logger import setup_logger
|
||||
@@ -237,6 +238,12 @@ def reset_vespa() -> None:
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
def reset_redis() -> None:
|
||||
"""Reset the Redis database."""
|
||||
redis_client = get_redis_client()
|
||||
redis_client.flushall()
|
||||
|
||||
|
||||
def reset_postgres_multitenant() -> None:
|
||||
"""Reset the Postgres database for all tenants in a multitenant setup."""
|
||||
|
||||
@@ -341,6 +348,8 @@ def reset_all() -> None:
|
||||
reset_postgres()
|
||||
logger.info("Resetting Vespa...")
|
||||
reset_vespa()
|
||||
logger.info("Resetting Redis...")
|
||||
reset_redis()
|
||||
|
||||
|
||||
def reset_all_multitenant() -> None:
|
||||
|
||||
38
backend/tests/integration/tests/auth/test_user_invitation.py
Normal file
38
backend/tests/integration/tests/auth/test_user_invitation.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import pytest
|
||||
from requests import HTTPError
|
||||
|
||||
from onyx.auth.schemas import UserRole
|
||||
from tests.integration.common_utils.managers.user import UserManager
|
||||
from tests.integration.common_utils.test_models import DATestUser
|
||||
|
||||
|
||||
def test_inviting_users_flow(reset: None) -> None:
|
||||
"""
|
||||
Test that verifies the functionality around inviting users:
|
||||
1. Creating an admin user
|
||||
2. Admin inviting a new user
|
||||
3. Invited user successfully signing in
|
||||
4. Non-invited user attempting to sign in (should result in an error)
|
||||
"""
|
||||
# 1) Create an admin user (the first user created is automatically admin)
|
||||
admin_user: DATestUser = UserManager.create(name="admin_user")
|
||||
assert admin_user is not None
|
||||
assert UserManager.is_role(admin_user, UserRole.ADMIN)
|
||||
|
||||
# 2) Admin invites a new user
|
||||
invited_email = "invited_user@test.com"
|
||||
invite_response = UserManager.invite_users(admin_user, [invited_email])
|
||||
|
||||
assert invite_response == 1
|
||||
|
||||
# 3) The invited user successfully registers/logs in
|
||||
invited_user: DATestUser = UserManager.create(
|
||||
name="invited_user", email=invited_email
|
||||
)
|
||||
assert invited_user is not None
|
||||
assert invited_user.email == invited_email
|
||||
assert UserManager.is_role(invited_user, UserRole.BASIC)
|
||||
|
||||
# 4) A non-invited user attempts to sign in/register (should fail)
|
||||
with pytest.raises(HTTPError):
|
||||
UserManager.create(name="uninvited_user", email="uninvited_user@test.com")
|
||||
@@ -42,7 +42,6 @@ ENV NEXT_PUBLIC_DISABLE_STREAMING=${NEXT_PUBLIC_DISABLE_STREAMING}
|
||||
ARG NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA
|
||||
ENV NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA=${NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA}
|
||||
|
||||
# allow user to specify custom feedback options
|
||||
ARG NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS
|
||||
ENV NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS}
|
||||
|
||||
|
||||
3379
web/package-lock.json
generated
3379
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -93,11 +93,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/playwright": "^0.10.2",
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/chrome": "^0.0.287",
|
||||
"@types/jest": "^29.5.14",
|
||||
"chromatic": "^11.25.2",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "2.8.8",
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function PostHogPageView(): null {
|
||||
// Track pageviews
|
||||
if (pathname) {
|
||||
let url = window.origin + pathname;
|
||||
if (searchParams.toString()) {
|
||||
if (searchParams?.toString()) {
|
||||
url = url + `?${searchParams.toString()}`;
|
||||
}
|
||||
posthog.capture("$pageview", {
|
||||
|
||||
@@ -149,7 +149,7 @@ export function AssistantEditor({
|
||||
const { refreshAssistants, isImageGenerationAvailable } = useAssistants();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const isAdminPage = searchParams.get("admin") === "true";
|
||||
const isAdminPage = searchParams?.get("admin") === "true";
|
||||
|
||||
const { popup, setPopup } = usePopup();
|
||||
const { labels, refreshLabels, createLabel, updateLabel, deleteLabel } =
|
||||
|
||||
@@ -302,11 +302,17 @@ export default function AddConnector({
|
||||
...connector_specific_config
|
||||
} = values;
|
||||
|
||||
// Apply transforms from connectors.ts configuration
|
||||
// Apply special transforms according to application logic
|
||||
const transformedConnectorSpecificConfig = Object.entries(
|
||||
connector_specific_config
|
||||
).reduce(
|
||||
(acc, [key, value]) => {
|
||||
// Filter out empty strings from arrays
|
||||
if (Array.isArray(value)) {
|
||||
value = (value as any[]).filter(
|
||||
(item) => typeof item !== "string" || item.trim() !== ""
|
||||
);
|
||||
}
|
||||
const matchingConfigValue = configuration.values.find(
|
||||
(configValue) => configValue.name === key
|
||||
);
|
||||
|
||||
@@ -26,8 +26,8 @@ export default function OAuthCallbackPage() {
|
||||
);
|
||||
|
||||
// Extract query parameters
|
||||
const code = searchParams.get("code");
|
||||
const state = searchParams.get("state");
|
||||
const code = searchParams?.get("code");
|
||||
const state = searchParams?.get("state");
|
||||
|
||||
const pathname = usePathname();
|
||||
const connector = pathname?.split("/")[3];
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useEffect, useState } from "react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Title from "@/components/ui/title";
|
||||
import { KeyIcon } from "@/components/icons/icons";
|
||||
import { getSourceMetadata, isValidSource } from "@/lib/sources";
|
||||
import { ConfluenceAccessibleResource, ValidSources } from "@/lib/types";
|
||||
@@ -74,7 +73,7 @@ export default function OAuthFinalizePage() {
|
||||
>([]);
|
||||
|
||||
// Extract query parameters
|
||||
const credentialParam = searchParams.get("credential");
|
||||
const credentialParam = searchParams?.get("credential");
|
||||
const credential = credentialParam ? parseInt(credentialParam, 10) : NaN;
|
||||
const pathname = usePathname();
|
||||
const connector = pathname?.split("/")[3];
|
||||
@@ -85,7 +84,7 @@ export default function OAuthFinalizePage() {
|
||||
// connector (url segment)= "google-drive"
|
||||
// sourceType (for looking up metadata) = "google_drive"
|
||||
|
||||
if (isNaN(credential)) {
|
||||
if (isNaN(credential) || !connector) {
|
||||
setStatusMessage("Improperly formed OAuth finalization request.");
|
||||
setStatusDetails("Invalid or missing credential id.");
|
||||
setIsError(true);
|
||||
|
||||
@@ -23,8 +23,8 @@ const ResetPasswordPage: React.FC = () => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
const [isWorking, setIsWorking] = useState(false);
|
||||
const searchParams = useSearchParams();
|
||||
const token = searchParams.get("token");
|
||||
const tenantId = searchParams.get(TENANT_ID_COOKIE_NAME);
|
||||
const token = searchParams?.get("token");
|
||||
const tenantId = searchParams?.get(TENANT_ID_COOKIE_NAME);
|
||||
// Keep search param same name as cookie for simplicity
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -15,9 +15,9 @@ export function Verify({ user }: { user: User | null }) {
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const verify = useCallback(async () => {
|
||||
const token = searchParams.get("token");
|
||||
const token = searchParams?.get("token");
|
||||
const firstUser =
|
||||
searchParams.get("first_user") && NEXT_PUBLIC_CLOUD_ENABLED;
|
||||
searchParams?.get("first_user") && NEXT_PUBLIC_CLOUD_ENABLED;
|
||||
if (!token) {
|
||||
setError(
|
||||
"Missing verification token. Try requesting a new verification email."
|
||||
|
||||
@@ -196,7 +196,9 @@ export function ChatPage({
|
||||
setCurrentMessageFiles,
|
||||
} = useDocumentsContext();
|
||||
|
||||
const defaultAssistantIdRaw = searchParams.get(SEARCH_PARAM_NAMES.PERSONA_ID);
|
||||
const defaultAssistantIdRaw = searchParams?.get(
|
||||
SEARCH_PARAM_NAMES.PERSONA_ID
|
||||
);
|
||||
const defaultAssistantId = defaultAssistantIdRaw
|
||||
? parseInt(defaultAssistantIdRaw)
|
||||
: undefined;
|
||||
@@ -252,8 +254,8 @@ export function ChatPage({
|
||||
);
|
||||
|
||||
const { user, isAdmin } = useUser();
|
||||
const slackChatId = searchParams.get("slackChatId");
|
||||
const existingChatIdRaw = searchParams.get("chatId");
|
||||
const slackChatId = searchParams?.get("slackChatId");
|
||||
const existingChatIdRaw = searchParams?.get("chatId");
|
||||
|
||||
const [showHistorySidebar, setShowHistorySidebar] = useState(false);
|
||||
|
||||
@@ -275,7 +277,7 @@ export function ChatPage({
|
||||
|
||||
const processSearchParamsAndSubmitMessage = (searchParamsString: string) => {
|
||||
const newSearchParams = new URLSearchParams(searchParamsString);
|
||||
const message = newSearchParams.get("user-prompt");
|
||||
const message = newSearchParams?.get("user-prompt");
|
||||
|
||||
filterManager.buildFiltersFromQueryString(
|
||||
newSearchParams.toString(),
|
||||
@@ -284,7 +286,7 @@ export function ChatPage({
|
||||
tags
|
||||
);
|
||||
|
||||
const fileDescriptorString = newSearchParams.get(SEARCH_PARAM_NAMES.FILES);
|
||||
const fileDescriptorString = newSearchParams?.get(SEARCH_PARAM_NAMES.FILES);
|
||||
const overrideFileDescriptors: FileDescriptor[] = fileDescriptorString
|
||||
? JSON.parse(decodeURIComponent(fileDescriptorString))
|
||||
: [];
|
||||
@@ -324,7 +326,7 @@ export function ChatPage({
|
||||
: undefined
|
||||
);
|
||||
// Gather default temperature settings
|
||||
const search_param_temperature = searchParams.get(
|
||||
const search_param_temperature = searchParams?.get(
|
||||
SEARCH_PARAM_NAMES.TEMPERATURE
|
||||
);
|
||||
|
||||
@@ -551,7 +553,7 @@ export function ChatPage({
|
||||
if (
|
||||
newMessageHistory.length === 1 &&
|
||||
!submitOnLoadPerformed.current &&
|
||||
searchParams.get(SEARCH_PARAM_NAMES.SEEDED) === "true"
|
||||
searchParams?.get(SEARCH_PARAM_NAMES.SEEDED) === "true"
|
||||
) {
|
||||
submitOnLoadPerformed.current = true;
|
||||
const seededMessage = newMessageHistory[0].message;
|
||||
@@ -572,11 +574,11 @@ export function ChatPage({
|
||||
|
||||
initialSessionFetch();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [existingChatSessionId, searchParams.get(SEARCH_PARAM_NAMES.PERSONA_ID)]);
|
||||
}, [existingChatSessionId, searchParams?.get(SEARCH_PARAM_NAMES.PERSONA_ID)]);
|
||||
|
||||
useEffect(() => {
|
||||
const userFolderId = searchParams.get(SEARCH_PARAM_NAMES.USER_FOLDER_ID);
|
||||
const allMyDocuments = searchParams.get(
|
||||
const userFolderId = searchParams?.get(SEARCH_PARAM_NAMES.USER_FOLDER_ID);
|
||||
const allMyDocuments = searchParams?.get(
|
||||
SEARCH_PARAM_NAMES.ALL_MY_DOCUMENTS
|
||||
);
|
||||
|
||||
@@ -599,14 +601,14 @@ export function ChatPage({
|
||||
}
|
||||
}, [
|
||||
userFolders,
|
||||
searchParams.get(SEARCH_PARAM_NAMES.USER_FOLDER_ID),
|
||||
searchParams.get(SEARCH_PARAM_NAMES.ALL_MY_DOCUMENTS),
|
||||
searchParams?.get(SEARCH_PARAM_NAMES.USER_FOLDER_ID),
|
||||
searchParams?.get(SEARCH_PARAM_NAMES.ALL_MY_DOCUMENTS),
|
||||
addSelectedFolder,
|
||||
clearSelectedItems,
|
||||
]);
|
||||
|
||||
const [message, setMessage] = useState(
|
||||
searchParams.get(SEARCH_PARAM_NAMES.USER_PROMPT) || ""
|
||||
searchParams?.get(SEARCH_PARAM_NAMES.USER_PROMPT) || ""
|
||||
);
|
||||
|
||||
const [completeMessageDetail, setCompleteMessageDetail] = useState<
|
||||
@@ -1048,7 +1050,7 @@ export function ChatPage({
|
||||
|
||||
// Equivalent to `loadNewPageLogic`
|
||||
useEffect(() => {
|
||||
if (searchParams.get(SEARCH_PARAM_NAMES.SEND_ON_LOAD)) {
|
||||
if (searchParams?.get(SEARCH_PARAM_NAMES.SEND_ON_LOAD)) {
|
||||
processSearchParamsAndSubmitMessage(searchParams.toString());
|
||||
}
|
||||
}, [searchParams, router]);
|
||||
@@ -1231,7 +1233,7 @@ export function ChatPage({
|
||||
const isNewSession = chatSessionIdRef.current === null;
|
||||
|
||||
const searchParamBasedChatSessionName =
|
||||
searchParams.get(SEARCH_PARAM_NAMES.TITLE) || null;
|
||||
searchParams?.get(SEARCH_PARAM_NAMES.TITLE) || null;
|
||||
|
||||
if (isNewSession) {
|
||||
currChatSessionId = await createChatSession(
|
||||
@@ -1409,11 +1411,11 @@ export function ChatPage({
|
||||
modelVersion:
|
||||
modelOverride?.modelName ||
|
||||
llmManager.currentLlm.modelName ||
|
||||
searchParams.get(SEARCH_PARAM_NAMES.MODEL_VERSION) ||
|
||||
searchParams?.get(SEARCH_PARAM_NAMES.MODEL_VERSION) ||
|
||||
undefined,
|
||||
temperature: llmManager.temperature || undefined,
|
||||
systemPromptOverride:
|
||||
searchParams.get(SEARCH_PARAM_NAMES.SYSTEM_PROMPT) || undefined,
|
||||
searchParams?.get(SEARCH_PARAM_NAMES.SYSTEM_PROMPT) || undefined,
|
||||
useExistingUserMessage: isSeededChat,
|
||||
useLanggraph:
|
||||
settings?.settings.pro_search_enabled &&
|
||||
|
||||
@@ -4,177 +4,62 @@ import {
|
||||
LlmDescriptor,
|
||||
useLlmManager,
|
||||
} from "@/lib/hooks";
|
||||
import { StringOrNumberOption } from "@/components/Dropdown";
|
||||
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { destructureValue, getFinalLLM, structureValue } from "@/lib/llm/utils";
|
||||
import { destructureValue } from "@/lib/llm/utils";
|
||||
import { useState } from "react";
|
||||
import { Hoverable } from "@/components/Hoverable";
|
||||
import { Popover } from "@/components/popover/Popover";
|
||||
import { IconType } from "react-icons";
|
||||
import { FiRefreshCw, FiCheck } from "react-icons/fi";
|
||||
|
||||
export function RegenerateDropdown({
|
||||
options,
|
||||
selected,
|
||||
onSelect,
|
||||
side,
|
||||
maxHeight,
|
||||
alternate,
|
||||
onDropdownVisibleChange,
|
||||
}: {
|
||||
alternate?: string;
|
||||
options: StringOrNumberOption[];
|
||||
selected: string | null;
|
||||
onSelect: (value: string | number | null) => void;
|
||||
includeDefault?: boolean;
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
maxHeight?: string;
|
||||
onDropdownVisibleChange: (isVisible: boolean) => void;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const toggleDropdownVisible = (isVisible: boolean) => {
|
||||
setIsOpen(isVisible);
|
||||
onDropdownVisibleChange(isVisible);
|
||||
};
|
||||
|
||||
const Dropdown = (
|
||||
<div className="overflow-y-auto border border-neutral-800 py-2 min-w-fit bg-neutral-50 dark:bg-neutral-900 rounded-md shadow-lg">
|
||||
<div className="mb-1 flex items-center justify-between px-4 pt-2">
|
||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Regenerate with
|
||||
</span>
|
||||
</div>
|
||||
{options.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
role="menuitem"
|
||||
className={`flex items-center m-1.5 p-1.5 text-sm cursor-pointer focus-visible:outline-0 group relative hover:bg-neutral-200 dark:hover:bg-neutral-800 rounded-md my-0 px-3 mx-2 gap-2.5 py-3 !pr-3 ${
|
||||
option.value === selected
|
||||
? "bg-neutral-200 dark:bg-neutral-800"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => onSelect(option.value)}
|
||||
>
|
||||
<div className="flex grow items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div>{getDisplayNameForModel(option.name)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{option.value === selected && (
|
||||
<FiCheck className="text-neutral-700 dark:text-neutral-300" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={toggleDropdownVisible}
|
||||
content={
|
||||
<div onClick={() => toggleDropdownVisible(!isOpen)}>
|
||||
{!alternate ? (
|
||||
<Hoverable size={16} icon={FiRefreshCw as IconType} />
|
||||
) : (
|
||||
<Hoverable
|
||||
size={16}
|
||||
icon={FiRefreshCw as IconType}
|
||||
hoverText={getDisplayNameForModel(alternate)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
popover={Dropdown}
|
||||
align="start"
|
||||
side={side}
|
||||
sideOffset={5}
|
||||
triggerMaxWidth
|
||||
/>
|
||||
);
|
||||
}
|
||||
import { FiRefreshCw } from "react-icons/fi";
|
||||
import LLMPopover from "./input/LLMPopover";
|
||||
|
||||
export default function RegenerateOption({
|
||||
selectedAssistant,
|
||||
regenerate,
|
||||
overriddenModel,
|
||||
onHoverChange,
|
||||
onDropdownVisibleChange,
|
||||
}: {
|
||||
selectedAssistant: Persona;
|
||||
regenerate: (modelOverRide: LlmDescriptor) => Promise<void>;
|
||||
overriddenModel?: string;
|
||||
onHoverChange: (isHovered: boolean) => void;
|
||||
onDropdownVisibleChange: (isVisible: boolean) => void;
|
||||
}) {
|
||||
const { llmProviders } = useChatContext();
|
||||
const llmManager = useLlmManager(llmProviders);
|
||||
|
||||
const [_, llmName] = getFinalLLM(llmProviders, selectedAssistant, null);
|
||||
|
||||
const llmOptionsByProvider: {
|
||||
[provider: string]: { name: string; value: string }[];
|
||||
} = {};
|
||||
const uniqueModelNames = new Set<string>();
|
||||
|
||||
llmProviders.forEach((llmProvider) => {
|
||||
if (!llmOptionsByProvider[llmProvider.provider]) {
|
||||
llmOptionsByProvider[llmProvider.provider] = [];
|
||||
}
|
||||
|
||||
(llmProvider.display_model_names || llmProvider.model_names).forEach(
|
||||
(modelName) => {
|
||||
if (!uniqueModelNames.has(modelName)) {
|
||||
uniqueModelNames.add(modelName);
|
||||
llmOptionsByProvider[llmProvider.provider].push({
|
||||
name: modelName,
|
||||
value: structureValue(
|
||||
llmProvider.name,
|
||||
llmProvider.provider,
|
||||
modelName
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const llmOptions = Object.entries(llmOptionsByProvider).flatMap(
|
||||
([provider, options]) => [...options]
|
||||
);
|
||||
|
||||
const currentModelName =
|
||||
llmManager?.currentLlm.modelName ||
|
||||
(selectedAssistant
|
||||
? selectedAssistant.llm_model_version_override || llmName
|
||||
: llmName);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const toggleDropdownVisible = (isVisible: boolean) => {
|
||||
setIsOpen(isVisible);
|
||||
onDropdownVisibleChange(isVisible);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group flex items-center relative"
|
||||
onMouseEnter={() => onHoverChange(true)}
|
||||
onMouseLeave={() => onHoverChange(false)}
|
||||
>
|
||||
<RegenerateDropdown
|
||||
onDropdownVisibleChange={onDropdownVisibleChange}
|
||||
alternate={overriddenModel}
|
||||
options={llmOptions}
|
||||
selected={currentModelName}
|
||||
onSelect={(value) => {
|
||||
const { name, provider, modelName } = destructureValue(
|
||||
value as string
|
||||
);
|
||||
regenerate({
|
||||
name: name,
|
||||
provider: provider,
|
||||
modelName: modelName,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<LLMPopover
|
||||
llmManager={llmManager}
|
||||
llmProviders={llmProviders}
|
||||
requiresImageGeneration={false}
|
||||
currentAssistant={selectedAssistant}
|
||||
currentModelName={overriddenModel}
|
||||
trigger={
|
||||
<div onClick={() => toggleDropdownVisible(!isOpen)}>
|
||||
{!overriddenModel ? (
|
||||
<Hoverable size={16} icon={FiRefreshCw as IconType} />
|
||||
) : (
|
||||
<Hoverable
|
||||
size={16}
|
||||
icon={FiRefreshCw as IconType}
|
||||
hoverText={getDisplayNameForModel(overriddenModel)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
onSelect={(value) => {
|
||||
const { name, provider, modelName } = destructureValue(value as string);
|
||||
regenerate({
|
||||
name: name,
|
||||
provider: provider,
|
||||
modelName: modelName,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import LLMPopover from "./LLMPopover";
|
||||
import { InputPrompt } from "@/app/chat/interfaces";
|
||||
|
||||
import { FilterManager, LlmManager } from "@/lib/hooks";
|
||||
import { FilterManager, getDisplayNameForModel, LlmManager } from "@/lib/hooks";
|
||||
import { useChatContext } from "@/components/context/ChatContext";
|
||||
import { ChatFileType, FileDescriptor } from "../interfaces";
|
||||
import {
|
||||
@@ -38,6 +38,7 @@ import { useUser } from "@/components/user/UserProvider";
|
||||
import { useDocumentSelection } from "../useDocumentSelection";
|
||||
import { AgenticToggle } from "./AgenticToggle";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { getProviderIcon } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { LoadingIndicator } from "react-select/dist/declarations/src/components/indicators";
|
||||
import { FidgetSpinner } from "react-loader-spinner";
|
||||
import { LoadingAnimation } from "@/components/Loading";
|
||||
@@ -799,6 +800,27 @@ export function ChatInputBar({
|
||||
llmManager={llmManager}
|
||||
requiresImageGeneration={false}
|
||||
currentAssistant={selectedAssistant}
|
||||
trigger={
|
||||
<button
|
||||
className="dark:text-white text-black focus:outline-none"
|
||||
data-testid="llm-popover-trigger"
|
||||
>
|
||||
<ChatInputOption
|
||||
minimize
|
||||
toggle
|
||||
flexPriority="stiff"
|
||||
name={getDisplayNameForModel(
|
||||
llmManager?.currentLlm.modelName || "Models"
|
||||
)}
|
||||
Icon={getProviderIcon(
|
||||
llmManager?.currentLlm.provider || "anthropic",
|
||||
llmManager?.currentLlm.modelName ||
|
||||
"claude-3-5-sonnet-20240620"
|
||||
)}
|
||||
tooltipContent="Switch models"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
{retrievalEnabled && (
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ChatInputOption } from "./ChatInputOption";
|
||||
import { getDisplayNameForModel } from "@/lib/hooks";
|
||||
import {
|
||||
checkLLMSupportsImageInput,
|
||||
@@ -35,12 +28,16 @@ import { FiAlertTriangle } from "react-icons/fi";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { TruncatedText } from "@/components/ui/truncatedText";
|
||||
import { ChatInputOption } from "./ChatInputOption";
|
||||
|
||||
interface LLMPopoverProps {
|
||||
llmProviders: LLMProviderDescriptor[];
|
||||
llmManager: LlmManager;
|
||||
requiresImageGeneration?: boolean;
|
||||
currentAssistant?: Persona;
|
||||
trigger?: React.ReactElement;
|
||||
onSelect?: (value: string) => void;
|
||||
currentModelName?: string;
|
||||
}
|
||||
|
||||
export default function LLMPopover({
|
||||
@@ -48,70 +45,69 @@ export default function LLMPopover({
|
||||
llmManager,
|
||||
requiresImageGeneration,
|
||||
currentAssistant,
|
||||
trigger,
|
||||
onSelect,
|
||||
currentModelName,
|
||||
}: LLMPopoverProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { user } = useUser();
|
||||
|
||||
// Memoize the options to prevent unnecessary recalculations
|
||||
const {
|
||||
llmOptionsByProvider,
|
||||
llmOptions,
|
||||
defaultProvider,
|
||||
defaultModelDisplayName,
|
||||
} = useMemo(() => {
|
||||
const llmOptionsByProvider: {
|
||||
[provider: string]: {
|
||||
name: string;
|
||||
value: string;
|
||||
icon: React.FC<{ size?: number; className?: string }>;
|
||||
}[];
|
||||
} = {};
|
||||
const { llmOptions, defaultProvider, defaultModelDisplayName } =
|
||||
useMemo(() => {
|
||||
const llmOptionsByProvider: {
|
||||
[provider: string]: {
|
||||
name: string;
|
||||
value: string;
|
||||
icon: React.FC<{ size?: number; className?: string }>;
|
||||
}[];
|
||||
} = {};
|
||||
|
||||
const uniqueModelNames = new Set<string>();
|
||||
const uniqueModelNames = new Set<string>();
|
||||
|
||||
llmProviders.forEach((llmProvider) => {
|
||||
if (!llmOptionsByProvider[llmProvider.provider]) {
|
||||
llmOptionsByProvider[llmProvider.provider] = [];
|
||||
}
|
||||
|
||||
(llmProvider.display_model_names || llmProvider.model_names).forEach(
|
||||
(modelName) => {
|
||||
if (!uniqueModelNames.has(modelName)) {
|
||||
uniqueModelNames.add(modelName);
|
||||
llmOptionsByProvider[llmProvider.provider].push({
|
||||
name: modelName,
|
||||
value: structureValue(
|
||||
llmProvider.name,
|
||||
llmProvider.provider,
|
||||
modelName
|
||||
),
|
||||
icon: getProviderIcon(llmProvider.provider, modelName),
|
||||
});
|
||||
}
|
||||
llmProviders.forEach((llmProvider) => {
|
||||
if (!llmOptionsByProvider[llmProvider.provider]) {
|
||||
llmOptionsByProvider[llmProvider.provider] = [];
|
||||
}
|
||||
|
||||
(llmProvider.display_model_names || llmProvider.model_names).forEach(
|
||||
(modelName) => {
|
||||
if (!uniqueModelNames.has(modelName)) {
|
||||
uniqueModelNames.add(modelName);
|
||||
llmOptionsByProvider[llmProvider.provider].push({
|
||||
name: modelName,
|
||||
value: structureValue(
|
||||
llmProvider.name,
|
||||
llmProvider.provider,
|
||||
modelName
|
||||
),
|
||||
icon: getProviderIcon(llmProvider.provider, modelName),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const llmOptions = Object.entries(llmOptionsByProvider).flatMap(
|
||||
([provider, options]) => [...options]
|
||||
);
|
||||
});
|
||||
|
||||
const llmOptions = Object.entries(llmOptionsByProvider).flatMap(
|
||||
([provider, options]) => [...options]
|
||||
);
|
||||
const defaultProvider = llmProviders.find(
|
||||
(llmProvider) => llmProvider.is_default_provider
|
||||
);
|
||||
|
||||
const defaultProvider = llmProviders.find(
|
||||
(llmProvider) => llmProvider.is_default_provider
|
||||
);
|
||||
const defaultModelName = defaultProvider?.default_model_name;
|
||||
const defaultModelDisplayName = defaultModelName
|
||||
? getDisplayNameForModel(defaultModelName)
|
||||
: null;
|
||||
|
||||
const defaultModelName = defaultProvider?.default_model_name;
|
||||
const defaultModelDisplayName = defaultModelName
|
||||
? getDisplayNameForModel(defaultModelName)
|
||||
: null;
|
||||
|
||||
return {
|
||||
llmOptionsByProvider,
|
||||
llmOptions,
|
||||
defaultProvider,
|
||||
defaultModelDisplayName,
|
||||
};
|
||||
}, [llmProviders]);
|
||||
return {
|
||||
llmOptionsByProvider,
|
||||
llmOptions,
|
||||
defaultProvider,
|
||||
defaultModelDisplayName,
|
||||
};
|
||||
}, [llmProviders]);
|
||||
|
||||
const [localTemperature, setLocalTemperature] = useState(
|
||||
llmManager.temperature ?? 0.5
|
||||
@@ -135,32 +131,34 @@ export default function LLMPopover({
|
||||
|
||||
// Memoize trigger content to prevent rerendering
|
||||
const triggerContent = useMemo(
|
||||
() => (
|
||||
<button
|
||||
className="dark:text-[#fff] text-[#000] focus:outline-none"
|
||||
data-testid="llm-popover-trigger"
|
||||
>
|
||||
<ChatInputOption
|
||||
minimize
|
||||
toggle
|
||||
flexPriority="stiff"
|
||||
name={getDisplayNameForModel(
|
||||
llmManager?.currentLlm.modelName ||
|
||||
defaultModelDisplayName ||
|
||||
"Models"
|
||||
)}
|
||||
Icon={getProviderIcon(
|
||||
llmManager?.currentLlm.provider ||
|
||||
defaultProvider?.provider ||
|
||||
"anthropic",
|
||||
llmManager?.currentLlm.modelName ||
|
||||
defaultProvider?.default_model_name ||
|
||||
"claude-3-5-sonnet-20240620"
|
||||
)}
|
||||
tooltipContent="Switch models"
|
||||
/>
|
||||
</button>
|
||||
),
|
||||
trigger
|
||||
? () => trigger
|
||||
: () => (
|
||||
<button
|
||||
className="dark:text-[#fff] text-[#000] focus:outline-none"
|
||||
data-testid="llm-popover-trigger"
|
||||
>
|
||||
<ChatInputOption
|
||||
minimize
|
||||
toggle
|
||||
flexPriority="stiff"
|
||||
name={getDisplayNameForModel(
|
||||
llmManager?.currentLlm.modelName ||
|
||||
defaultModelDisplayName ||
|
||||
"Models"
|
||||
)}
|
||||
Icon={getProviderIcon(
|
||||
llmManager?.currentLlm.provider ||
|
||||
defaultProvider?.provider ||
|
||||
"anthropic",
|
||||
llmManager?.currentLlm.modelName ||
|
||||
defaultProvider?.default_model_name ||
|
||||
"claude-3-5-sonnet-20240620"
|
||||
)}
|
||||
tooltipContent="Switch models"
|
||||
/>
|
||||
</button>
|
||||
),
|
||||
[defaultModelDisplayName, defaultProvider, llmManager?.currentLlm]
|
||||
);
|
||||
|
||||
@@ -178,12 +176,14 @@ export default function LLMPopover({
|
||||
<button
|
||||
key={index}
|
||||
className={`w-full flex items-center gap-x-2 px-3 py-2 text-sm text-left hover:bg-background-100 dark:hover:bg-neutral-800 transition-colors duration-150 ${
|
||||
llmManager.currentLlm.modelName === name
|
||||
(currentModelName || llmManager.currentLlm.modelName) ===
|
||||
name
|
||||
? "bg-background-100 dark:bg-neutral-900 text-text"
|
||||
: "text-text-darker"
|
||||
}`}
|
||||
onClick={() => {
|
||||
llmManager.updateCurrentLlm(destructureValue(value));
|
||||
onSelect?.(value);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -668,7 +668,7 @@ const PARAMS_TO_SKIP = [
|
||||
];
|
||||
|
||||
export function buildChatUrl(
|
||||
existingSearchParams: ReadonlyURLSearchParams,
|
||||
existingSearchParams: ReadonlyURLSearchParams | null,
|
||||
chatSessionId: string | null,
|
||||
personaId: number | null,
|
||||
search?: boolean
|
||||
@@ -685,7 +685,7 @@ export function buildChatUrl(
|
||||
finalSearchParams.push(`${SEARCH_PARAM_NAMES.PERSONA_ID}=${personaId}`);
|
||||
}
|
||||
|
||||
existingSearchParams.forEach((value, key) => {
|
||||
existingSearchParams?.forEach((value, key) => {
|
||||
if (!PARAMS_TO_SKIP.includes(key)) {
|
||||
finalSearchParams.push(`${key}=${value}`);
|
||||
}
|
||||
@@ -719,7 +719,7 @@ export async function uploadFilesForChat(
|
||||
return [responseJson.files as FileDescriptor[], null];
|
||||
}
|
||||
|
||||
export async function useScrollonStream({
|
||||
export function useScrollonStream({
|
||||
chatState,
|
||||
scrollableDivRef,
|
||||
scrollDist,
|
||||
@@ -817,5 +817,5 @@ export async function useScrollonStream({
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [chatState, distance, scrollDist, scrollableDivRef]);
|
||||
}, [chatState, distance, scrollDist, scrollableDivRef, enableAutoScroll]);
|
||||
}
|
||||
|
||||
@@ -178,7 +178,6 @@ export const AgenticMessage = ({
|
||||
const [isViewingInitialAnswer, setIsViewingInitialAnswer] = useState(true);
|
||||
|
||||
const [canShowResponse, setCanShowResponse] = useState(isComplete);
|
||||
const [isRegenerateHovered, setIsRegenerateHovered] = useState(false);
|
||||
const [isRegenerateDropdownVisible, setIsRegenerateDropdownVisible] =
|
||||
useState(false);
|
||||
|
||||
@@ -597,7 +596,6 @@ export const AgenticMessage = ({
|
||||
onDropdownVisibleChange={
|
||||
setIsRegenerateDropdownVisible
|
||||
}
|
||||
onHoverChange={setIsRegenerateHovered}
|
||||
selectedAssistant={currentPersona!}
|
||||
regenerate={regenerate}
|
||||
overriddenModel={overriddenModel}
|
||||
@@ -613,16 +611,10 @@ export const AgenticMessage = ({
|
||||
absolute -bottom-5
|
||||
z-10
|
||||
invisible ${
|
||||
(isHovering ||
|
||||
isRegenerateHovered ||
|
||||
settings?.isMobile) &&
|
||||
"!visible"
|
||||
(isHovering || settings?.isMobile) && "!visible"
|
||||
}
|
||||
opacity-0 ${
|
||||
(isHovering ||
|
||||
isRegenerateHovered ||
|
||||
settings?.isMobile) &&
|
||||
"!opacity-100"
|
||||
(isHovering || settings?.isMobile) && "!opacity-100"
|
||||
}
|
||||
translate-y-2 ${
|
||||
(isHovering || settings?.isMobile) &&
|
||||
@@ -697,7 +689,6 @@ export const AgenticMessage = ({
|
||||
}
|
||||
regenerate={regenerate}
|
||||
overriddenModel={overriddenModel}
|
||||
onHoverChange={setIsRegenerateHovered}
|
||||
/>
|
||||
</CustomTooltip>
|
||||
)}
|
||||
|
||||
@@ -301,7 +301,6 @@ export const AIMessage = ({
|
||||
|
||||
const finalContent = processContent(content as string);
|
||||
|
||||
const [isRegenerateHovered, setIsRegenerateHovered] = useState(false);
|
||||
const [isRegenerateDropdownVisible, setIsRegenerateDropdownVisible] =
|
||||
useState(false);
|
||||
const { isHovering, trackedElementRef, hoverElementRef } = useMouseTracking();
|
||||
@@ -728,7 +727,6 @@ export const AIMessage = ({
|
||||
onDropdownVisibleChange={
|
||||
setIsRegenerateDropdownVisible
|
||||
}
|
||||
onHoverChange={setIsRegenerateHovered}
|
||||
selectedAssistant={currentPersona!}
|
||||
regenerate={regenerate}
|
||||
overriddenModel={overriddenModel}
|
||||
@@ -744,16 +742,10 @@ export const AIMessage = ({
|
||||
absolute -bottom-5
|
||||
z-10
|
||||
invisible ${
|
||||
(isHovering ||
|
||||
isRegenerateHovered ||
|
||||
settings?.isMobile) &&
|
||||
"!visible"
|
||||
(isHovering || settings?.isMobile) && "!visible"
|
||||
}
|
||||
opacity-0 ${
|
||||
(isHovering ||
|
||||
isRegenerateHovered ||
|
||||
settings?.isMobile) &&
|
||||
"!opacity-100"
|
||||
(isHovering || settings?.isMobile) && "!opacity-100"
|
||||
}
|
||||
flex md:flex-row gap-x-0.5 bg-background-125/40 -mx-1.5 p-1.5 rounded-lg
|
||||
`}
|
||||
@@ -818,7 +810,6 @@ export const AIMessage = ({
|
||||
}
|
||||
regenerate={regenerate}
|
||||
overriddenModel={overriddenModel}
|
||||
onHoverChange={setIsRegenerateHovered}
|
||||
/>
|
||||
</CustomTooltip>
|
||||
)}
|
||||
|
||||
@@ -23,8 +23,10 @@ export const SEARCH_PARAM_NAMES = {
|
||||
SEND_ON_LOAD: "send-on-load",
|
||||
};
|
||||
|
||||
export function shouldSubmitOnLoad(searchParams: ReadonlyURLSearchParams) {
|
||||
const rawSubmitOnLoad = searchParams.get(SEARCH_PARAM_NAMES.SUBMIT_ON_LOAD);
|
||||
export function shouldSubmitOnLoad(
|
||||
searchParams: ReadonlyURLSearchParams | null
|
||||
) {
|
||||
const rawSubmitOnLoad = searchParams?.get(SEARCH_PARAM_NAMES.SUBMIT_ON_LOAD);
|
||||
if (rawSubmitOnLoad === "true" || rawSubmitOnLoad === "1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ export function UserDropdown({
|
||||
|
||||
// Construct the current URL
|
||||
const currentUrl = `${pathname}${
|
||||
searchParams.toString() ? `?${searchParams.toString()}` : ""
|
||||
searchParams?.toString() ? `?${searchParams.toString()}` : ""
|
||||
}`;
|
||||
|
||||
// Encode the current URL to use as a redirect parameter
|
||||
|
||||
@@ -59,8 +59,8 @@ export function ClientLayout({
|
||||
const { llmProviders } = useChatContext();
|
||||
const { popup, setPopup } = usePopup();
|
||||
if (
|
||||
pathname.startsWith("/admin/connectors") ||
|
||||
pathname.startsWith("/admin/embeddings")
|
||||
(pathname && pathname.startsWith("/admin/connectors")) ||
|
||||
(pathname && pathname.startsWith("/admin/embeddings"))
|
||||
) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export const ChatProvider: React.FC<{
|
||||
const { sessions } = await response.json();
|
||||
setChatSessions(sessions);
|
||||
|
||||
const currentSessionId = searchParams.get("chatId");
|
||||
const currentSessionId = searchParams?.get("chatId");
|
||||
if (
|
||||
currentSessionId &&
|
||||
!sessions.some(
|
||||
|
||||
@@ -34,7 +34,7 @@ export const EmbeddingFormProvider: React.FC<{
|
||||
const pathname = usePathname();
|
||||
|
||||
// Initialize formStep based on the URL parameter
|
||||
const initialStep = parseInt(searchParams.get("step") || "0", 10);
|
||||
const initialStep = parseInt(searchParams?.get("step") || "0", 10);
|
||||
const [formStep, setFormStep] = useState(initialStep);
|
||||
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
||||
|
||||
@@ -56,8 +56,10 @@ export const EmbeddingFormProvider: React.FC<{
|
||||
|
||||
useEffect(() => {
|
||||
// Update URL when formStep changes
|
||||
const updatedSearchParams = new URLSearchParams(searchParams.toString());
|
||||
const existingStep = updatedSearchParams.get("step");
|
||||
const updatedSearchParams = new URLSearchParams(
|
||||
searchParams?.toString() || ""
|
||||
);
|
||||
const existingStep = updatedSearchParams?.get("step");
|
||||
updatedSearchParams.set("step", formStep.toString());
|
||||
const newUrl = `${pathname}?${updatedSearchParams.toString()}`;
|
||||
|
||||
@@ -70,7 +72,7 @@ export const EmbeddingFormProvider: React.FC<{
|
||||
|
||||
// Update formStep when URL changes
|
||||
useEffect(() => {
|
||||
const stepFromUrl = parseInt(searchParams.get("step") || "0", 10);
|
||||
const stepFromUrl = parseInt(searchParams?.get("step") || "0", 10);
|
||||
if (stepFromUrl !== formStep) {
|
||||
setFormStep(stepFromUrl);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export const FormProvider: React.FC<{
|
||||
const pathname = usePathname();
|
||||
|
||||
// Initialize formStep based on the URL parameter
|
||||
const initialStep = parseInt(searchParams.get("step") || "0", 10);
|
||||
const initialStep = parseInt(searchParams?.get("step") || "0", 10);
|
||||
const [formStep, setFormStep] = useState(initialStep);
|
||||
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
||||
|
||||
@@ -56,8 +56,10 @@ export const FormProvider: React.FC<{
|
||||
|
||||
useEffect(() => {
|
||||
// Update URL when formStep changes
|
||||
const updatedSearchParams = new URLSearchParams(searchParams.toString());
|
||||
const existingStep = updatedSearchParams.get("step");
|
||||
const updatedSearchParams = new URLSearchParams(
|
||||
searchParams?.toString() || ""
|
||||
);
|
||||
const existingStep = updatedSearchParams?.get("step");
|
||||
updatedSearchParams.set("step", formStep.toString());
|
||||
const newUrl = `${pathname}?${updatedSearchParams.toString()}`;
|
||||
|
||||
@@ -69,7 +71,7 @@ export const FormProvider: React.FC<{
|
||||
}, [formStep, router, pathname, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const stepFromUrl = parseInt(searchParams.get("step") || "0", 10);
|
||||
const stepFromUrl = parseInt(searchParams?.get("step") || "0", 10);
|
||||
if (stepFromUrl !== formStep) {
|
||||
setFormStep(stepFromUrl);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export const HealthCheckBanner = () => {
|
||||
useEffect(() => {
|
||||
if (userError && userError.status === 403) {
|
||||
logout().then(() => {
|
||||
if (!pathname.includes("/auth")) {
|
||||
if (!pathname?.includes("/auth")) {
|
||||
setShowLoggedOutModal(true);
|
||||
}
|
||||
});
|
||||
@@ -61,7 +61,7 @@ export const HealthCheckBanner = () => {
|
||||
expirationTimeoutRef.current = setTimeout(() => {
|
||||
setExpired(true);
|
||||
|
||||
if (!pathname.includes("/auth")) {
|
||||
if (!pathname?.includes("/auth")) {
|
||||
setShowLoggedOutModal(true);
|
||||
}
|
||||
}, timeUntilExpire);
|
||||
@@ -205,7 +205,7 @@ export const HealthCheckBanner = () => {
|
||||
}
|
||||
|
||||
if (error instanceof RedirectError || expired) {
|
||||
if (!pathname.includes("/auth")) {
|
||||
if (!pathname?.includes("/auth")) {
|
||||
setShowLoggedOutModal(true);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -19,12 +19,12 @@ function setWelcomeFlowComplete() {
|
||||
Cookies.set(COMPLETED_WELCOME_FLOW_COOKIE, "true", { expires: 365 });
|
||||
}
|
||||
|
||||
export function _CompletedWelcomeFlowDummyComponent() {
|
||||
export function CompletedWelcomeFlowDummyComponent() {
|
||||
setWelcomeFlowComplete();
|
||||
return null;
|
||||
}
|
||||
|
||||
export function _WelcomeModal({ user }: { user: User | null }) {
|
||||
export function WelcomeModal({ user }: { user: User | null }) {
|
||||
const router = useRouter();
|
||||
|
||||
const [providerOptions, setProviderOptions] = useState<
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
_CompletedWelcomeFlowDummyComponent,
|
||||
_WelcomeModal,
|
||||
CompletedWelcomeFlowDummyComponent,
|
||||
WelcomeModal as WelcomeModalComponent,
|
||||
} from "./WelcomeModal";
|
||||
import { COMPLETED_WELCOME_FLOW_COOKIE } from "./constants";
|
||||
import { User } from "@/lib/types";
|
||||
@@ -24,8 +24,8 @@ export function WelcomeModal({
|
||||
}) {
|
||||
const hasCompletedWelcomeFlow = hasCompletedWelcomeFlowSS(requestCookies);
|
||||
if (hasCompletedWelcomeFlow) {
|
||||
return <_CompletedWelcomeFlowDummyComponent />;
|
||||
return <CompletedWelcomeFlowDummyComponent />;
|
||||
}
|
||||
|
||||
return <_WelcomeModal user={user} />;
|
||||
return <WelcomeModalComponent user={user} />;
|
||||
}
|
||||
|
||||
@@ -31,13 +31,13 @@ export function NewTeamModal() {
|
||||
const { setPopup } = usePopup();
|
||||
|
||||
useEffect(() => {
|
||||
const hasNewTeamParam = searchParams.has("new_team");
|
||||
const hasNewTeamParam = searchParams?.has("new_team");
|
||||
if (hasNewTeamParam) {
|
||||
setShowNewTeamModal(true);
|
||||
fetchTenantInfo();
|
||||
|
||||
// Remove the new_team parameter from the URL without page reload
|
||||
const newParams = new URLSearchParams(searchParams.toString());
|
||||
const newParams = new URLSearchParams(searchParams?.toString() || "");
|
||||
newParams.delete("new_team");
|
||||
const newUrl =
|
||||
window.location.pathname +
|
||||
|
||||
@@ -16,7 +16,7 @@ export const usePopupFromQuery = (messages: PopupMessages) => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// Get the value for search param with key "message"
|
||||
const messageValue = searchParams.get("message");
|
||||
const messageValue = searchParams?.get("message");
|
||||
|
||||
// Check if any key from messages object is present in search params
|
||||
if (messageValue && messageValue in messages) {
|
||||
|
||||
@@ -148,7 +148,7 @@ function usePaginatedFetch<T extends PaginatedType>({
|
||||
// Updates the URL with the current page number
|
||||
const updatePageUrl = useCallback(
|
||||
(page: number) => {
|
||||
if (currentPath) {
|
||||
if (currentPath && searchParams) {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("page", page.toString());
|
||||
router.replace(`${currentPath}?${params.toString()}`, {
|
||||
|
||||
@@ -1333,10 +1333,10 @@ export function createConnectorValidationSchema(
|
||||
): Yup.ObjectSchema<Record<string, any>> {
|
||||
const configuration = connectorConfigs[connector];
|
||||
|
||||
return Yup.object().shape({
|
||||
const object = Yup.object().shape({
|
||||
access_type: Yup.string().required("Access Type is required"),
|
||||
name: Yup.string().required("Connector Name is required"),
|
||||
...configuration.values.reduce(
|
||||
...[...configuration.values, ...configuration.advanced_values].reduce(
|
||||
(acc, field) => {
|
||||
let schema: any =
|
||||
field.type === "select"
|
||||
@@ -1363,6 +1363,8 @@ export function createConnectorValidationSchema(
|
||||
pruneFreq: Yup.number().min(0, "Prune frequency must be non-negative"),
|
||||
refreshFreq: Yup.number().min(0, "Refresh frequency must be non-negative"),
|
||||
});
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
export const defaultPruneFreqDays = 30; // 30 days
|
||||
|
||||
Reference in New Issue
Block a user