Various Admin Page + User Flow Improvements (#1987)

This commit is contained in:
pablodanswer 2024-08-03 18:09:46 -07:00 committed by GitHub
parent aa4a00cbc2
commit 0261d689dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 688 additions and 364 deletions

View File

@ -81,18 +81,18 @@ def _should_create_new_indexing(
return False
return True
# If the connector is disabled, don't index
# If the connector is disabled or is the ingestion API, don't index
# NOTE: during an embedding model switch over, the following logic
# is bypassed by the above check for a future model
if connector.disabled:
return False
if connector.refresh_freq is None:
if connector.disabled or connector.id == 0:
return False
if not last_index:
return True
if connector.refresh_freq is None:
return False
# Only one scheduled job per connector at a time
# Can schedule another one if the current one is already running however
# Because the currently running one will not be until the latest time

View File

@ -9,7 +9,9 @@ from danswer.db.document_set import get_or_create_document_set_by_name
from danswer.db.engine import get_sqlalchemy_engine
from danswer.db.input_prompt import insert_input_prompt_if_not_exists
from danswer.db.models import DocumentSet as DocumentSetDBModel
from danswer.db.models import Persona
from danswer.db.models import Prompt as PromptDBModel
from danswer.db.models import Tool as ToolDBModel
from danswer.db.persona import get_prompt_by_name
from danswer.db.persona import upsert_persona
from danswer.db.persona import upsert_prompt
@ -78,9 +80,31 @@ def load_personas_from_yaml(
prompt_ids = [prompt.id for prompt in prompts if prompt is not None]
p_id = persona.get("id")
tool_ids = []
if persona.get("image_generation"):
image_gen_tool = (
db_session.query(ToolDBModel)
.filter(ToolDBModel.name == "ImageGenerationTool")
.first()
)
if image_gen_tool:
tool_ids.append(image_gen_tool.id)
llm_model_provider_override = persona.get("llm_model_provider_override")
llm_model_version_override = persona.get("llm_model_version_override")
# Set specific overrides for image generation persona
if persona.get("image_generation"):
llm_model_version_override = "gpt-4o"
existing_persona = (
db_session.query(Persona)
.filter(Persona.name == persona["name"])
.first()
)
upsert_persona(
user=None,
# Negative to not conflict with existing personas
persona_id=(-1 * p_id) if p_id is not None else None,
name=persona["name"],
description=persona["description"],
@ -92,13 +116,20 @@ def load_personas_from_yaml(
llm_filter_extraction=persona.get("llm_filter_extraction"),
icon_shape=persona.get("icon_shape"),
icon_color=persona.get("icon_color"),
llm_model_provider_override=None,
llm_model_version_override=None,
llm_model_provider_override=llm_model_provider_override,
llm_model_version_override=llm_model_version_override,
recency_bias=RecencyBiasSetting(persona["recency_bias"]),
prompt_ids=prompt_ids,
document_set_ids=doc_set_ids,
tool_ids=tool_ids,
default_persona=True,
is_public=True,
display_priority=existing_persona.display_priority
if existing_persona is not None
else persona.get("display_priority"),
is_visible=existing_persona.is_visible
if existing_persona is not None
else persona.get("is_visible"),
db_session=db_session,
)

View File

@ -5,7 +5,7 @@ personas:
# this is for DanswerBot to use when tagged in a non-configured channel
# Careful setting specific IDs, this won't autoincrement the next ID value for postgres
- id: 0
name: "Danswer"
name: "Knowledge"
description: >
Assistant with access to documents from your Connected Sources.
# Default Prompt objects attached to the persona, see prompts.yaml
@ -39,11 +39,13 @@ personas:
document_sets: []
icon_shape: 23013
icon_color: "#6FB1FF"
display_priority: 1
is_visible: true
- id: 1
name: "GPT"
name: "General"
description: >
Assistant with no access to documents. Chat with just the Language Model.
Assistant with no access to documents. Chat with just the Large Language Model.
prompts:
- "OnlyLLM"
num_chunks: 0
@ -53,7 +55,8 @@ personas:
document_sets: []
icon_shape: 50910
icon_color: "#FF6F6F"
display_priority: 0
is_visible: true
- id: 2
name: "Paraphrase"
@ -68,4 +71,23 @@ personas:
document_sets: []
icon_shape: 45519
icon_color: "#6FFF8D"
display_priority: 2
is_visible: false
- id: 3
name: "Art"
description: >
Assistant for generating images based on descriptions.
prompts:
- "ImageGeneration"
num_chunks: 0
llm_relevance_filter: false
llm_filter_extraction: false
recency_bias: "no_decay"
document_sets: []
icon_shape: 234124
icon_color: "#9B59B6"
image_generation: true
display_priority: 3
is_visible: true

View File

@ -30,7 +30,25 @@ prompts:
# Prompts the LLM to include citations in the for [1], [2] etc.
# which get parsed to match the passed in sources
include_citations: true
- name: "ImageGeneration"
description: "Generates images based on user prompts!"
system: >
You are an advanced image generation system capable of creating diverse and detailed images.
The current date is DANSWER_DATETIME_REPLACEMENT.
You can interpret user prompts and generate high-quality, creative images that match their descriptions.
You always strive to create safe and appropriate content, avoiding any harmful or offensive imagery.
task: >
Generate an image based on the user's description.
If the user's request is unclear or too vague, ask for clarification to ensure the best possible result.
Provide a detailed description of the generated image, including key elements, colors, and composition.
If the request is not possible or appropriate, explain why and suggest alternatives.
datetime_aware: true
include_citations: false
- name: "OnlyLLM"
description: "Chat directly with the LLM!"

View File

@ -212,6 +212,7 @@ EXPERIMENTAL_CHECKPOINTING_ENABLED = (
os.environ.get("EXPERIMENTAL_CHECKPOINTING_ENABLED", "").lower() == "true"
)
PRUNING_DISABLED = -1
DEFAULT_PRUNING_FREQ = 60 * 60 * 24 # Once a day
ALLOW_SIMULTANEOUS_PRUNING = (

View File

@ -87,9 +87,7 @@ def create_connector(
connector_specific_config=connector_data.connector_specific_config,
refresh_freq=connector_data.refresh_freq,
indexing_start=connector_data.indexing_start,
prune_freq=connector_data.prune_freq
if connector_data.prune_freq is not None
else DEFAULT_PRUNING_FREQ,
prune_freq=connector_data.prune_freq,
disabled=connector_data.disabled,
)
db_session.add(connector)
@ -249,20 +247,32 @@ def fetch_unique_document_sources(db_session: Session) -> list[DocumentSource]:
def create_initial_default_connector(db_session: Session) -> None:
default_connector_id = 0
default_connector = fetch_connector_by_id(default_connector_id, db_session)
if default_connector is not None:
# Check if the existing connector has the correct values
if (
default_connector.source != DocumentSource.INGESTION_API
or default_connector.input_type != InputType.LOAD_STATE
or default_connector.refresh_freq is not None
or default_connector.disabled
or default_connector.name != "Ingestion API"
or default_connector.connector_specific_config != {}
or default_connector.prune_freq is not None
):
raise ValueError(
"DB is not in a valid initial state. "
"Default connector does not have expected values."
logger.warning(
"Default connector does not have expected values. Updating to proper state."
)
# Ensure default connector has correct valuesg
default_connector.source = DocumentSource.INGESTION_API
default_connector.input_type = InputType.LOAD_STATE
default_connector.refresh_freq = None
default_connector.disabled = False
default_connector.name = "Ingestion API"
default_connector.connector_specific_config = {}
default_connector.prune_freq = None
db_session.commit()
return
# Create a new default connector if it doesn't exist
connector = Connector(
id=default_connector_id,
name="Ingestion API",
@ -271,6 +281,7 @@ def create_initial_default_connector(db_session: Session) -> None:
connector_specific_config={},
refresh_freq=None,
prune_freq=None,
disabled=False,
)
db_session.add(connector)
db_session.commit()

View File

@ -172,14 +172,12 @@ def alter_credential(
return None
credential.name = credential_data.name
credential.name = credential_data.name
# Update only the keys present in credential_data.credential_json
for key, value in credential_data.credential_json.items():
credential.credential_json[key] = value
credential.user_id = user.id if user is not None else None
db_session.commit()
return credential

View File

@ -1153,7 +1153,9 @@ class Persona(Base):
# controls the ordering of personas in the UI
# higher priority personas are displayed first, ties are resolved by the ID,
# where lower value IDs (e.g. created earlier) are displayed first
display_priority: Mapped[int] = mapped_column(Integer, nullable=True, default=None)
display_priority: Mapped[int | None] = mapped_column(
Integer, nullable=True, default=None
)
deleted: Mapped[bool] = mapped_column(Boolean, default=False)
uploaded_image_id: Mapped[str | None] = mapped_column(String, nullable=True)

View File

@ -336,6 +336,8 @@ def upsert_persona(
icon_color: str | None = None,
icon_shape: int | None = None,
uploaded_image_id: str | None = None,
display_priority: int | None = None,
is_visible: bool = True,
) -> Persona:
if persona_id is not None:
persona = db_session.query(Persona).filter_by(id=persona_id).first()
@ -394,6 +396,8 @@ def upsert_persona(
persona.icon_color = icon_color
persona.icon_shape = icon_shape
persona.uploaded_image_id = uploaded_image_id
persona.display_priority = display_priority
persona.is_visible = is_visible
# Do not delete any associations manually added unless
# a new updated list is provided
@ -429,6 +433,8 @@ def upsert_persona(
icon_shape=icon_shape,
icon_color=icon_color,
uploaded_image_id=uploaded_image_id,
display_priority=display_priority,
is_visible=is_visible,
)
db_session.add(persona)

View File

@ -72,6 +72,28 @@ def get_cc_pair_full_info(
)
@router.put("/admin/cc-pair/{cc_pair_id}/name")
def update_cc_pair_name(
cc_pair_id: int,
new_name: str,
user: User | None = Depends(current_user),
db_session: Session = Depends(get_session),
) -> StatusResponse[int]:
cc_pair = get_connector_credential_pair_from_id(cc_pair_id, db_session)
if not cc_pair:
raise HTTPException(status_code=404, detail="CC Pair not found")
try:
cc_pair.name = new_name
db_session.commit()
return StatusResponse(
success=True, message="Name updated successfully", data=cc_pair_id
)
except IntegrityError:
db_session.rollback()
raise HTTPException(status_code=400, detail="Name must be unique")
@router.put("/connector/{connector_id}/credential/{credential_id}")
def associate_credential_to_connector(
connector_id: int,

View File

@ -170,10 +170,8 @@ def create_connector(env_name: str, file_paths: list[str]) -> int:
)
body = connector.dict()
print("body:", body)
response = requests.post(url, headers=GENERAL_HEADERS, json=body)
if response.status_code == 200:
print("Connector created successfully:", response.json())
return response.json()["id"]
else:
raise RuntimeError(response.__dict__)

View File

@ -4,7 +4,7 @@ import { CCPairFullInfo } from "./types";
import { HealthCheckBanner } from "@/components/health/healthcheck";
import { CCPairStatus } from "@/components/Status";
import { BackButton } from "@/components/BackButton";
import { Divider, Title } from "@tremor/react";
import { Button, Divider, Title } from "@tremor/react";
import { IndexingAttemptsTable } from "./IndexingAttemptsTable";
import { ConfigDisplay } from "./ConfigDisplay";
import { ModifyStatusButtonCluster } from "./ModifyStatusButtonCluster";
@ -19,8 +19,11 @@ import { ThreeDotsLoader } from "@/components/Loading";
import CredentialSection from "@/components/credentials/CredentialSection";
import { buildCCPairInfoUrl } from "./lib";
import { SourceIcon } from "@/components/SourceIcon";
import { connectorConfigs } from "@/lib/connectors/connectors";
import { credentialTemplates } from "@/lib/connectors/credentials";
import { useEffect, useRef, useState } from "react";
import { CheckmarkIcon, EditIcon, XIcon } from "@/components/icons/icons";
import { usePopup } from "@/components/admin/connectors/Popup";
import { updateConnectorCredentialPairName } from "@/lib/connector";
// since the uploaded files are cleaned up after some period of time
// re-indexing will not work for the file connector. Also, it would not
@ -38,6 +41,43 @@ function Main({ ccPairId }: { ccPairId: number }) {
{ refreshInterval: 5000 } // 5 seconds
);
const [editableName, setEditableName] = useState(ccPair?.name || "");
const [isEditing, setIsEditing] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const { popup, setPopup } = usePopup();
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
}, [isEditing]);
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditableName(e.target.value);
};
const handleUpdateName = async () => {
try {
const response = await updateConnectorCredentialPairName(
ccPair?.id!,
editableName
);
if (!response.ok) {
throw new Error(await response.text());
}
mutate(buildCCPairInfoUrl(ccPairId));
setIsEditing(false);
setPopup({
message: "Connector name updated successfully",
type: "success",
});
} catch (error) {
setPopup({
message: `Failed to update connector name`,
type: "error",
});
}
};
if (isLoading) {
return <ThreeDotsLoader />;
}
@ -68,18 +108,52 @@ function Main({ ccPairId }: { ccPairId: number }) {
mutate(buildCCPairInfoUrl(ccPairId));
};
const startEditing = () => {
setEditableName(ccPair.name);
setIsEditing(true);
};
const deleting =
ccPair.latest_deletion_attempt?.status == "PENDING" ||
ccPair.latest_deletion_attempt?.status == "STARTED";
const resetEditing = () => {
setIsEditing(false);
setEditableName(ccPair.name);
};
return (
<>
{popup}
<BackButton />
<div className="pb-1 flex mt-1">
<div className="mr-2 my-auto ">
<SourceIcon iconSize={24} sourceType={ccPair.connector.source} />
</div>
<h1 className="text-3xl text-emphasis font-bold">{ccPair.name} </h1>
{isEditing ? (
<div className="flex items-center">
<input
ref={inputRef}
type="text"
value={editableName}
onChange={handleNameChange}
className="text-3xl w-full ring ring-1 ring-neutral-800 text-emphasis font-bold"
/>
<Button onClick={handleUpdateName} className="ml-2">
<CheckmarkIcon className="text-neutral-200" />
</Button>
<Button onClick={() => resetEditing()} className="ml-2">
<XIcon className="text-neutral-200" />
</Button>
</div>
) : (
<h1
onClick={() => startEditing()}
className="group flex cursor-pointer text-3xl text-emphasis gap-x-2 items-center font-bold"
>
{ccPair.name}
<EditIcon className="group-hover:visible invisible" />
</h1>
)}
<div className="ml-auto flex gap-x-2">
{!CONNECTOR_TYPES_THAT_CANT_REINDEX.includes(

View File

@ -140,9 +140,9 @@ export default function AddConnector({
const createConnector = async () => {
const AdvancedConfig: AdvancedConfig = {
pruneFreq: (pruneFreq || defaultPrune) * 60,
pruneFreq: pruneFreq * 60,
indexingStart,
refreshFreq: (refreshFreq || defaultRefresh) * 60,
refreshFreq: refreshFreq * 60,
};
// google sites-specific handling
@ -186,8 +186,8 @@ export default function AddConnector({
input_type: connector == "web" ? "load_state" : "poll", // single case
name: name,
source: connector,
refresh_freq: (refreshFreq || defaultRefresh) * 60,
prune_freq: (pruneFreq || defaultPrune) * 60,
refresh_freq: refreshFreq * 60 || null,
prune_freq: pruneFreq * 60 || null,
indexing_start: indexingStart,
disabled: false,
},

View File

@ -50,11 +50,10 @@ const AdvancedFormPage = forwardRef<FormikProps<any>, AdvancedFormPageProps>(
<Form className="space-y-6">
<div key="prune_freq">
<EditingValue
description="Checking all documents against the source to see if any no longer exist. Documents are deleted based on this. Note: To do this, we must check every document with the source so careful turning up the frequency of this (in minutes). This defaults to 1 day."
showNever
description="Checking all documents against the source to see if any no longer exist. Documents are deleted based on this. Note: To do this, we must check every document with the source so careful turning up the frequency of this (in minutes). This defaults to 1 day. If you input 0, we will never prune this connector."
optional
currentValue={
values.pruneFreq === 0 ? undefined : values.pruneFreq
}
currentValue={values.pruneFreq}
onChangeNumber={(value: number) => {
setPruneFreq(value);
setFieldValue("pruneFreq", value);
@ -67,11 +66,10 @@ const AdvancedFormPage = forwardRef<FormikProps<any>, AdvancedFormPageProps>(
</div>
<div key="refresh_freq">
<EditingValue
description="This is how frequently we pull new documents from the source (in minutes)"
showNever
description="This is how frequently we pull new documents from the source (in minutes). If you input 0, we will never pull new documents for this connector."
optional
currentValue={
values.refreshFreq === 0 ? undefined : values.refreshFreq
}
currentValue={values.refreshFreq}
onChangeNumber={(value: number) => {
setRefreshFreq(value);
setFieldValue("refreshFreq", value);

View File

@ -1,4 +1,4 @@
import React, { useState, useMemo, useEffect } from "react";
import React, { useState, useMemo, useEffect, useRef } from "react";
import {
Table,
TableRow,
@ -6,6 +6,7 @@ import {
TableBody,
TableCell,
Badge,
Button,
} from "@tremor/react";
import { IndexAttemptStatus } from "@/components/Status";
import { timeAgo } from "@/lib/time";
@ -28,6 +29,8 @@ import { SourceIcon } from "@/components/SourceIcon";
import { getSourceDisplayName } from "@/lib/sources";
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
import { Warning } from "@phosphor-icons/react";
import Cookies from "js-cookie";
import { TOGGLED_CONNECTORS_COOKIE_NAME } from "@/lib/constants";
const columnWidths = {
first: "20%",
@ -253,10 +256,22 @@ export function CCPairIndexingStatusTable({
}: {
ccPairsIndexingStatuses: ConnectorIndexingStatus<any, any>[];
}) {
const [allToggleTracker, setAllToggleTracker] = useState(true);
const [openSources, setOpenSources] = useState<Record<ValidSources, boolean>>(
{} as Record<ValidSources, boolean>
);
const [searchTerm, setSearchTerm] = useState("");
const searchInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (searchInputRef.current) {
searchInputRef.current.focus();
}
}, []);
const [connectorsToggled, setConnectorsToggled] = useState<
Record<ValidSources, boolean>
>(() => {
const savedState = Cookies.get(TOGGLED_CONNECTORS_COOKIE_NAME);
return savedState ? JSON.parse(savedState) : {};
});
const { groupedStatuses, sortedSources, groupSummaries } = useMemo(() => {
const grouped: Record<ValidSources, ConnectorIndexingStatus<any, any>[]> =
@ -295,39 +310,39 @@ export function CCPairIndexingStatusTable({
};
}, [ccPairsIndexingStatuses]);
const toggleSource = (source: ValidSources) => {
setOpenSources((prev) => ({
...prev,
[source]: !prev[source],
}));
};
const toggleSources = (toggle: boolean) => {
const updatedSources = Object.fromEntries(
sortedSources.map((item) => [item, toggle])
const toggleSource = (
source: ValidSources,
toggled: boolean | null = null
) => {
const newConnectorsToggled = {
...connectorsToggled,
[source]: toggled == null ? !connectorsToggled[source] : toggled,
};
setConnectorsToggled(newConnectorsToggled);
Cookies.set(
TOGGLED_CONNECTORS_COOKIE_NAME,
JSON.stringify(newConnectorsToggled)
);
setOpenSources(updatedSources as Record<ValidSources, boolean>);
setAllToggleTracker(!toggle);
};
const toggleSources = () => {
const currentToggledCount =
Object.values(connectorsToggled).filter(Boolean).length;
const shouldToggleOn = currentToggledCount < sortedSources.length / 2;
const router = useRouter();
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.metaKey || event.ctrlKey) {
switch (event.key.toLowerCase()) {
case "e":
toggleSources(false);
event.preventDefault();
break;
}
}
};
toggleSources(true);
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [router, allToggleTracker]);
const connectors = sortedSources.reduce(
(acc, source) => {
acc[source] = shouldToggleOn;
return acc;
},
{} as Record<ValidSources, boolean>
);
setConnectorsToggled(connectors);
Cookies.set(TOGGLED_CONNECTORS_COOKIE_NAME, JSON.stringify(connectors));
};
const shouldExpand =
Object.values(connectorsToggled).filter(Boolean).length <
sortedSources.length / 2;
return (
<div className="-mt-20">
@ -348,56 +363,99 @@ export function CCPairIndexingStatusTable({
}}
/>
<div className="-mb-10" />
<TableBody>
{sortedSources.map((source, ind) => (
<React.Fragment key={ind}>
<div className="mt-4" />
<div className="flex items-center mt-4 gap-x-2">
<input
type="text"
ref={searchInputRef}
placeholder="Search connectors..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="ml-2 w-96 h-9 flex-none rounded-md border-2 border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
<SummaryRow
source={source}
summary={groupSummaries[source]}
isOpen={openSources[source] || false}
onToggle={() => toggleSource(source)}
/>
<Button className="h-9" onClick={() => toggleSources()}>
{!shouldExpand ? "Collapse All" : "Expand All"}
</Button>
</div>
{sortedSources.map((source, ind) => {
const sourceMatches = source
.toLowerCase()
.includes(searchTerm.toLowerCase());
const matchingConnectors = groupedStatuses[source].filter(
(status) =>
(status.name || "")
.toLowerCase()
.includes(searchTerm.toLowerCase())
);
if (sourceMatches || matchingConnectors.length > 0) {
return (
<React.Fragment key={ind}>
<div className="mt-4" />
{openSources[source] && (
<>
<TableRow className="border border-border">
<TableHeaderCell className={`w-[${columnWidths.first}]`}>
Name
</TableHeaderCell>
<TableHeaderCell className={`w-[${columnWidths.fifth}]`}>
Last Indexed
</TableHeaderCell>
<TableHeaderCell className={`w-[${columnWidths.second}]`}>
Activity
</TableHeaderCell>
<TableHeaderCell className={`w-[${columnWidths.fourth}]`}>
Public
</TableHeaderCell>
<TableHeaderCell className={`w-[${columnWidths.sixth}]`}>
Total Docs
</TableHeaderCell>
<TableHeaderCell className={`w-[${columnWidths.third}]`}>
Last Status
</TableHeaderCell>
<TableHeaderCell
className={`w-[${columnWidths.seventh}]`}
></TableHeaderCell>
</TableRow>
{groupedStatuses[source].map((ccPairsIndexingStatus) => (
<ConnectorRow
key={ccPairsIndexingStatus.cc_pair_id}
ccPairsIndexingStatus={ccPairsIndexingStatus}
/>
))}
</>
)}
</React.Fragment>
))}
<SummaryRow
source={source}
summary={groupSummaries[source]}
isOpen={connectorsToggled[source] || false}
onToggle={() => toggleSource(source)}
/>
{connectorsToggled[source] && (
<>
<TableRow className="border border-border">
<TableHeaderCell
className={`w-[${columnWidths.first}]`}
>
Name
</TableHeaderCell>
<TableHeaderCell
className={`w-[${columnWidths.fifth}]`}
>
Last Indexed
</TableHeaderCell>
<TableHeaderCell
className={`w-[${columnWidths.second}]`}
>
Activity
</TableHeaderCell>
<TableHeaderCell
className={`w-[${columnWidths.fourth}]`}
>
Public
</TableHeaderCell>
<TableHeaderCell
className={`w-[${columnWidths.sixth}]`}
>
Total Docs
</TableHeaderCell>
<TableHeaderCell
className={`w-[${columnWidths.third}]`}
>
Last Status
</TableHeaderCell>
<TableHeaderCell
className={`w-[${columnWidths.seventh}]`}
></TableHeaderCell>
</TableRow>
{(sourceMatches
? groupedStatuses[source]
: matchingConnectors
).map((ccPairsIndexingStatus) => (
<ConnectorRow
key={ccPairsIndexingStatus.cc_pair_id}
ccPairsIndexingStatus={ccPairsIndexingStatus}
/>
))}
</>
)}
</React.Fragment>
);
}
return null;
})}
</TableBody>
{/* Padding between table and bottom of page */}
<div className="invisible w-full pb-40" />
</Table>
</div>

View File

@ -103,84 +103,117 @@ function IntegerInput({
export function SettingsForm() {
const router = useRouter();
const combinedSettings = useContext(SettingsContext);
const [settings, setSettings] = useState<Settings | null>(null);
const [chatRetention, setChatRetention] = useState("");
const { popup, setPopup } = usePopup();
const isEnterpriseEnabled = usePaidEnterpriseFeaturesEnabled();
const combinedSettings = useContext(SettingsContext);
useEffect(() => {
if (combinedSettings?.settings.maximum_chat_retention_days !== undefined) {
if (combinedSettings) {
setSettings(combinedSettings.settings);
setChatRetention(
combinedSettings.settings.maximum_chat_retention_days?.toString() || ""
);
}
}, [combinedSettings?.settings.maximum_chat_retention_days]);
}, []);
if (!combinedSettings) {
if (!settings) {
return null;
}
const settings = combinedSettings.settings;
async function updateSettingField(
updateRequests: { fieldName: keyof Settings; newValue: any }[]
) {
const newValues: any = {};
updateRequests.forEach(({ fieldName, newValue }) => {
newValues[fieldName] = newValue;
});
// Optimistically update the local state
const newSettings: Settings | null = settings
? {
...settings,
...updateRequests.reduce((acc, { fieldName, newValue }) => {
acc[fieldName] = newValue ?? settings[fieldName];
return acc;
}, {} as Partial<Settings>),
}
: null;
setSettings(newSettings);
try {
const response = await fetch("/api/admin/settings", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newSettings),
});
if (!response.ok) {
const errorMsg = (await response.json()).detail;
throw new Error(errorMsg);
}
const response = await fetch("/api/admin/settings", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...settings,
...newValues,
}),
});
if (response.ok) {
router.refresh();
} else {
const errorMsg = (await response.json()).detail;
alert(`Failed to update settings. ${errorMsg}`);
setPopup({
message: "Settings updated successfully!",
type: "success",
});
} catch (error) {
// Revert the optimistic update
setSettings(settings);
console.error("Error updating settings:", error);
setPopup({
message: `Failed to update settings`,
type: "error",
});
}
}
function handleToggleSettingsField(
fieldName: keyof Settings,
checked: boolean
) {
const updates: { fieldName: keyof Settings; newValue: any }[] = [
{ fieldName, newValue: checked },
];
// If we're disabling a page, check if we need to update the default page
if (
!checked &&
(fieldName === "search_page_enabled" || fieldName === "chat_page_enabled")
) {
const otherPageField =
fieldName === "search_page_enabled"
? "chat_page_enabled"
: "search_page_enabled";
const otherPageEnabled = settings && settings[otherPageField];
if (
otherPageEnabled &&
settings?.default_page ===
(fieldName === "search_page_enabled" ? "search" : "chat")
) {
updates.push({
fieldName: "default_page",
newValue: fieldName === "search_page_enabled" ? "chat" : "search",
});
}
}
updateSettingField(updates);
}
function handleSetChatRetention() {
// Convert chatRetention to a number or null and update the global settings
const newValue =
chatRetention === "" ? null : parseInt(chatRetention.toString(), 10);
const newValue = chatRetention === "" ? null : parseInt(chatRetention, 10);
updateSettingField([
{ fieldName: "maximum_chat_retention_days", newValue: newValue },
])
.then(() => {
setPopup({
message: "Chat retention settings updated successfully!",
type: "success",
});
})
.catch((error) => {
console.error("Error updating settings:", error);
const errorMessage =
error.response?.data?.message || error.message || "Unknown error";
setPopup({
message: `Failed to update settings: ${errorMessage}`,
type: "error",
});
});
{ fieldName: "maximum_chat_retention_days", newValue },
]);
}
function handleClearChatRetention() {
setChatRetention(""); // Clear the chat retention input
setChatRetention("");
updateSettingField([
{ fieldName: "maximum_chat_retention_days", newValue: null },
]).then(() => {
setPopup({
message: "Chat retention cleared successfully!",
type: "success",
});
});
]);
}
return (
@ -190,36 +223,20 @@ export function SettingsForm() {
<Checkbox
label="Search Page Enabled?"
sublabel={`If set, then the "Search" page will be accessible to all users
and will show up as an option on the top navbar. If unset, then this
page will not be available.`}
sublabel="If set, then the 'Search' page will be accessible to all users and will show up as an option on the top navbar. If unset, then this page will not be available."
checked={settings.search_page_enabled}
onChange={(e) => {
const updates: any[] = [
{ fieldName: "search_page_enabled", newValue: e.target.checked },
];
if (!e.target.checked && settings.default_page === "search") {
updates.push({ fieldName: "default_page", newValue: "chat" });
}
updateSettingField(updates);
}}
onChange={(e) =>
handleToggleSettingsField("search_page_enabled", e.target.checked)
}
/>
<Checkbox
label="Chat Page Enabled?"
sublabel={`If set, then the "Chat" page will be accessible to all users
and will show up as an option on the top navbar. If unset, then this
page will not be available.`}
sublabel="If set, then the 'Chat' page will be accessible to all users and will show up as an option on the top navbar. If unset, then this page will not be available."
checked={settings.chat_page_enabled}
onChange={(e) => {
const updates: any[] = [
{ fieldName: "chat_page_enabled", newValue: e.target.checked },
];
if (!e.target.checked && settings.default_page === "chat") {
updates.push({ fieldName: "default_page", newValue: "search" });
}
updateSettingField(updates);
}}
onChange={(e) =>
handleToggleSettingsField("chat_page_enabled", e.target.checked)
}
/>
<Selector
@ -237,6 +254,7 @@ export function SettingsForm() {
]);
}}
/>
{isEnterpriseEnabled && (
<>
<Title className="mb-4">Chat Settings</Title>
@ -246,10 +264,8 @@ export function SettingsForm() {
value={chatRetention === "" ? null : Number(chatRetention)}
onChange={(e) => {
const numValue = parseInt(e.target.value, 10);
if (numValue >= 1) {
setChatRetention(numValue.toString());
} else if (e.target.value === "") {
setChatRetention("");
if (numValue >= 1 || e.target.value === "") {
setChatRetention(e.target.value);
}
}}
id="chatRetentionInput"

View File

@ -6,9 +6,6 @@ import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrap
import { fetchChatData } from "@/lib/chat/fetchChatData";
import { unstable_noStore as noStore } from "next/cache";
import { redirect } from "next/navigation";
import { AssistantsGallery } from "./AssistantsGallery";
import FixedLogo from "@/app/chat/shared_chat_search/FixedLogo";
import GalleryWrapper from "../SidebarWrapper";
import WrappedAssistantsGallery from "./WrappedAssistantsGallery";
export default async function GalleryPage({

View File

@ -16,10 +16,12 @@ export function ChatBanner() {
className={`
mt-8
mb-2
p-1
mx-2
z-[39]
w-full
h-[30px]
text-wrap
w-[500px]
mx-auto
bg-background-100
shadow-sm
rounded
@ -30,7 +32,7 @@ export function ChatBanner() {
<div className="mx-auto text-emphasis text-sm flex flex-col">
<div className="my-auto">
<ReactMarkdown
className="prose max-w-full"
className="prose flex text-wrap break-all text-wrap max-w-full"
components={{
a: ({ node, ...props }) => (
<a
@ -40,7 +42,12 @@ export function ChatBanner() {
rel="noopener noreferrer"
/>
),
p: ({ node, ...props }) => <p {...props} className="text-sm" />,
p: ({ node, ...props }) => (
<p
{...props}
className="text-wrap break-all line-clamp-3 text-sm"
/>
),
}}
remarkPlugins={[remarkGfm]}
>

View File

@ -799,7 +799,6 @@ export function ChatPage({
if (!stack.isEmpty()) {
const packet = stack.nextPacket();
if (packet) {
if (Object.hasOwn(packet, "answer_piece")) {
answer += (packet as AnswerPiecePacket).answer_piece;
@ -1216,7 +1215,7 @@ export function ChatPage({
duration-300
ease-in-out
h-full
${toggledSidebar || showDocSidebar ? "w-[250px]" : "w-[0px]"}
${toggledSidebar ? "w-[250px]" : "w-[0px]"}
`}
/>
<ChatBanner />
@ -1512,17 +1511,13 @@ export function ChatPage({
messageId={null}
personaName={liveAssistant.name}
content={
<div className="text-sm my-auto">
<ThreeDots
height="30"
width="50"
color="#3b82f6"
ariaLabel="grid-loading"
radius="12.5"
wrapperStyle={{}}
wrapperClass=""
visible={true}
/>
<div
key={"Generating"}
className="mr-auto relative inline-block"
>
<span className="text-sm loading-text">
Thinking...
</span>
</div>
}
/>

View File

@ -535,6 +535,13 @@ export function personaIncludesRetrieval(selectedPersona: Persona) {
);
}
export function personaIncludesImage(selectedPersona: Persona) {
return selectedPersona.tools.some(
(tool) =>
tool.in_code_tool_id && tool.in_code_tool_id == "ImageGenerationTool"
);
}
const PARAMS_TO_SKIP = [
SEARCH_PARAM_NAMES.SUBMIT_ON_LOAD,
SEARCH_PARAM_NAMES.USER_MESSAGE,

View File

@ -91,7 +91,8 @@ export function CodeBlock({
codeText = findTextNode(props.node);
}
const handleCopy = () => {
const handleCopy = (event: React.MouseEvent) => {
event.preventDefault();
if (!codeText) {
return;
}
@ -109,7 +110,7 @@ export function CodeBlock({
{codeText && (
<div
className="ml-auto cursor-pointer select-none"
onClick={handleCopy}
onMouseDown={handleCopy}
>
{copied ? (
<div className="flex items-center space-x-2">

View File

@ -153,7 +153,25 @@ export const AIMessage = ({
handleForceSearch?: () => void;
retrievalDisabled?: boolean;
}) => {
const finalContent = content + (!isComplete ? "[*](test)" : "");
const processContent = (content: string | JSX.Element) => {
if (typeof content !== "string") {
return content;
}
const codeBlockRegex = /```[\s\S]*?```|```[\s\S]*?$/g;
const matches = content.match(codeBlockRegex);
if (matches) {
const lastMatch = matches[matches.length - 1];
if (!lastMatch.endsWith("```")) {
return content;
}
}
return content + (!isComplete ? " [*]() " : "");
};
const finalContent = processContent(content as string);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
@ -228,7 +246,7 @@ export const AIMessage = ({
return (
<div ref={trackedElementRef} className={"py-5 px-2 lg:px-5 relative flex "}>
<div className="mx-auto w-[90%] max-w-message-max">
<div className=" mobile:ml-4 xl:ml-8">
<div className="mobile:ml-4 xl:ml-8">
<div className="flex">
<AssistantIcon
size="small"
@ -311,50 +329,44 @@ export const AIMessage = ({
!TOOLS_WITH_CUSTOM_HANDLING.includes(
toolCall.tool_name
) && (
<div className="my-2">
<ToolRunDisplay
toolName={
toolCall.tool_result && content
? `Used "${toolCall.tool_name}"`
: `Using "${toolCall.tool_name}"`
}
toolLogo={
<FiTool size={15} className="my-auto mr-1" />
}
isRunning={!toolCall.tool_result || !content}
/>
</div>
<ToolRunDisplay
toolName={
toolCall.tool_result && content
? `Used "${toolCall.tool_name}"`
: `Using "${toolCall.tool_name}"`
}
toolLogo={
<FiTool size={15} className="my-auto mr-1" />
}
isRunning={!toolCall.tool_result || !content}
/>
)}
{toolCall &&
toolCall.tool_name === IMAGE_GENERATION_TOOL_NAME &&
!toolCall.tool_result && (
<div className="my-2">
<ToolRunDisplay
toolName={`Generating images`}
toolLogo={
<FiImage size={15} className="my-auto mr-1" />
}
isRunning={!toolCall.tool_result}
/>
</div>
<ToolRunDisplay
toolName={`Generating images`}
toolLogo={
<FiImage size={15} className="my-auto mr-1" />
}
isRunning={!toolCall.tool_result}
/>
)}
{toolCall &&
toolCall.tool_name === INTERNET_SEARCH_TOOL_NAME && (
<div className="my-2">
<ToolRunDisplay
toolName={
toolCall.tool_result
? `Searched the internet`
: `Searching the internet`
}
toolLogo={
<FiGlobe size={15} className="my-auto mr-1" />
}
isRunning={!toolCall.tool_result}
/>
</div>
<ToolRunDisplay
toolName={
toolCall.tool_result
? `Searched the internet`
: `Searching the internet`
}
toolLogo={
<FiGlobe size={15} className="my-auto mr-1" />
}
isRunning={!toolCall.tool_result}
/>
)}
{content ? (
@ -362,64 +374,70 @@ export const AIMessage = ({
<FileDisplay files={files || []} />
{typeof content === "string" ? (
<ReactMarkdown
key={messageId}
className="prose max-w-full"
components={{
a: (props) => {
const { node, ...rest } = props;
const value = rest.children;
<div className="overflow-x-auto w-full pr-2 max-w-[675px]">
<ReactMarkdown
key={messageId}
className="prose max-w-full"
components={{
a: (props) => {
const { node, ...rest } = props;
const value = rest.children;
if (value?.toString().startsWith("*")) {
return (
<div className="flex-none bg-background-800 inline-block rounded-full h-3 w-3 ml-2" />
);
} else if (value?.toString().startsWith("[")) {
// for some reason <a> tags cause the onClick to not apply
// and the links are unclickable
// TODO: fix the fact that you have to double click to follow link
// for the first link
return (
<Citation
link={rest?.href}
key={node?.position?.start?.offset}
>
{rest.children}
</Citation>
);
} else {
return (
<a
key={node?.position?.start?.offset}
onClick={() =>
rest.href
? window.open(rest.href, "_blank")
: undefined
}
className="cursor-pointer text-link hover:text-link-hover"
>
{rest.children}
</a>
);
}
},
code: (props) => (
<CodeBlock
{...props}
content={content as string}
/>
),
p: ({ node, ...props }) => (
<p {...props} className="text-default" />
),
}}
remarkPlugins={[remarkGfm]}
rehypePlugins={[
[rehypePrism, { ignoreMissing: true }],
]}
>
{finalContent}
</ReactMarkdown>
if (value?.toString().startsWith("*")) {
return (
<div className="flex-none bg-background-800 inline-block rounded-full h-3 w-3 ml-2" />
);
} else if (
value?.toString().startsWith("[")
) {
// for some reason <a> tags cause the onClick to not apply
// and the links are unclickable
// TODO: fix the fact that you have to double click to follow link
// for the first link
return (
<Citation
link={rest?.href}
key={node?.position?.start?.offset}
>
{rest.children}
</Citation>
);
} else {
return (
<a
key={node?.position?.start?.offset}
onMouseDown={() =>
rest.href
? window.open(rest.href, "_blank")
: undefined
}
className="cursor-pointer text-link hover:text-link-hover"
>
{rest.children}
</a>
);
}
},
code: (props) => (
<CodeBlock
className="w-full"
{...props}
content={content as string}
/>
),
p: ({ node, ...props }) => (
<p {...props} className="text-default" />
),
}}
remarkPlugins={[remarkGfm]}
rehypePlugins={[
[rehypePrism, { ignoreMissing: true }],
]}
>
{finalContent as string}
</ReactMarkdown>
</div>
) : (
content
)}
@ -772,11 +790,11 @@ export const HumanMessage = ({
</div>
) : typeof content === "string" ? (
<>
{onEdit &&
isHovered &&
!isEditing &&
(!files || files.length === 0) ? (
<div className="ml-auto mr-1 my-auto">
<div className="ml-auto mr-1 my-auto">
{onEdit &&
isHovered &&
!isEditing &&
(!files || files.length === 0) ? (
<Tooltip delayDuration={1000} content={"Edit message"}>
<button
className="hover:bg-hover p-1.5 rounded"
@ -785,13 +803,13 @@ export const HumanMessage = ({
setIsHovered(false);
}}
>
<FiEdit2 />
<FiEdit2 className="!h-4 !w-4" />
</button>
</Tooltip>
</div>
) : (
<div className="h-[27px]" />
)}
) : (
<div className="w-7" />
)}
</div>
<div
className={`${
@ -801,11 +819,10 @@ export const HumanMessage = ({
!isEditing &&
(!files || files.length === 0)
) && "ml-auto"
} relative max-w-[70%] mb-auto whitespace-break-spaces rounded-3xl bg-user px-5 py-2.5`}
} relative flex-none max-w-[70%] mb-auto whitespace-break-spaces rounded-3xl bg-user px-5 py-2.5`}
>
{content}
</div>
{/* </div> */}
</>
) : (
<>

View File

@ -91,12 +91,11 @@ export function SearchSummary({
<div className={`flex p-1 rounded ${isOverflowed && "cursor-default"}`}>
<FiSearch className="flex-none mr-2 my-auto" size={14} />
<div
className={`${
!finished && "loading-text"
} line-clamp-1 break-all px-0.5`}
className={`${!finished && "loading-text"}
!text-sm !line-clamp-1 !break-all px-0.5`}
ref={searchingForRef}
>
{finished ? "Searched" : "Searching"} for: <i>{finalQuery}</i>
{finished ? "Searched" : "Searching"} for: <i> {finalQuery}</i>
</div>
</div>
);

View File

@ -27,17 +27,19 @@ export function SkippedSearch({
handleForceSearch: () => void;
}) {
return (
<div className="flex text-sm !pt-0 p-1">
<FiBook className="my-auto flex-none mr-2" size={14} />
<div className="my-auto cursor-default">
<span className="mobile:hidden">
The AI decided this query didn&apos;t need a search
</span>
<span className="desktop:hidden">No search</span>
<div className="flex text-sm !pt-0 p-1">
<div className="flex mb-auto">
<FiBook className="my-auto flex-none mr-2" size={14} />
<div className="my-auto cursor-default">
<span className="mobile:hidden">
The AI decided this query didn&apos;t need a search
</span>
<span className="desktop:hidden">No search</span>
</div>
</div>
<div className="ml-auto my-auto" onClick={handleForceSearch}>
<EmphasizedClickable>
<EmphasizedClickable size="sm">
<div className="w-24 text-xs">Force Search</div>
</EmphasizedClickable>
</div>

View File

@ -77,7 +77,7 @@ const AssistantCard = ({
</div>
</div>
<div className="text-xs text-subtle mb-2 mt-2 line-clamp-3 py-1">
<div className="text-xs text-subtle mb-2 text-wrap mt-2 line-clamp-3 py-1">
{assistant.description}
</div>
<div className="mt-2 flex flex-col gap-y-1">

View File

@ -15,7 +15,7 @@ export default function FixedLogo() {
return (
<>
<div className="fixed pointer-events-none flex z-40 left-2.5 top-2">
<div className="max-w-[200px] mobile:hidden flex items-center gap-x-1 my-auto">
<div className="max-w-[200px] mobile:hidden flex items-center gap-x-1 my-auto">
<div className="flex-none my-auto">
<Logo height={24} width={24} />
</div>

View File

@ -28,7 +28,7 @@ const ToggleSwitch = () => {
const handleTabChange = (tab: string) => {
setActiveTab(tab);
localStorage.setItem("activeTab", tab);
if (settings?.isMobile) {
if (settings?.isMobile && window) {
window.location.href = tab;
} else {
router.push(tab === "search" ? "/search" : "/chat");

View File

@ -1,5 +1,3 @@
import { LoadingAnimation } from "@/components/Loading";
export function ToolRunDisplay({
toolName,
toolLogo,
@ -12,7 +10,12 @@ export function ToolRunDisplay({
return (
<div className="text-sm text-subtle my-auto flex">
{toolLogo}
{isRunning ? <LoadingAnimation text={toolName} /> : toolName}
<div
className={`${isRunning && "loading-text"}
!text-sm !line-clamp-1 !break-all !px-0.5`}
>
{toolName}
</div>
</div>
);
}

View File

@ -37,29 +37,35 @@ export function EmphasizedClickable({
children,
onClick,
fullWidth = false,
size = "md",
}: {
children: string | JSX.Element;
onClick?: () => void;
fullWidth?: boolean;
size?: "sm" | "md" | "lg";
}) {
return (
<button
className={`
inline-flex
items-center
justify-center
flex-shrink-0
font-medium
min-h-[38px]
py-2
px-3
w-fit
bg-hover
border-1 border-border-medium border bg-background-100
text-sm
rounded-lg
hover:bg-background-125
`}
inline-flex
items-center
justify-center
flex-shrink-0
font-medium
${
size === "sm"
? `p-1`
: size === "md"
? `min-h-[38px] py-1 px-3`
: `min-h-[42px] py-2 px-4`
}
w-fit
bg-hover
border-1 border-border-medium border bg-background-100
text-sm
rounded-lg
hover:bg-background-125
`}
onClick={onClick}
>
{children}

View File

@ -61,7 +61,7 @@ export default function FunctionalHeader({
return (
<div className="pb-6 left-0 sticky top-0 z-20 w-full relative flex">
<div className="mt-2 mx-4 text-text-700 flex w-full">
<div className="absolute z-[100] my-auto flex items-center text-xl font-bold">
<div className="absolute z-[100] my-auto flex items-center text-xl font-bold">
<button
onClick={() => toggleSidebar()}
className="pt-[2px] desktop:invisible mb-auto"

View File

@ -14,6 +14,7 @@ export const EditingValue: React.FC<{
optional?: boolean;
description?: string;
setFieldValue: (field: string, value: any) => void;
showNever?: boolean;
// These are escape hatches from the overall
// value editing component (when need to modify)
@ -31,7 +32,7 @@ export const EditingValue: React.FC<{
description,
optional,
setFieldValue,
showNever,
onChange,
onChangeBool,
onChangeNumber,
@ -116,7 +117,11 @@ export const EditingValue: React.FC<{
type="number"
name={name}
value={value as number}
placeholder={currentValue}
placeholder={
currentValue === 0 && showNever
? "Never"
: currentValue?.toString()
}
onChange={(e) => {
const inputValue = e.target.value;
if (inputValue === "") {

View File

@ -540,7 +540,7 @@ export const SearchSection = ({
{
<div className="desktop:px-24 w-full pt-10 relative max-w-[2000px] xl:max-w-[1430px] mx-auto">
<div className="absolute z-10 mobile:px-4 mobile:max-w-searchbar-max mobile:w-[90%] top-12 desktop:left-0 hidden 2xl:block mobile:left-1/2 mobile:transform mobile:-translate-x-1/2 desktop:w-52 3xl:w-64">
<div className="absolute z-10 mobile:px-4 mobile:max-w-searchbar-max mobile:w-[90%] top-12 desktop:left-0 hidden 2xl:block mobile:left-1/2 mobile:transform mobile:-translate-x-1/2 desktop:w-52 3xl:w-64">
{!settings?.isMobile &&
(ccPairs.length > 0 || documentSets.length > 0) && (
<SourceSelector

View File

@ -22,7 +22,7 @@ export function Citation({
content={<div className="inline-block p-0 m-0 truncate">{link}</div>}
>
<a
onClick={() => (link ? window.open(link, "_blank") : undefined)}
onMouseDown={() => (link ? window.open(link, "_blank") : undefined)}
className="cursor-pointer inline ml-1 align-middle"
>
<span className="group relative -top-1 text-sm text-gray-500 dark:text-gray-400 selection:bg-indigo-300 selection:text-black dark:selection:bg-indigo-900 dark:selection:text-white">

View File

@ -202,6 +202,19 @@ export async function fetchChatData(searchParams: {
assistants = assistants.filter((assistant) => assistant.num_chunks === 0);
}
const hasOpenAIProvider = llmProviders.some(
(provider) => provider.provider === "openai"
);
if (!hasOpenAIProvider) {
assistants = assistants.filter(
(assistant) =>
!assistant.tools.some(
(tool) => tool.in_code_tool_id === "ImageGenerationTool"
)
);
}
// // TODO check for image capabilities and enable if so
let folders: Folder[] = [];
if (foldersResponse?.ok) {
folders = (await foldersResponse.json()).folders as Folder[];

View File

@ -24,6 +24,21 @@ export async function createConnector<T>(
return handleResponse(response);
}
export async function updateConnectorCredentialPairName(
ccPairId: number,
newName: string
): Promise<Response> {
return fetch(
`/api/manage/admin/cc-pair/${ccPairId}/name?new_name=${encodeURIComponent(newName)}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
}
);
}
export async function updateConnector<T>(
connector: Connector<T>
): Promise<Connector<T>> {

View File

@ -33,6 +33,8 @@ export const LOGOUT_DISABLED =
export const NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN =
process.env.NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN?.toLowerCase() === "true";
export const TOGGLED_CONNECTORS_COOKIE_NAME = "toggled_connectors";
/* Enterprise-only settings */
// NOTE: this should ONLY be used on the server-side. If used client side,