mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-04-08 11:58:34 +02:00
Various Admin Page + User Flow Improvements (#1987)
This commit is contained in:
parent
aa4a00cbc2
commit
0261d689dc
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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!"
|
||||
|
@ -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 = (
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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__)
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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({
|
||||
|
@ -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]}
|
||||
>
|
||||
|
@ -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>
|
||||
}
|
||||
/>
|
||||
|
@ -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,
|
||||
|
@ -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">
|
||||
|
@ -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> */}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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'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'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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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");
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
|
@ -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 === "") {
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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[];
|
||||
|
@ -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>> {
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user