diff --git a/backend/danswer/background/update.py b/backend/danswer/background/update.py
index 5ba1fbde9..ac8e876d8 100755
--- a/backend/danswer/background/update.py
+++ b/backend/danswer/background/update.py
@@ -2,6 +2,8 @@ import time
from typing import cast
from danswer.configs.constants import DocumentSource
+from danswer.connectors.factory import build_connector
+from danswer.connectors.models import InputType
from danswer.connectors.slack.config import get_pull_frequency
from danswer.connectors.slack.pull import PeriodicSlackLoader
from danswer.connectors.web.pull import WebLoader
@@ -35,6 +37,8 @@ def run_update() -> None:
current_time = int(time.time())
# Slack
+ # TODO (chris): make Slack use the same approach as other connectors /
+ # make other connectors periodic
try:
pull_frequency = get_pull_frequency()
except ConfigNotFoundError:
@@ -56,17 +60,15 @@ def run_update() -> None:
indexing_pipeline(doc_batch)
dynamic_config_store.store(last_slack_pull_key, current_time)
- # Web
# TODO (chris): make this more efficient / in a single transaction to
# prevent race conditions across multiple background jobs. For now,
# this assumes we only ever run a single background job at a time
- # TODO (chris): make this generic for all pull connectors (not just web)
not_started_index_attempts = fetch_index_attempts(
- sources=[DocumentSource.WEB], statuses=[IndexingStatus.NOT_STARTED]
+ input_types=[InputType.PULL], statuses=[IndexingStatus.NOT_STARTED]
)
for not_started_index_attempt in not_started_index_attempts:
logger.info(
- "Attempting to index website with IndexAttempt id: "
+ "Attempting to index with IndexAttempt id: "
f"{not_started_index_attempt.id}, source: "
f"{not_started_index_attempt.source}, input_type: "
f"{not_started_index_attempt.input_type}, and connector_specific_config: "
@@ -78,17 +80,25 @@ def run_update() -> None:
)
error_msg = None
- base_url = not_started_index_attempt.connector_specific_config["url"]
try:
# TODO (chris): spawn processes to parallelize / take advantage of
# multiple cores + implement retries
+ connector = build_connector(
+ source=not_started_index_attempt.source,
+ input_type=InputType.PULL,
+ connector_specific_config=not_started_index_attempt.connector_specific_config,
+ )
+
document_ids: list[str] = []
- for doc_batch in WebLoader(base_url=base_url).load():
+ for doc_batch in connector.load():
chunks = indexing_pipeline(doc_batch)
document_ids.extend([chunk.source_document.id for chunk in chunks])
except Exception as e:
logger.exception(
- "Failed to index website with url %s due to: %s", base_url, e
+ "Failed to index for source %s with config %s due to: %s",
+ not_started_index_attempt.source,
+ not_started_index_attempt.connector_specific_config,
+ e,
)
error_msg = str(e)
diff --git a/backend/danswer/connectors/factory.py b/backend/danswer/connectors/factory.py
new file mode 100644
index 000000000..dd3a20106
--- /dev/null
+++ b/backend/danswer/connectors/factory.py
@@ -0,0 +1,40 @@
+from typing import Any
+
+from danswer.configs.constants import DocumentSource
+from danswer.connectors.github.batch import BatchGithubLoader
+from danswer.connectors.google_drive.batch import BatchGoogleDriveLoader
+from danswer.connectors.interfaces import PullLoader
+from danswer.connectors.interfaces import RangePullLoader
+from danswer.connectors.models import InputType
+from danswer.connectors.slack.batch import BatchSlackLoader
+from danswer.connectors.slack.pull import PeriodicSlackLoader
+from danswer.connectors.web.pull import WebLoader
+
+
+class ConnectorMissingException(Exception):
+ pass
+
+
+def build_connector(
+ source: DocumentSource,
+ input_type: InputType,
+ connector_specific_config: dict[str, Any],
+) -> PullLoader | RangePullLoader:
+ if source == DocumentSource.SLACK:
+ if input_type == InputType.PULL:
+ return PeriodicSlackLoader(**connector_specific_config)
+ if input_type == InputType.LOAD_STATE:
+ return BatchSlackLoader(**connector_specific_config)
+ elif source == DocumentSource.GOOGLE_DRIVE:
+ if input_type == InputType.PULL:
+ return BatchGoogleDriveLoader(**connector_specific_config)
+ elif source == DocumentSource.GITHUB:
+ if input_type == InputType.PULL:
+ return BatchGithubLoader(**connector_specific_config)
+ elif source == DocumentSource.WEB:
+ if input_type == InputType.PULL:
+ return WebLoader(**connector_specific_config)
+
+ raise ConnectorMissingException(
+ f"Connector not found for source={source}, input_type={input_type}"
+ )
diff --git a/backend/danswer/connectors/interfaces.py b/backend/danswer/connectors/interfaces.py
index d10463b91..f2977e0fc 100644
--- a/backend/danswer/connectors/interfaces.py
+++ b/backend/danswer/connectors/interfaces.py
@@ -9,6 +9,7 @@ from danswer.connectors.models import Document
SecondsSinceUnixEpoch = float
+# TODO (chris): rename from Loader -> Connector
class PullLoader:
@abc.abstractmethod
def load(self) -> Generator[List[Document], None, None]:
diff --git a/backend/danswer/db/index_attempt.py b/backend/danswer/db/index_attempt.py
index 05bbe9717..dd4c985b2 100644
--- a/backend/danswer/db/index_attempt.py
+++ b/backend/danswer/db/index_attempt.py
@@ -1,4 +1,5 @@
from danswer.configs.constants import DocumentSource
+from danswer.connectors.models import InputType
from danswer.db.engine import build_engine
from danswer.db.models import IndexAttempt
from danswer.db.models import IndexingStatus
@@ -20,6 +21,7 @@ def fetch_index_attempts(
*,
sources: list[DocumentSource] | None = None,
statuses: list[IndexingStatus] | None = None,
+ input_types: list[InputType] | None = None,
) -> list[IndexAttempt]:
with Session(build_engine(), future=True, expire_on_commit=False) as session:
stmt = select(IndexAttempt)
@@ -27,6 +29,8 @@ def fetch_index_attempts(
stmt = stmt.where(IndexAttempt.source.in_(sources))
if statuses:
stmt = stmt.where(IndexAttempt.status.in_(statuses))
+ if input_types:
+ stmt = stmt.where(IndexAttempt.input_type.in_(input_types))
results = session.scalars(stmt)
return list(results.all())
diff --git a/backend/danswer/server/admin.py b/backend/danswer/server/admin.py
index eddcef82f..a43ec5cb8 100644
--- a/backend/danswer/server/admin.py
+++ b/backend/danswer/server/admin.py
@@ -1,6 +1,9 @@
+from typing import Any
+
from danswer.auth.users import current_admin_user
from danswer.configs.constants import DocumentSource
from danswer.configs.constants import NO_AUTH_USER
+from danswer.connectors.factory import build_connector
from danswer.connectors.google_drive.connector_auth import get_auth_url
from danswer.connectors.google_drive.connector_auth import get_drive_tokens
from danswer.connectors.google_drive.connector_auth import save_access_tokens
@@ -19,11 +22,10 @@ from danswer.server.models import AuthStatus
from danswer.server.models import AuthUrl
from danswer.server.models import GDriveCallback
from danswer.server.models import IndexAttemptSnapshot
-from danswer.server.models import ListWebsiteIndexAttemptsResponse
-from danswer.server.models import WebIndexAttemptRequest
from danswer.utils.logging import setup_logger
from fastapi import APIRouter
from fastapi import Depends
+from pydantic import BaseModel
router = APIRouter(prefix="/admin")
@@ -67,29 +69,51 @@ def modify_slack_config(
update_slack_config(slack_config)
-@router.post("/connectors/web/index-attempt", status_code=201)
-def index_website(
- web_index_attempt_request: WebIndexAttemptRequest,
+class IndexAttemptRequest(BaseModel):
+ input_type: InputType = InputType.PULL
+ connector_specific_config: dict[str, Any]
+
+
+@router.post("/connectors/{source}/index-attempt", status_code=201)
+def index(
+ source: DocumentSource,
+ index_attempt_request: IndexAttemptRequest,
_: User = Depends(current_admin_user),
) -> None:
- index_request = IndexAttempt(
- source=DocumentSource.WEB,
- input_type=InputType.PULL,
- connector_specific_config={"url": web_index_attempt_request.url},
- status=IndexingStatus.NOT_STARTED,
+ # validate that the connector specified by the source / input_type combination
+ # exists AND that the connector_specific_config is valid for that connector type
+ build_connector(
+ source=source,
+ input_type=index_attempt_request.input_type,
+ connector_specific_config=index_attempt_request.connector_specific_config,
+ )
+
+ # once validated, insert the index attempt into the database where it will
+ # get picked up by a background job
+ insert_index_attempt(
+ index_attempt=IndexAttempt(
+ source=source,
+ input_type=index_attempt_request.input_type,
+ connector_specific_config=index_attempt_request.connector_specific_config,
+ status=IndexingStatus.NOT_STARTED,
+ )
)
- insert_index_attempt(index_request)
-@router.get("/connectors/web/index-attempt")
-def list_website_index_attempts(
+class ListIndexAttemptsResponse(BaseModel):
+ index_attempts: list[IndexAttemptSnapshot]
+
+
+@router.get("/connectors/{source}/index-attempt")
+def list_index_attempts(
+ source: DocumentSource,
_: User = Depends(current_admin_user),
-) -> ListWebsiteIndexAttemptsResponse:
- index_attempts = fetch_index_attempts(sources=[DocumentSource.WEB])
- return ListWebsiteIndexAttemptsResponse(
+) -> ListIndexAttemptsResponse:
+ index_attempts = fetch_index_attempts(sources=[source])
+ return ListIndexAttemptsResponse(
index_attempts=[
IndexAttemptSnapshot(
- url=index_attempt.connector_specific_config["url"],
+ connector_specific_config=index_attempt.connector_specific_config,
status=index_attempt.status,
time_created=index_attempt.time_created,
time_updated=index_attempt.time_updated,
diff --git a/backend/danswer/server/models.py b/backend/danswer/server/models.py
index 530ed995c..f2e52bc50 100644
--- a/backend/danswer/server/models.py
+++ b/backend/danswer/server/models.py
@@ -1,4 +1,5 @@
from datetime import datetime
+from typing import Any
from danswer.datastores.interfaces import DatastoreFilter
from danswer.db.models import IndexingStatus
@@ -49,12 +50,8 @@ class UserByEmail(BaseModel):
user_email: str
-class WebIndexAttemptRequest(BaseModel):
- url: str
-
-
class IndexAttemptSnapshot(BaseModel):
- url: str
+ connector_specific_config: dict[str, Any]
status: IndexingStatus
time_created: datetime
time_updated: datetime
diff --git a/web/src/app/admin/connectors/github/page.tsx b/web/src/app/admin/connectors/github/page.tsx
new file mode 100644
index 000000000..1504ed2be
--- /dev/null
+++ b/web/src/app/admin/connectors/github/page.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import * as Yup from "yup";
+import { IndexForm } from "@/components/admin/connectors/Form";
+import {
+ ConnectorStatus,
+ ReccuringConnectorStatus,
+} from "@/components/admin/connectors/RecurringConnectorStatus";
+import { GithubIcon } from "@/components/icons/icons";
+import { TextFormField } from "@/components/admin/connectors/Field";
+
+export default function Page() {
+ return (
+
+
+
+
Github PRs
+
+
+
+ Status
+
+
+
+ {/* TODO: make this periodic */}
+
+ Request Indexing
+
+
+
+
+
+ >
+ }
+ validationSchema={Yup.object().shape({
+ repo_owner: Yup.string().required(
+ "Please enter the owner of the repo scrape e.g. danswer-ai"
+ ),
+ repo_name: Yup.string().required(
+ "Please enter the name of the repo scrape e.g. danswer "
+ ),
+ })}
+ initialValues={{
+ repo_owner: "",
+ repo_name: "",
+ }}
+ onSubmit={(isSuccess) => console.log(isSuccess)}
+ />
+
+
+ );
+}
diff --git a/web/src/app/admin/connectors/interfaces.ts b/web/src/app/admin/connectors/interfaces.ts
new file mode 100644
index 000000000..fa32867e9
--- /dev/null
+++ b/web/src/app/admin/connectors/interfaces.ts
@@ -0,0 +1,17 @@
+export interface SlackConfig {
+ slack_bot_token: string;
+ workspace_id: string;
+ pull_frequency: number;
+}
+
+export interface IndexAttempt {
+ connector_specific_config: { [key: string]: any };
+ status: "success" | "failure" | "in_progress" | "not_started";
+ time_created: string;
+ time_updated: string;
+ docs_indexed: number;
+}
+
+export interface ListIndexingResponse {
+ index_attempts: IndexAttempt[];
+}
diff --git a/web/src/app/admin/connectors/slack/SlackForm.tsx b/web/src/app/admin/connectors/slack/SlackForm.tsx
index 2b1b1adff..9dc23234c 100644
--- a/web/src/app/admin/connectors/slack/SlackForm.tsx
+++ b/web/src/app/admin/connectors/slack/SlackForm.tsx
@@ -3,7 +3,7 @@ import { Formik, Form, FormikHelpers } from "formik";
import * as Yup from "yup";
import { Popup } from "../../../../components/admin/connectors/Popup";
import { TextFormField } from "../../../../components/admin/connectors/Field";
-import { SlackConfig } from "./interfaces";
+import { SlackConfig } from "../interfaces";
const validationSchema = Yup.object().shape({
slack_bot_token: Yup.string().required("Please enter your Slack Bot Token"),
diff --git a/web/src/app/admin/connectors/slack/interfaces.ts b/web/src/app/admin/connectors/slack/interfaces.ts
deleted file mode 100644
index daa89952d..000000000
--- a/web/src/app/admin/connectors/slack/interfaces.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export interface SlackConfig {
- slack_bot_token: string;
- workspace_id: string;
- pull_frequency: number;
-}
-
-// interface SlackIndexAttempt {}
-
-// interface ListSlackIndexingResponse {
-// index_attempts: SlackIndexAttempt[];
-// }
diff --git a/web/src/app/admin/connectors/slack/page.tsx b/web/src/app/admin/connectors/slack/page.tsx
index d24e64d67..7a8727cc5 100644
--- a/web/src/app/admin/connectors/slack/page.tsx
+++ b/web/src/app/admin/connectors/slack/page.tsx
@@ -8,7 +8,7 @@ import { SlackForm } from "@/app/admin/connectors/slack/SlackForm";
import { SlackIcon } from "@/components/icons/icons";
import { fetcher } from "@/lib/fetcher";
import useSWR, { useSWRConfig } from "swr";
-import { SlackConfig } from "./interfaces";
+import { SlackConfig } from "../interfaces";
import { ThinkingAnimation } from "@/components/Thinking";
const MainSection = () => {
@@ -47,6 +47,7 @@ const MainSection = () => {
? ConnectorStatus.Running
: ConnectorStatus.NotSetup
}
+ source="slack"
/>
}
diff --git a/web/src/app/admin/connectors/web/WebIndexForm.tsx b/web/src/app/admin/connectors/web/WebIndexForm.tsx
deleted file mode 100644
index 3e1ef532e..000000000
--- a/web/src/app/admin/connectors/web/WebIndexForm.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import React, { useState } from "react";
-import { Formik, Form, FormikHelpers } from "formik";
-import * as Yup from "yup";
-import { Popup } from "../../../../components/admin/connectors/Popup";
-import { TextFormField } from "../../../../components/admin/connectors/Field";
-
-interface FormData {
- url: string;
-}
-
-const validationSchema = Yup.object().shape({
- url: Yup.string().required(
- "Please enter the website URL to scrape e.g. https://docs.github.com/en/actions"
- ),
-});
-
-const handleSubmit = async (
- values: FormData,
- { setSubmitting }: FormikHelpers,
- setPopup: (
- popup: { message: string; type: "success" | "error" } | null
- ) => void
-): Promise => {
- setSubmitting(true);
- let isSuccess = false;
- try {
- const response = await fetch("/api/admin/connectors/web/index-attempt", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(values),
- });
-
- if (response.ok) {
- isSuccess = true;
- setPopup({ message: "Success!", type: "success" });
- } else {
- const errorData = await response.json();
- setPopup({ message: `Error: ${errorData.detail}`, type: "error" });
- }
- } catch (error) {
- setPopup({ message: `Error: ${error}`, type: "error" });
- } finally {
- setSubmitting(false);
- setTimeout(() => {
- setPopup(null);
- }, 3000);
- return isSuccess;
- }
-};
-
-interface SlackFormProps {
- onSubmit: (isSuccess: boolean) => void;
-}
-
-export const WebIndexForm: React.FC = ({ onSubmit }) => {
- const [popup, setPopup] = useState<{
- message: string;
- type: "success" | "error";
- } | null>(null);
-
- return (
- <>
- {popup && }
-
- handleSubmit(values, formikHelpers, setPopup).then((isSuccess) =>
- onSubmit(isSuccess)
- )
- }
- >
- {({ isSubmitting }) => (
-
- )}
-
- >
- );
-};
diff --git a/web/src/app/admin/connectors/web/page.tsx b/web/src/app/admin/connectors/web/page.tsx
index 99247652c..3fde88e69 100644
--- a/web/src/app/admin/connectors/web/page.tsx
+++ b/web/src/app/admin/connectors/web/page.tsx
@@ -1,25 +1,16 @@
"use client";
import useSWR, { useSWRConfig } from "swr";
+import * as Yup from "yup";
import { BasicTable } from "@/components/admin/connectors/BasicTable";
-import { WebIndexForm } from "@/app/admin/connectors/web/WebIndexForm";
import { ThinkingAnimation } from "@/components/Thinking";
import { timeAgo } from "@/lib/time";
import { GlobeIcon } from "@/components/icons/icons";
import { fetcher } from "@/lib/fetcher";
-
-interface WebsiteIndexAttempt {
- url: string;
- status: "success" | "failure" | "in_progress" | "not_started";
- time_created: string;
- time_updated: string;
- docs_indexed: number;
-}
-
-interface ListWebIndexingResponse {
- index_attempts: WebsiteIndexAttempt[];
-}
+import { IndexAttempt, ListIndexingResponse } from "../interfaces";
+import { IndexForm } from "@/components/admin/connectors/Form";
+import { TextFormField } from "@/components/admin/connectors/Field";
const COLUMNS = [
{ header: "Base URL", key: "url" },
@@ -30,28 +21,29 @@ const COLUMNS = [
export default function Web() {
const { mutate } = useSWRConfig();
- const { data, isLoading, error } = useSWR(
+ const { data, isLoading, error } = useSWR(
"/api/admin/connectors/web/index-attempt",
fetcher
);
- const urlToLatestIndexAttempt = new Map();
+ const urlToLatestIndexAttempt = new Map();
const urlToLatestIndexSuccess = new Map();
data?.index_attempts?.forEach((indexAttempt) => {
- const latestIndexAttempt = urlToLatestIndexAttempt.get(indexAttempt.url);
+ const url = indexAttempt.connector_specific_config.base_url;
+ const latestIndexAttempt = urlToLatestIndexAttempt.get(url);
if (
!latestIndexAttempt ||
indexAttempt.time_created > latestIndexAttempt.time_created
) {
- urlToLatestIndexAttempt.set(indexAttempt.url, indexAttempt);
+ urlToLatestIndexAttempt.set(url, indexAttempt);
}
- const latestIndexSuccess = urlToLatestIndexSuccess.get(indexAttempt.url);
+ const latestIndexSuccess = urlToLatestIndexSuccess.get(url);
if (
indexAttempt.status === "success" &&
(!latestIndexSuccess || indexAttempt.time_updated > latestIndexSuccess)
) {
- urlToLatestIndexSuccess.set(indexAttempt.url, indexAttempt.time_updated);
+ urlToLatestIndexSuccess.set(url, indexAttempt.time_updated);
}
});
@@ -65,7 +57,15 @@ export default function Web() {
Request Indexing
-
}
+ validationSchema={Yup.object().shape({
+ base_url: Yup.string().required(
+ "Please enter the website URL to scrape e.g. https://docs.github.com/en/actions"
+ ),
+ })}
+ initialValues={{ base_url: "" }}
onSubmit={(success) => {
if (success) {
mutate("/api/admin/connectors/web/index-attempt");
@@ -87,22 +87,21 @@ export default function Web() {
data={
urlToLatestIndexAttempt.size > 0
? Array.from(urlToLatestIndexAttempt.values()).map(
- (indexAttempt) => ({
- ...indexAttempt,
- indexed_at:
- timeAgo(urlToLatestIndexSuccess.get(indexAttempt.url)) ||
- "-",
- docs_indexed: indexAttempt.docs_indexed || "-",
- url: (
-
- {indexAttempt.url}
-
- ),
- })
+ (indexAttempt) => {
+ const url = indexAttempt.connector_specific_config
+ .base_url as string;
+ return {
+ indexed_at:
+ timeAgo(urlToLatestIndexSuccess.get(url)) || "-",
+ docs_indexed: indexAttempt.docs_indexed || "-",
+ url: (
+
+ {url}
+
+ ),
+ status: indexAttempt.status,
+ };
+ }
)
: []
}
diff --git a/web/src/app/admin/layout.tsx b/web/src/app/admin/layout.tsx
index 24eaea3c5..064b9af31 100644
--- a/web/src/app/admin/layout.tsx
+++ b/web/src/app/admin/layout.tsx
@@ -1,6 +1,6 @@
import { Header } from "@/components/Header";
import { Sidebar } from "@/components/admin/connectors/Sidebar";
-import { GlobeIcon, SlackIcon } from "@/components/icons/icons";
+import { GithubIcon, GlobeIcon, SlackIcon } from "@/components/icons/icons";
import { getCurrentUserSS } from "@/lib/userSS";
import { redirect } from "next/navigation";
@@ -45,6 +45,15 @@ export default async function AdminLayout({
),
link: "/admin/connectors/web",
},
+ {
+ name: (
+
+ ),
+ link: "/admin/connectors/github",
+ },
],
},
]}
diff --git a/web/src/components/admin/connectors/Form.tsx b/web/src/components/admin/connectors/Form.tsx
new file mode 100644
index 000000000..2daba47a0
--- /dev/null
+++ b/web/src/components/admin/connectors/Form.tsx
@@ -0,0 +1,106 @@
+import React, { useState } from "react";
+import { Formik, Form, FormikHelpers } from "formik";
+import * as Yup from "yup";
+import { Popup } from "./Popup";
+
+type ValidSources = "web" | "github";
+
+const handleSubmit = async (
+ source: ValidSources,
+ values: Yup.AnyObject,
+ { setSubmitting }: FormikHelpers
,
+ setPopup: (
+ popup: { message: string; type: "success" | "error" } | null
+ ) => void
+): Promise => {
+ setSubmitting(true);
+ let isSuccess = false;
+ try {
+ const response = await fetch(
+ `/api/admin/connectors/${source}/index-attempt`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ connector_specific_config: values }),
+ }
+ );
+
+ if (response.ok) {
+ isSuccess = true;
+ setPopup({ message: "Success!", type: "success" });
+ } else {
+ const errorData = await response.json();
+ setPopup({ message: `Error: ${errorData.detail}`, type: "error" });
+ }
+ } catch (error) {
+ setPopup({ message: `Error: ${error}`, type: "error" });
+ } finally {
+ setSubmitting(false);
+ setTimeout(() => {
+ setPopup(null);
+ }, 3000);
+ return isSuccess;
+ }
+};
+
+interface IndexFormProps {
+ source: ValidSources;
+ formBody: JSX.Element | null;
+ validationSchema: Yup.ObjectSchema;
+ initialValues: YupObjectType;
+ onSubmit: (isSuccess: boolean) => void;
+ additionalNonFormValues?: Yup.AnyObject;
+}
+
+export function IndexForm({
+ source,
+ formBody,
+ validationSchema,
+ initialValues,
+ onSubmit,
+ additionalNonFormValues = {},
+}: IndexFormProps): JSX.Element {
+ const [popup, setPopup] = useState<{
+ message: string;
+ type: "success" | "error";
+ } | null>(null);
+
+ return (
+ <>
+ {popup && }
+ {
+ handleSubmit(
+ source,
+ { ...values, ...additionalNonFormValues },
+ formikHelpers as FormikHelpers,
+ setPopup
+ ).then((isSuccess) => onSubmit(isSuccess));
+ }}
+ >
+ {({ isSubmitting }) => (
+
+ )}
+
+ >
+ );
+}
diff --git a/web/src/components/admin/connectors/RecurringConnectorStatus.tsx b/web/src/components/admin/connectors/RecurringConnectorStatus.tsx
index 02acb0b62..30b34a852 100644
--- a/web/src/components/admin/connectors/RecurringConnectorStatus.tsx
+++ b/web/src/components/admin/connectors/RecurringConnectorStatus.tsx
@@ -1,4 +1,10 @@
+"use client";
+
+import { ListIndexingResponse } from "@/app/admin/connectors/interfaces";
+import { fetcher } from "@/lib/fetcher";
+import { timeAgo } from "@/lib/time";
import { CheckCircle, MinusCircle } from "@phosphor-icons/react";
+import useSWR from "swr";
export enum ConnectorStatus {
Running = "Running",
@@ -7,16 +13,39 @@ export enum ConnectorStatus {
interface ReccuringConnectorStatusProps {
status: ConnectorStatus;
+ source: string;
}
export const ReccuringConnectorStatus = ({
status,
+ source,
}: ReccuringConnectorStatusProps) => {
+ const { data } = useSWR(
+ `/api/admin/connectors/${source}/index-attempt`,
+ fetcher
+ );
+
+ const lastSuccessfulAttempt = data?.index_attempts
+ .filter((attempt) => attempt.status === "success")
+ .sort((a, b) => {
+ if (a.time_updated === b.time_updated) {
+ return 0;
+ }
+ return a.time_updated > b.time_updated ? -1 : 1;
+ })[0];
+
if (status === ConnectorStatus.Running) {
return (
-
-
-
{status}
+
+
+ {lastSuccessfulAttempt && (
+
+ Last updated {timeAgo(lastSuccessfulAttempt.time_updated)}
+
+ )}
);
}
diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx
index f7b576bb8..8c232e62c 100644
--- a/web/src/components/icons/icons.tsx
+++ b/web/src/components/icons/icons.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Globe, SlackLogo } from "@phosphor-icons/react";
+import { Globe, SlackLogo, GithubLogo } from "@phosphor-icons/react";
interface IconProps {
size?: string;
@@ -22,3 +22,10 @@ export const SlackIcon = ({
}: IconProps) => {
return
;
};
+
+export const GithubIcon = ({
+ size = "16",
+ className = defaultTailwindCSS,
+}: IconProps) => {
+ return
;
+};
diff --git a/web/src/components/search/SearchResultsDisplay.tsx b/web/src/components/search/SearchResultsDisplay.tsx
index 7fb981616..8a2e3d793 100644
--- a/web/src/components/search/SearchResultsDisplay.tsx
+++ b/web/src/components/search/SearchResultsDisplay.tsx
@@ -3,6 +3,7 @@ import { Globe, SlackLogo, GoogleDriveLogo } from "@phosphor-icons/react";
import "tailwindcss/tailwind.css";
import { Quote, Document } from "./types";
import { ThinkingAnimation } from "../Thinking";
+import { GithubIcon } from "../icons/icons";
interface SearchResultsDisplayProps {
answer: string | null;
@@ -22,6 +23,8 @@ const getSourceIcon = (sourceType: string) => {
return
;
case "google_drive":
return
;
+ case "github":
+ return
;
default:
return null;
}
@@ -102,7 +105,7 @@ export const SearchResultsDisplay: React.FC
= ({
>
{getSourceIcon(doc.source_type)}
- {doc.semantic_name || doc.document_id}
+ {doc.semantic_identifier || doc.document_id}
{doc.blurb}
diff --git a/web/src/components/search/types.tsx b/web/src/components/search/types.tsx
index 928d006ee..b5f0213dd 100644
--- a/web/src/components/search/types.tsx
+++ b/web/src/components/search/types.tsx
@@ -11,7 +11,7 @@ export interface Document {
link: string;
source_type: string;
blurb: string;
- semantic_name: string | null;
+ semantic_identifier: string | null;
}
export interface SearchResponse {