mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-16 23:35:46 +00:00
Compare commits
10 Commits
v2.9.6
...
show_sync_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9aa5dae28 | ||
|
|
439764c5d1 | ||
|
|
cd618edce0 | ||
|
|
d187b7e9e0 | ||
|
|
0d64d70c59 | ||
|
|
cc7b21afd2 | ||
|
|
3db8619d84 | ||
|
|
e0b43dbca5 | ||
|
|
1083d53bb5 | ||
|
|
d3c6ff5e95 |
@@ -92,6 +92,7 @@ from onyx.db.enums import IndexingMode
|
||||
from onyx.db.index_attempt import get_index_attempts_for_cc_pair
|
||||
from onyx.db.index_attempt import get_latest_index_attempts_by_status
|
||||
from onyx.db.index_attempt import get_latest_index_attempts_parallel
|
||||
from onyx.db.models import Connector
|
||||
from onyx.db.models import ConnectorCredentialPair
|
||||
from onyx.db.models import IndexAttempt
|
||||
from onyx.db.models import IndexingStatus
|
||||
@@ -774,6 +775,7 @@ def get_connector_indexing_status(
|
||||
last_finished_status=(
|
||||
latest_finished_attempt.status if latest_finished_attempt else None
|
||||
),
|
||||
perm_sync_completed=cc_pair.last_time_perm_sync is not None,
|
||||
last_status=(
|
||||
latest_index_attempt.status if latest_index_attempt else None
|
||||
),
|
||||
@@ -788,6 +790,7 @@ def get_connector_indexing_status(
|
||||
if latest_index_attempt
|
||||
else None
|
||||
),
|
||||
is_seeded=is_connector_seeded(connector),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1242,9 +1245,18 @@ def get_connector_by_id(
|
||||
|
||||
class BasicCCPairInfo(BaseModel):
|
||||
has_successful_run: bool
|
||||
has_successful_sync_if_needs_sync: bool
|
||||
seeded: bool
|
||||
source: DocumentSource
|
||||
|
||||
|
||||
def is_connector_seeded(connector: Connector) -> bool:
|
||||
return (
|
||||
connector.connector_specific_config.get("base_url")
|
||||
== "https://docs.onyx.app/more/use_cases"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/connector-status")
|
||||
def get_basic_connector_indexing_status(
|
||||
user: User = Depends(current_chat_accesssible_user),
|
||||
@@ -1256,10 +1268,16 @@ def get_basic_connector_indexing_status(
|
||||
get_editable=False,
|
||||
user=user,
|
||||
)
|
||||
|
||||
return [
|
||||
BasicCCPairInfo(
|
||||
has_successful_run=cc_pair.last_successful_index_time is not None,
|
||||
has_successful_sync_if_needs_sync=(
|
||||
cc_pair.last_time_perm_sync is not None
|
||||
or cc_pair.access_type != AccessType.SYNC
|
||||
),
|
||||
source=cc_pair.connector.source,
|
||||
seeded=is_connector_seeded(cc_pair.connector),
|
||||
)
|
||||
for cc_pair in cc_pairs
|
||||
if cc_pair.connector.source != DocumentSource.INGESTION_API
|
||||
|
||||
@@ -218,6 +218,8 @@ class CCPairFullInfo(BaseModel):
|
||||
indexing: bool
|
||||
creator: UUID | None
|
||||
creator_email: str | None
|
||||
last_successful_index_time: datetime | None
|
||||
last_time_perm_sync: datetime | None
|
||||
|
||||
@classmethod
|
||||
def from_models(
|
||||
@@ -258,8 +260,10 @@ class CCPairFullInfo(BaseModel):
|
||||
),
|
||||
number_of_index_attempts=number_of_index_attempts,
|
||||
last_index_attempt_status=last_indexing_status,
|
||||
last_successful_index_time=cc_pair_model.last_successful_index_time,
|
||||
latest_deletion_attempt=latest_deletion_attempt,
|
||||
access_type=cc_pair_model.access_type,
|
||||
last_time_perm_sync=cc_pair_model.last_time_perm_sync,
|
||||
is_editable_for_current_user=is_editable_for_current_user,
|
||||
deletion_failure_message=cc_pair_model.deletion_failure_message,
|
||||
indexing=indexing,
|
||||
@@ -311,9 +315,11 @@ class ConnectorIndexingStatus(ConnectorStatus):
|
||||
last_finished_status: IndexingStatus | None
|
||||
last_status: IndexingStatus | None
|
||||
last_success: datetime | None
|
||||
perm_sync_completed: bool
|
||||
latest_index_attempt: IndexAttemptSnapshot | None
|
||||
docs_indexed: int
|
||||
in_progress: bool
|
||||
is_seeded: bool
|
||||
|
||||
|
||||
class ConnectorCredentialPairIdentifier(BaseModel):
|
||||
|
||||
@@ -16,6 +16,7 @@ def load_settings() -> Settings:
|
||||
kv_store = get_kv_store()
|
||||
try:
|
||||
stored_settings = kv_store.load(KV_SETTINGS_KEY)
|
||||
|
||||
settings = (
|
||||
Settings.model_validate(stored_settings) if stored_settings else Settings()
|
||||
)
|
||||
|
||||
@@ -4,13 +4,10 @@ import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Text from "@/components/ui/text";
|
||||
import { triggerIndexing } from "./lib";
|
||||
import { mutate } from "swr";
|
||||
import { buildCCPairInfoUrl, getTooltipMessage } from "./lib";
|
||||
import { useState } from "react";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ConnectorCredentialPairStatus } from "./types";
|
||||
import { CCPairStatus } from "@/components/Status";
|
||||
import { getCCPairStatusMessage } from "@/lib/ccPair";
|
||||
|
||||
function ReIndexPopup({
|
||||
|
||||
@@ -39,11 +39,14 @@ import EditPropertyModal from "@/components/modals/EditPropertyModal";
|
||||
|
||||
import * as Yup from "yup";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import IndexAttemptErrorsModal from "./IndexAttemptErrorsModal";
|
||||
import usePaginatedFetch from "@/hooks/usePaginatedFetch";
|
||||
import { IndexAttemptSnapshot } from "@/lib/types";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
import { Callout } from "@/components/ui/callout";
|
||||
import { FiAlertCircle } from "react-icons/fi";
|
||||
import { Card, CardHeader } from "@/components/ui/card";
|
||||
|
||||
// synchronize these validations with the SQLAlchemy connector class until we have a
|
||||
// centralized schema for both frontend and backend
|
||||
@@ -378,10 +381,13 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<CCPairStatus
|
||||
status={ccPair.last_index_attempt_status || "not_started"}
|
||||
ccPairStatus={ccPair.status}
|
||||
/>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<CCPairStatus
|
||||
status={ccPair.last_index_attempt_status || "not_started"}
|
||||
ccPairStatus={ccPair.status}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-sm mt-1">
|
||||
Creator:{" "}
|
||||
<b className="text-emphasis">{ccPair.creator_email ?? "Unknown"}</b>
|
||||
@@ -400,6 +406,29 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!ccPair.last_successful_index_time ||
|
||||
(ccPair.access_type === "sync" && !ccPair.last_time_perm_sync)) && (
|
||||
<div className="mt-4">
|
||||
<Card className="max-w-2xl w-fit border-blue-500 bg-blue-50 dark:bg-blue-950/30 dark:border-blue-700 p-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<FiAlertCircle className="h-4 w-4 text-blue-600 dark:text-blue-400 mt-1" />
|
||||
<div>
|
||||
<h4 className="font-semibold text-blue-700 dark:text-blue-400">
|
||||
{!ccPair.last_successful_index_time
|
||||
? "No Successful Indexing"
|
||||
: "Permissions Sync In Progress"}
|
||||
</h4>
|
||||
<p className="text-sm text-blue-600 dark:text-blue-300 mt-1">
|
||||
{!ccPair.last_successful_index_time
|
||||
? "This connector has never been successfully indexed. Documents from this connector will not appear in search results until indexing completes successfully."
|
||||
: "Permissions sync is still in progress for this connector. Some documents may not appear in search results until this process completes."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ccPair.deletion_failure_message &&
|
||||
ccPair.status === ConnectorCredentialPairStatus.DELETING && (
|
||||
<>
|
||||
|
||||
@@ -40,9 +40,11 @@ export interface CCPairFullInfo {
|
||||
access_type: AccessType;
|
||||
is_editable_for_current_user: boolean;
|
||||
deletion_failure_message: string | null;
|
||||
last_time_perm_sync: string | null;
|
||||
indexing: boolean;
|
||||
creator: UUID | null;
|
||||
creator_email: string | null;
|
||||
last_successful_index_time: string | null;
|
||||
}
|
||||
|
||||
export interface PaginatedIndexAttempts {
|
||||
|
||||
@@ -26,6 +26,9 @@ import {
|
||||
FiUnlock,
|
||||
FiRefreshCw,
|
||||
FiPauseCircle,
|
||||
FiCheckCircle,
|
||||
FiAlertCircle,
|
||||
FiAlertTriangle,
|
||||
} from "react-icons/fi";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -77,9 +80,11 @@ function SummaryRow({
|
||||
|
||||
<TableCell>
|
||||
<div className="text-sm text-neutral-500 dark:text-neutral-300">
|
||||
Total Connectors
|
||||
Ready Connectors
|
||||
</div>
|
||||
<div className="text-xl font-semibold">
|
||||
{summary.complete}/{summary.count}
|
||||
</div>
|
||||
<div className="text-xl font-semibold">{summary.count}</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
@@ -211,12 +216,54 @@ border border-border dark:border-neutral-700
|
||||
}}
|
||||
>
|
||||
<TableCell className="">
|
||||
<p className="lg:w-[200px] xl:w-[400px] inline-block ellipsis truncate">
|
||||
{ccPairsIndexingStatus.name}
|
||||
<p className="lg:w-[500px] flex gap-x-2 xl:w-[600px] inline-block ellipsis truncate">
|
||||
<span>{ccPairsIndexingStatus.name}</span>
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{timeAgo(ccPairsIndexingStatus?.last_success) || "-"}
|
||||
<div className="flex items-center gap-x-1">
|
||||
{timeAgo(ccPairsIndexingStatus?.last_success) ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>{timeAgo(ccPairsIndexingStatus?.last_success)}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Last successful indexing time</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="flex gap-x-1 items-center">
|
||||
<FiAlertTriangle className="mr-1 text-yellow-600" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>This connector has never been successfully indexed</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{!ccPairsIndexingStatus.perm_sync_completed &&
|
||||
ccPairsIndexingStatus.access_type === "sync" && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="flex items-center">
|
||||
<FiAlertTriangle className="mr-1 text-yellow-600" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Permissions sync is still in progress</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getActivityBadge()}</TableCell>
|
||||
{isPaidEnterpriseFeaturesEnabled && (
|
||||
@@ -226,12 +273,14 @@ border border-border dark:border-neutral-700
|
||||
Public
|
||||
</Badge>
|
||||
) : ccPairsIndexingStatus.access_type === "sync" ? (
|
||||
<Badge
|
||||
variant={isEditable ? "auto-sync" : "default"}
|
||||
icon={FiRefreshCw}
|
||||
>
|
||||
Auto-Sync
|
||||
</Badge>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Badge
|
||||
variant={isEditable ? "auto-sync" : "default"}
|
||||
icon={FiRefreshCw}
|
||||
>
|
||||
<div className="flex items-center gap-x-1">Auto-Sync</div>
|
||||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
<Badge variant={isEditable ? "private" : "default"} icon={FiLock}>
|
||||
Private
|
||||
@@ -246,16 +295,6 @@ border border-border dark:border-neutral-700
|
||||
errorMsg={ccPairsIndexingStatus?.latest_index_attempt?.error_msg}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isEditable && (
|
||||
<CustomTooltip content="Manage Connector">
|
||||
<FiSettings
|
||||
className="cursor-pointer"
|
||||
onClick={handleManageClick}
|
||||
/>
|
||||
</CustomTooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -321,6 +360,17 @@ export function CCPairIndexingStatusTable({
|
||||
const statuses = grouped[source];
|
||||
summaries[source] = {
|
||||
count: statuses.length,
|
||||
not_ready: statuses.filter(
|
||||
(status) =>
|
||||
status.last_success === null ||
|
||||
(status.connector.access_type === "sync" &&
|
||||
!status.perm_sync_completed)
|
||||
).length,
|
||||
complete: statuses.filter(
|
||||
(status) =>
|
||||
status.last_success !== null &&
|
||||
(status.access_type != "sync" || status.perm_sync_completed)
|
||||
).length,
|
||||
active: statuses.filter(
|
||||
(status) =>
|
||||
status.cc_pair_status === ConnectorCredentialPairStatus.ACTIVE
|
||||
@@ -411,12 +461,14 @@ export function CCPairIndexingStatusTable({
|
||||
credential_json: {},
|
||||
admin_public: false,
|
||||
},
|
||||
perm_sync_completed: false,
|
||||
access_type: "public",
|
||||
docs_indexed: 1000,
|
||||
last_success: "2023-07-01T12:00:00Z",
|
||||
last_finished_status: "success",
|
||||
latest_index_attempt: null,
|
||||
groups: [], // Add this line
|
||||
is_seeded: false,
|
||||
}}
|
||||
isEditable={false}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CheckmarkIcon } from "@/components/icons/icons";
|
||||
|
||||
export function ConnectorCreatedSuccessModal() {
|
||||
const [open, setOpen] = useState(true);
|
||||
const router = useRouter();
|
||||
|
||||
// Close the modal and update the URL to remove the query param
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
router.replace("/admin/indexing/status");
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="sm:max-w-md p-6">
|
||||
<DialogHeader className="flex flex-col items-center text-center gap-4">
|
||||
<div className="flex items-center justify-center w-16 h-16 rounded-full bg-green-100 dark:bg-green-950 transition-all duration-200 animate-in fade-in">
|
||||
<CheckmarkIcon
|
||||
size={32}
|
||||
className="text-green-600 dark:text-green-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<DialogTitle className="text-2xl font-bold">
|
||||
Congratulations!
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-lg">
|
||||
You've successfully created your first connector.
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="bg-neutral-100 dark:bg-neutral-900 p-5 rounded-lg my-4 border border-neutral-200 dark:border-neutral-700 shadow-sm dark:shadow-md dark:shadow-black/10">
|
||||
<h3 className="font-semibold text-lg mb-2 flex items-center">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500 dark:bg-blue-400 mr-2 animate-pulse"></div>
|
||||
Syncing in progress
|
||||
</h3>
|
||||
<p className="text-neutral-600 dark:text-neutral-300 leading-relaxed">
|
||||
It will take some time to sync your documents. You'll know
|
||||
it's complete when the "Last Indexed" field is filled
|
||||
in on the Connectors page.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex justify-center sm:justify-center pt-2">
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
variant="default"
|
||||
size="lg"
|
||||
className="font-medium transition-all duration-200 hover:shadow-md dark:hover:bg-primary/90 dark:hover:shadow-lg dark:hover:shadow-primary/20"
|
||||
>
|
||||
Understood
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -7,10 +7,19 @@ import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import Link from "next/link";
|
||||
import Text from "@/components/ui/text";
|
||||
import { useConnectorCredentialIndexingStatus } from "@/lib/hooks";
|
||||
import { usePopupFromQuery } from "@/components/popup/PopupFromQuery";
|
||||
import {
|
||||
PopupMessages,
|
||||
usePopupFromQuery,
|
||||
} from "@/components/popup/PopupFromQuery";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { ConnectorCreatedSuccessModal } from "./ConnectorCreatedSuccessModal";
|
||||
import { useMemo } from "react";
|
||||
|
||||
function Main() {
|
||||
// Constants
|
||||
const ADD_CONNECTOR_PATH = "/admin/add-connector";
|
||||
|
||||
const ConnectorStatusList = () => {
|
||||
const {
|
||||
data: indexAttemptData,
|
||||
isLoading: indexAttemptIsLoading,
|
||||
@@ -23,10 +32,12 @@ function Main() {
|
||||
error: editableIndexAttemptError,
|
||||
} = useConnectorCredentialIndexingStatus(undefined, true);
|
||||
|
||||
// Handle loading state
|
||||
if (indexAttemptIsLoading || editableIndexAttemptIsLoading) {
|
||||
return <LoadingAnimation text="" />;
|
||||
}
|
||||
|
||||
// Handle error states
|
||||
if (
|
||||
indexAttemptError ||
|
||||
!indexAttemptData ||
|
||||
@@ -42,11 +53,12 @@ function Main() {
|
||||
);
|
||||
}
|
||||
|
||||
// Show empty state when no connectors
|
||||
if (indexAttemptData.length === 0) {
|
||||
return (
|
||||
<Text>
|
||||
It looks like you don't have any connectors setup yet. Visit the{" "}
|
||||
<Link className="text-link" href="/admin/add-connector">
|
||||
<Link className="text-link" href={ADD_CONNECTOR_PATH}>
|
||||
Add Connector
|
||||
</Link>{" "}
|
||||
page to get started!
|
||||
@@ -54,36 +66,59 @@ function Main() {
|
||||
);
|
||||
}
|
||||
|
||||
// sort by source name
|
||||
indexAttemptData.sort((a, b) => {
|
||||
if (a.connector.source < b.connector.source) {
|
||||
return -1;
|
||||
} else if (a.connector.source > b.connector.source) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
// Sort data by source name
|
||||
const sortedIndexAttemptData = [...indexAttemptData].sort((a, b) =>
|
||||
a.connector.source.localeCompare(b.connector.source)
|
||||
);
|
||||
|
||||
return (
|
||||
<CCPairIndexingStatusTable
|
||||
ccPairsIndexingStatuses={indexAttemptData}
|
||||
editableCcPairsIndexingStatuses={editableIndexAttemptData}
|
||||
/>
|
||||
<>
|
||||
<CCPairIndexingStatusTable
|
||||
ccPairsIndexingStatuses={sortedIndexAttemptData}
|
||||
editableCcPairsIndexingStatuses={editableIndexAttemptData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default function Status() {
|
||||
const { popup } = usePopupFromQuery({
|
||||
"connector-created": {
|
||||
message: "Connector created successfully",
|
||||
type: "success",
|
||||
},
|
||||
const searchParams = useSearchParams();
|
||||
const justCreatedConnector =
|
||||
searchParams.get("message") === "connector-created";
|
||||
|
||||
// Use data to determine if we should show the popup or modal
|
||||
const { data: indexAttemptData, isLoading: indexAttemptIsLoading } =
|
||||
useConnectorCredentialIndexingStatus();
|
||||
|
||||
// Only show popup if we're not showing the success modal and there's exactly one seeded connector
|
||||
const showSuccessModal = useMemo(() => {
|
||||
return (
|
||||
!indexAttemptIsLoading &&
|
||||
indexAttemptData &&
|
||||
justCreatedConnector &&
|
||||
indexAttemptData.filter((attempt) => attempt.is_seeded).length === 1
|
||||
);
|
||||
}, [indexAttemptIsLoading, indexAttemptData]);
|
||||
|
||||
// Create popup messages based on query parameters
|
||||
const popupMessages: PopupMessages = {
|
||||
"connector-deleted": {
|
||||
message: "Connector deleted successfully",
|
||||
type: "success",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Conditionally add connector-created message
|
||||
if (!showSuccessModal) {
|
||||
Object.assign(popupMessages, {
|
||||
"connector-created": {
|
||||
message: "Connector created successfully",
|
||||
type: "success",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { popup } = usePopupFromQuery(popupMessages);
|
||||
|
||||
return (
|
||||
<div className="mx-auto container">
|
||||
@@ -92,13 +127,15 @@ export default function Status() {
|
||||
icon={<NotebookIcon size={32} />}
|
||||
title="Existing Connectors"
|
||||
farRightElement={
|
||||
<Link href="/admin/add-connector">
|
||||
<Link href={ADD_CONNECTOR_PATH}>
|
||||
<Button variant="success-reverse">Add Connector</Button>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<Main />
|
||||
{showSuccessModal && <ConnectorCreatedSuccessModal />}
|
||||
|
||||
<ConnectorStatusList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ import { DocumentResults } from "./documentSidebar/DocumentResults";
|
||||
import { OnyxInitializingLoader } from "@/components/OnyxInitializingLoader";
|
||||
import { FeedbackModal } from "./modal/FeedbackModal";
|
||||
import { ShareChatSessionModal } from "./modal/ShareChatSessionModal";
|
||||
import { FiArrowDown } from "react-icons/fi";
|
||||
import { FiArrowDown, FiExternalLink } from "react-icons/fi";
|
||||
import { ChatIntro } from "./ChatIntro";
|
||||
import { AIMessage, HumanMessage } from "./message/Messages";
|
||||
import { StarterMessages } from "../../components/assistants/StarterMessage";
|
||||
@@ -138,6 +138,10 @@ import { useSidebarShortcut } from "@/lib/browserUtilities";
|
||||
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
|
||||
import { ChatSearchModal } from "./chat_search/ChatSearchModal";
|
||||
import { ErrorBanner } from "./message/Resubmit";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { XIcon } from "@/components/icons/icons";
|
||||
import { HIDE_NO_SOURCES_MESSAGE_COOKIE_NAME } from "@/lib/constants";
|
||||
import Link from "next/link";
|
||||
|
||||
const TEMP_USER_MESSAGE_ID = -1;
|
||||
const TEMP_ASSISTANT_MESSAGE_ID = -2;
|
||||
@@ -166,9 +170,19 @@ export function ChatPage({
|
||||
folders,
|
||||
shouldShowWelcomeModal,
|
||||
refreshChatSessions,
|
||||
showNoSourcesMessage: initialShowNoSourcesMessage,
|
||||
proSearchToggled,
|
||||
} = useChatContext();
|
||||
|
||||
const [showNoSourcesMessage, setShowNoSourcesMessage] = useState(
|
||||
initialShowNoSourcesMessage
|
||||
);
|
||||
|
||||
const dismissNoSourcesMessage = () => {
|
||||
Cookies.set(HIDE_NO_SOURCES_MESSAGE_COOKIE_NAME, "true");
|
||||
setShowNoSourcesMessage(false);
|
||||
};
|
||||
|
||||
const defaultAssistantIdRaw = searchParams.get(SEARCH_PARAM_NAMES.PERSONA_ID);
|
||||
const defaultAssistantId = defaultAssistantIdRaw
|
||||
? parseInt(defaultAssistantIdRaw)
|
||||
@@ -201,6 +215,7 @@ export function ChatPage({
|
||||
// available in server-side components
|
||||
const settings = useContext(SettingsContext);
|
||||
const enterpriseSettings = settings?.enterpriseSettings;
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
const [documentSidebarVisible, setDocumentSidebarVisible] = useState(false);
|
||||
const [proSearchEnabled, setProSearchEnabled] = useState(proSearchToggled);
|
||||
@@ -3118,6 +3133,39 @@ export function ChatPage({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showNoSourcesMessage && (
|
||||
<div className="pointer-events-auto flex items-center justify-center w-full mb-2">
|
||||
<div className="bg-transparent border border-neutral-200 dark:border-neutral-700 rounded-lg p-3 flex items-center justify-between w-fit shadow-sm dark:shadow-md dark:shadow-black/20">
|
||||
<div className="flex gap-x-2 items-center">
|
||||
<p className="text-neutral-500 dark:text-neutral-300 text-sm font-medium">
|
||||
No sources have been completed. Answers will
|
||||
only include the seeded doc
|
||||
</p>
|
||||
<Link
|
||||
href="/admin/indexing/status"
|
||||
className="underline flex items-center inline-flex"
|
||||
>
|
||||
<ExternalLinkIcon
|
||||
className="text-neutral-500 hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200"
|
||||
size={16}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="w-[1px] ml-2 -my-3 h-auto self-stretch bg-neutral-200 dark:bg-neutral-700"></div>
|
||||
<button
|
||||
className="ml-2 text-neutral-600 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200 transition-colors"
|
||||
aria-label="Dismiss"
|
||||
onClick={dismissNoSourcesMessage}
|
||||
>
|
||||
<XIcon
|
||||
size={16}
|
||||
className="text-neutral-500 cursor-pointer hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pointer-events-auto w-[95%] mx-auto relative mb-8">
|
||||
<ChatInputBar
|
||||
proSearchEnabled={proSearchEnabled}
|
||||
@@ -3218,7 +3266,6 @@ export function ChatPage({
|
||||
</div>
|
||||
<FixedLogo backgroundToggled={sidebarVisible || showHistorySidebar} />
|
||||
</div>
|
||||
{/* Right Sidebar - DocumentSidebar */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -37,6 +37,7 @@ export default async function Layout({
|
||||
ccPairs,
|
||||
inputPrompts,
|
||||
proSearchToggled,
|
||||
showNoSourcesMessage,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
@@ -59,6 +60,7 @@ export default async function Layout({
|
||||
openedFolders,
|
||||
shouldShowWelcomeModal,
|
||||
defaultAssistantId,
|
||||
showNoSourcesMessage,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -71,6 +71,8 @@ import remarkMath from "remark-math";
|
||||
import rehypeKatex from "rehype-katex";
|
||||
import "katex/dist/katex.min.css";
|
||||
import { copyAll, handleCopy } from "./copyingUtils";
|
||||
import Link from "next/link";
|
||||
import { NoDocuments } from "./NoDocuments";
|
||||
|
||||
const TOOLS_WITH_CUSTOM_HANDLING = [
|
||||
SEARCH_TOOL_NAME,
|
||||
@@ -485,7 +487,7 @@ export const AIMessage = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{docs && docs.length > 0 && (
|
||||
{docs && docs.length > 0 ? (
|
||||
<div
|
||||
className={`mobile:hidden ${
|
||||
(query ||
|
||||
@@ -518,6 +520,8 @@ export const AIMessage = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
toolCall?.tool_result && <NoDocuments />
|
||||
)}
|
||||
|
||||
{content || files ? (
|
||||
@@ -746,8 +750,8 @@ function MessageSwitcher({
|
||||
disableForStreaming
|
||||
? () => null
|
||||
: currentPage === 1
|
||||
? undefined
|
||||
: handlePrevious
|
||||
? undefined
|
||||
: handlePrevious
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -774,8 +778,8 @@ function MessageSwitcher({
|
||||
disableForStreaming
|
||||
? () => null
|
||||
: currentPage === totalPages
|
||||
? undefined
|
||||
: handleNext
|
||||
? undefined
|
||||
: handleNext
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
20
web/src/app/chat/message/NoDocuments.tsx
Normal file
20
web/src/app/chat/message/NoDocuments.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import Link from "next/link";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
|
||||
export function NoDocuments() {
|
||||
const { isAdmin } = useUser();
|
||||
|
||||
return (
|
||||
<div className="text-xs py-2 text-neutral-800 dark:text-neutral-200 italic">
|
||||
No documents found. This may be because the documents are still indexing
|
||||
or syncing.{" "}
|
||||
{isAdmin ? (
|
||||
<Link href="/admin/indexing/status" className="underline">
|
||||
Check indexing status
|
||||
</Link>
|
||||
) : (
|
||||
<p>Check with your admin</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,10 +11,13 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { OnyxDocument } from "@/lib/search/interfaces";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FiCheck, FiEdit2, FiSearch, FiX } from "react-icons/fi";
|
||||
import { NoDocuments } from "./NoDocuments";
|
||||
|
||||
export function ShowHideDocsButton({
|
||||
messageId,
|
||||
@@ -63,6 +66,7 @@ export function SearchSummary({
|
||||
const [isOverflowed, setIsOverflowed] = useState(false);
|
||||
const searchingForRef = useRef<HTMLDivElement>(null);
|
||||
const editQueryRef = useRef<HTMLInputElement>(null);
|
||||
const { isAdmin } = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
const checkOverflow = () => {
|
||||
@@ -120,7 +124,9 @@ export function SearchSummary({
|
||||
|
||||
<div className="desktop:hidden">
|
||||
{" "}
|
||||
{docs && (
|
||||
{docs && docs.length === 0 ? (
|
||||
finished && <NoDocuments />
|
||||
) : (
|
||||
<button
|
||||
className="cursor-pointer mr-2 flex items-center gap-0.5"
|
||||
onClick={() => toggleDocumentSelection()}
|
||||
|
||||
@@ -36,6 +36,7 @@ export default async function GalleryPage(props: {
|
||||
defaultAssistantId,
|
||||
inputPrompts,
|
||||
proSearchToggled,
|
||||
showNoSourcesMessage,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
@@ -56,6 +57,7 @@ export default async function GalleryPage(props: {
|
||||
openedFolders,
|
||||
shouldShowWelcomeModal,
|
||||
defaultAssistantId,
|
||||
showNoSourcesMessage,
|
||||
}}
|
||||
>
|
||||
{shouldShowWelcomeModal && (
|
||||
|
||||
@@ -62,6 +62,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
||||
shouldShowWelcomeModal,
|
||||
ccPairs,
|
||||
inputPrompts,
|
||||
showNoSourcesMessage,
|
||||
proSearchToggled,
|
||||
} = data;
|
||||
|
||||
@@ -83,6 +84,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
||||
openedFolders,
|
||||
shouldShowWelcomeModal,
|
||||
defaultAssistantId,
|
||||
showNoSourcesMessage,
|
||||
}}
|
||||
>
|
||||
<ClientLayout
|
||||
|
||||
@@ -35,6 +35,7 @@ interface ChatContextProps {
|
||||
refreshInputPrompts: () => Promise<void>;
|
||||
inputPrompts: InputPrompt[];
|
||||
proSearchToggled: boolean;
|
||||
showNoSourcesMessage: boolean;
|
||||
}
|
||||
|
||||
const ChatContext = createContext<ChatContextProps | undefined>(undefined);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Credential,
|
||||
} from "@/lib/connectors/credentials";
|
||||
import { Connector } from "@/lib/connectors/connectors";
|
||||
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
|
||||
|
||||
const CredentialSelectionTable = ({
|
||||
credentials,
|
||||
@@ -188,35 +189,16 @@ export default function ModifyCredential({
|
||||
return (
|
||||
<>
|
||||
{confirmDeletionCredential != null && (
|
||||
<Modal
|
||||
onOutsideClick={() => setConfirmDeletionCredential(null)}
|
||||
className="max-w-sm"
|
||||
>
|
||||
<>
|
||||
<p className="text-lg mb-2">
|
||||
Are you sure you want to delete this credential? You cannot delete
|
||||
credentials that are linked to live connectors.
|
||||
</p>
|
||||
<div className="mt-6 flex justify-between">
|
||||
<button
|
||||
className="rounded py-1.5 px-2 bg-background-800 text-text-200"
|
||||
onClick={async () => {
|
||||
await onDeleteCredential(confirmDeletionCredential);
|
||||
setConfirmDeletionCredential(null);
|
||||
}}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmDeletionCredential(null)}
|
||||
className="rounded py-1.5 px-2 bg-background-150 text-text-800"
|
||||
>
|
||||
{" "}
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
<ConfirmEntityModal
|
||||
entityType="Credential"
|
||||
entityName={confirmDeletionCredential.name || "Unnamed Credential"}
|
||||
onClose={() => setConfirmDeletionCredential(null)}
|
||||
onSubmit={async () => {
|
||||
await onDeleteCredential(confirmDeletionCredential);
|
||||
setConfirmDeletionCredential(null);
|
||||
}}
|
||||
additionalDetails="You cannot delete credentials that are linked to live connectors."
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="mb-0">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { usePopup } from "../admin/connectors/Popup";
|
||||
import { PopupSpec } from "../admin/connectors/Popup";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface PopupMessages {
|
||||
export interface PopupMessages {
|
||||
[key: string]: PopupSpec;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ const badgeVariants = cva(
|
||||
"border-orange-200 bg-orange-50 text-orange-600 dark:border-orange-700 dark:bg-orange-900 dark:text-orange-50",
|
||||
outline:
|
||||
"border-neutral-200 bg-neutral-50 text-neutral-600 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-50",
|
||||
|
||||
fuchsia:
|
||||
"border-fuchsia-200 bg-fuchsia-50 text-fuchsia-700 dark:border-fuchsia-700 dark:bg-fuchsia-900 dark:text-fuchsia-100",
|
||||
|
||||
purple:
|
||||
"border-purple-200 bg-purple-50 text-purple-700 dark:border-purple-700 dark:bg-purple-900 dark:text-purple-100",
|
||||
public:
|
||||
@@ -39,6 +43,9 @@ const badgeVariants = cva(
|
||||
"border-green-200 bg-emerald-50 text-green-600 dark:border-green-600 dark:bg-green-900 dark:text-green-50",
|
||||
default:
|
||||
"border-neutral-200 bg-neutral-50 text-neutral-600 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-50",
|
||||
warning:
|
||||
"border-orange-200 bg-orange-50 text-orange-600 dark:border-orange-700 dark:bg-orange-900 dark:text-orange-50",
|
||||
|
||||
secondary:
|
||||
"border-neutral-200 bg-neutral-50 text-neutral-600 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-50",
|
||||
destructive:
|
||||
|
||||
@@ -47,7 +47,7 @@ const TooltipContent = React.forwardRef<
|
||||
"bg-neutral-900 dark:bg-neutral-200 dark:text-neutral-900"
|
||||
}
|
||||
${width || "max-w-40"}
|
||||
|
||||
text-wrap
|
||||
px-2 py-1.5 text-xs 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`,
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "@/components/resizable/constants";
|
||||
import { hasCompletedWelcomeFlowSS } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
|
||||
import {
|
||||
HIDE_NO_SOURCES_MESSAGE_COOKIE_NAME,
|
||||
NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN,
|
||||
NEXT_PUBLIC_ENABLE_CHROME_EXTENSION,
|
||||
} from "../constants";
|
||||
@@ -47,6 +48,7 @@ interface FetchChatDataResult {
|
||||
shouldShowWelcomeModal: boolean;
|
||||
inputPrompts: InputPrompt[];
|
||||
proSearchToggled: boolean;
|
||||
showNoSourcesMessage: boolean;
|
||||
}
|
||||
|
||||
export async function fetchChatData(searchParams: {
|
||||
@@ -130,6 +132,21 @@ export async function fetchChatData(searchParams: {
|
||||
} else {
|
||||
console.log(`Failed to fetch connectors - ${ccPairsResponse?.status}`);
|
||||
}
|
||||
|
||||
const hideNoSourcesMessage =
|
||||
requestCookies
|
||||
.get(HIDE_NO_SOURCES_MESSAGE_COOKIE_NAME)
|
||||
?.value.toLowerCase() === "true";
|
||||
|
||||
const showNoSourcesMessage = !(
|
||||
ccPairs.some(
|
||||
(ccPair) =>
|
||||
!ccPair.seeded &&
|
||||
ccPair.has_successful_run &&
|
||||
ccPair.has_successful_sync_if_needs_sync
|
||||
) || hideNoSourcesMessage
|
||||
);
|
||||
|
||||
const availableSources: ValidSources[] = [];
|
||||
ccPairs.forEach((ccPair) => {
|
||||
if (!availableSources.includes(ccPair.source)) {
|
||||
@@ -234,5 +251,6 @@ export async function fetchChatData(searchParams: {
|
||||
shouldShowWelcomeModal,
|
||||
inputPrompts,
|
||||
proSearchToggled,
|
||||
showNoSourcesMessage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,7 +45,9 @@ export async function updateConnectorCredentialPairName(
|
||||
newName: string
|
||||
): Promise<Response> {
|
||||
return fetch(
|
||||
`/api/manage/admin/cc-pair/${ccPairId}/name?new_name=${encodeURIComponent(newName)}`,
|
||||
`/api/manage/admin/cc-pair/${ccPairId}/name?new_name=${encodeURIComponent(
|
||||
newName
|
||||
)}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
|
||||
@@ -94,3 +94,5 @@ export const NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK =
|
||||
|
||||
export const NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY =
|
||||
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
|
||||
|
||||
export const HIDE_NO_SOURCES_MESSAGE_COOKIE_NAME = "hide_no_sources_message";
|
||||
|
||||
@@ -158,9 +158,11 @@ export interface ConnectorIndexingStatus<
|
||||
last_success: string | null;
|
||||
last_status: ValidStatuses | null;
|
||||
last_finished_status: ValidStatuses | null;
|
||||
perm_sync_completed: boolean;
|
||||
cc_pair_status: ConnectorCredentialPairStatus;
|
||||
latest_index_attempt: IndexAttemptSnapshot | null;
|
||||
docs_indexed: number;
|
||||
is_seeded: boolean;
|
||||
}
|
||||
|
||||
export interface OAuthPrepareAuthorizationResponse {
|
||||
@@ -202,6 +204,8 @@ export interface OAuthConfluenceFinalizeResponse {
|
||||
export interface CCPairBasicInfo {
|
||||
has_successful_run: boolean;
|
||||
source: ValidSources;
|
||||
seeded: boolean;
|
||||
has_successful_sync_if_needs_sync: boolean;
|
||||
}
|
||||
|
||||
export type ConnectorSummary = {
|
||||
@@ -209,6 +213,8 @@ export type ConnectorSummary = {
|
||||
active: number;
|
||||
public: number;
|
||||
totalDocsIndexed: number;
|
||||
not_ready: number;
|
||||
complete: number;
|
||||
errors: number; // New field for error count
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user