Compare commits

...

13 Commits

Author SHA1 Message Date
pablonyx
d3540aa370 update 2025-04-17 09:30:49 -07:00
pablonyx
5d3d8fbd39 k 2025-04-17 09:30:49 -07:00
pablonyx
938e926d10 try a fix 2025-04-17 09:30:49 -07:00
pablonyx
f4f651fef0 fix preprocessing 2025-04-17 09:30:49 -07:00
pablonyx
b592f374b8 k 2025-04-17 09:30:49 -07:00
pablonyx
c295374655 looking good 2025-04-17 09:30:49 -07:00
pablonyx
f5bc35e48e update 2025-04-17 09:30:49 -07:00
pablonyx
25d9b9f1e9 push changes 2025-04-17 09:30:49 -07:00
pablonyx
2c2e0c77b5 k 2025-04-17 09:30:49 -07:00
pablonyx
38aed7799d nit 2025-04-17 09:30:49 -07:00
pablonyx
08f6ca45ea nit 2025-04-17 09:30:49 -07:00
pablonyx
e858fc344a improved my docs 2025-04-17 09:30:49 -07:00
pablonyx
5f0f1a5fdf update 2025-04-17 09:30:49 -07:00
49 changed files with 528 additions and 540 deletions

View File

@@ -24,6 +24,9 @@ from onyx.configs.constants import SSL_CERT_FILE
from shared_configs.configs import MULTI_TENANT, POSTGRES_DEFAULT_SCHEMA
from onyx.db.models import Base
from celery.backends.database.session import ResultModelBase # type: ignore
from onyx.db.engine import SqlEngine
SqlEngine.init_engine(10, 10)
# Make sure in alembic.ini [logger_root] level=INFO is set or most logging will be
# hidden! (defaults to level=WARN)

View File

@@ -11,6 +11,7 @@ from onyx.server.features.persona.models import PersonaSharedNotificationData
def make_persona_private(
persona_id: int,
creator_user_id: UUID | None,
user_ids: list[UUID] | None,
group_ids: list[int] | None,
db_session: Session,
@@ -29,15 +30,15 @@ def make_persona_private(
user_ids_set = set(user_ids)
for user_id in user_ids_set:
db_session.add(Persona__User(persona_id=persona_id, user_id=user_id))
create_notification(
user_id=user_id,
notif_type=NotificationType.PERSONA_SHARED,
db_session=db_session,
additional_data=PersonaSharedNotificationData(
persona_id=persona_id,
).model_dump(),
)
if user_id != creator_user_id:
create_notification(
user_id=user_id,
notif_type=NotificationType.PERSONA_SHARED,
db_session=db_session,
additional_data=PersonaSharedNotificationData(
persona_id=persona_id,
).model_dump(),
)
if group_ids:
group_ids_set = set(group_ids)

View File

@@ -40,6 +40,7 @@ def process_llm_stream(
# This stream will be the llm answer if no tool is chosen. When a tool is chosen,
# the stream will contain AIMessageChunks with tool call information.
for message in messages:
answer_piece = message.content
if not isinstance(answer_piece, str):
# this is only used for logging, so fine to

View File

@@ -51,7 +51,6 @@ def _parse_agent_event(
Parse the event into a typed object.
Return None if we are not interested in the event.
"""
event_type = event["event"]
# We always just yield the event data, but this piece is useful for two development reasons:

View File

@@ -167,7 +167,6 @@ class Answer:
break
processed_stream.append(packet)
yield packet
self._processed_stream = processed_stream
@property

View File

@@ -157,7 +157,6 @@ from onyx.utils.logger import setup_logger
from onyx.utils.long_term_log import LongTermLogger
from onyx.utils.telemetry import mt_cloud_telemetry
from onyx.utils.timing import log_function_time
from onyx.utils.timing import log_generator_function_time
from shared_configs.contextvars import get_current_tenant_id
logger = setup_logger()
@@ -1496,7 +1495,7 @@ def _post_llm_answer_processing(
yield StreamingError(error="Failed to parse LLM output")
@log_generator_function_time()
# @log_generator_function_time()
def stream_chat_message(
new_msg_req: CreateChatMessageRequest,
user: User | None,

View File

@@ -168,6 +168,7 @@ def _get_persona_by_name(
def make_persona_private(
persona_id: int,
creator_user_id: UUID | None,
user_ids: list[UUID] | None,
group_ids: list[int] | None,
db_session: Session,
@@ -179,15 +180,15 @@ def make_persona_private(
for user_uuid in user_ids:
db_session.add(Persona__User(persona_id=persona_id, user_id=user_uuid))
create_notification(
user_id=user_uuid,
notif_type=NotificationType.PERSONA_SHARED,
db_session=db_session,
additional_data=PersonaSharedNotificationData(
persona_id=persona_id,
).model_dump(),
)
if user_uuid != creator_user_id:
create_notification(
user_id=user_uuid,
notif_type=NotificationType.PERSONA_SHARED,
db_session=db_session,
additional_data=PersonaSharedNotificationData(
persona_id=persona_id,
).model_dump(),
)
db_session.commit()
@@ -262,6 +263,7 @@ def create_update_persona(
# Privatize Persona
versioned_make_persona_private(
persona_id=persona.id,
creator_user_id=user.id if user else None,
user_ids=create_persona_request.users,
group_ids=create_persona_request.groups,
db_session=db_session,
@@ -297,6 +299,7 @@ def update_persona_shared_users(
# Privatize Persona
versioned_make_persona_private(
persona_id=persona_id,
creator_user_id=user.id if user else None,
user_ids=user_ids,
group_ids=None,
db_session=db_session,

View File

@@ -90,8 +90,25 @@ def get_folders(
db_session: Session = Depends(get_session),
) -> list[UserFolderSnapshot]:
user_id = user.id if user else None
folders = db_session.query(UserFolder).filter(UserFolder.user_id == user_id).all()
return [UserFolderSnapshot.from_model(folder) for folder in folders]
# Get folders that belong to the user or have the RECENT_DOCS_FOLDER_ID
folders = (
db_session.query(UserFolder)
.filter(
(UserFolder.user_id == user_id) | (UserFolder.id == RECENT_DOCS_FOLDER_ID)
)
.all()
)
# For each folder, filter files to only include those belonging to the current user
result = []
for folder in folders:
folder_snapshot = UserFolderSnapshot.from_model(folder)
folder_snapshot.files = [
file for file in folder_snapshot.files if file.user_id == user_id
]
result.append(folder_snapshot)
return result
@router.get("/user/folder/{folder_id}")
@@ -103,13 +120,25 @@ def get_folder(
user_id = user.id if user else None
folder = (
db_session.query(UserFolder)
.filter(UserFolder.id == folder_id, UserFolder.user_id == user_id)
.filter(
UserFolder.id == folder_id,
(
(UserFolder.user_id == user_id)
| (UserFolder.id == RECENT_DOCS_FOLDER_ID)
),
)
.first()
)
if not folder:
raise HTTPException(status_code=404, detail="Folder not found")
return UserFolderSnapshot.from_model(folder)
folder_snapshot = UserFolderSnapshot.from_model(folder)
# Filter files to only include those belonging to the current user
folder_snapshot.files = [
file for file in folder_snapshot.files if file.user_id == user_id
]
return folder_snapshot
RECENT_DOCS_FOLDER_ID = -1

View File

@@ -27,7 +27,15 @@ ONYX_REQUEST_ID_CONTEXTVAR: contextvars.ContextVar[str | None] = contextvars.Con
def get_current_tenant_id() -> str:
tenant_id = CURRENT_TENANT_ID_CONTEXTVAR.get()
if tenant_id is None:
import traceback
if not MULTI_TENANT:
return POSTGRES_DEFAULT_SCHEMA
raise RuntimeError("Tenant ID is not set. This should never happen.")
stack_trace = traceback.format_stack()
error_message = (
"Tenant ID is not set. This should never happen.\nStack trace:\n"
+ "".join(stack_trace)
)
raise RuntimeError(error_message)
return tenant_id

View File

@@ -25,6 +25,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useAuthType } from "@/lib/hooks";
import { InfoIcon } from "lucide-react";
function parseJsonWithTrailingCommas(jsonString: string) {
// Regular expression to remove trailing commas before } or ]
@@ -159,25 +160,14 @@ function ActionForm({
component="div"
className="mb-4 text-error text-sm"
/>
<div className="mt-4 text-sm bg-blue-50 p-4 rounded-md border border-blue-200">
<div className="mt-4 text-sm bg-blue-50 text-blue-700 dark:text-blue-300 dark:bg-blue-900 p-4 rounded-md border border-blue-200 dark:border-blue-800">
<Link
href="https://docs.onyx.app/tools/custom"
className="text-link hover:underline flex items-center"
target="_blank"
rel="noopener noreferrer"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 mr-2"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
<InfoIcon className="w-4 h-4 mr-2 " />
Learn more about actions in our documentation
</Link>
</div>
@@ -367,7 +357,7 @@ interface ToolFormValues {
}
const ToolSchema = Yup.object().shape({
definition: Yup.string().required("Tool definition is required"),
definition: Yup.string().required("Action definition is required"),
customHeaders: Yup.array()
.of(
Yup.object().shape({

View File

@@ -34,8 +34,10 @@ export default async function Page(props: {
<ActionEditor tool={tool} />
</CardSection>
<Title className="mt-12">Delete Tool</Title>
<Text>Click the button below to permanently delete this tool.</Text>
<Title className="mt-12">Delete Action</Title>
<Text>
Click the button below to permanently delete this action.
</Text>
<div className="flex mt-6">
<DeleteToolButton toolId={tool.id} />
</div>
@@ -50,7 +52,7 @@ export default async function Page(props: {
<BackButton />
<AdminPageTitle
title="Edit Tool"
title="Edit Action"
icon={<ToolIcon size={32} className="my-auto" />}
/>

View File

@@ -232,7 +232,13 @@ export function AssistantEditor({
enabledToolsMap[tool.id] = personaCurrentToolIds.includes(tool.id);
});
const { selectedFiles, selectedFolders } = useDocumentsContext();
const {
selectedFiles,
selectedFolders,
setSelectedFiles,
setSelectedFolders,
refreshFolders,
} = useDocumentsContext();
const [showVisibilityWarning, setShowVisibilityWarning] = useState(false);
@@ -556,6 +562,8 @@ export function AssistantEditor({
// don't set groups if marked as public
const groups = values.is_public ? [] : values.selectedGroups;
const teamKnowledge = values.knowledge_source === "team_knowledge";
const submissionData: PersonaUpsertParameters = {
...values,
existing_prompt_id: existingPrompt?.id ?? null,
@@ -573,8 +581,13 @@ export function AssistantEditor({
? new Date(values.search_start_date)
: null,
num_chunks: numChunks,
user_file_ids: selectedFiles.map((file) => file.id),
user_folder_ids: selectedFolders.map((folder) => folder.id),
document_set_ids: teamKnowledge ? values.document_set_ids : [],
user_file_ids: teamKnowledge
? []
: selectedFiles.map((file) => file.id),
user_folder_ids: teamKnowledge
? []
: selectedFolders.map((folder) => folder.id),
};
let personaResponse;
@@ -625,6 +638,9 @@ export function AssistantEditor({
}
await refreshAssistants();
await refreshFolders();
setSelectedFiles([]);
setSelectedFolders([]);
router.push(
isAdminPage
@@ -972,6 +988,7 @@ export function AssistantEditor({
</div>
)}
<button
type="button"
onClick={() => setFilePickerModalOpen(true)}
className="text-primary hover:underline"
>

View File

@@ -396,7 +396,6 @@ export function LLMProviderUpdateForm({
/>
</div>
)}
<IsPublicGroupSelector
formikProps={formikProps}
objectName="LLM Provider"

View File

@@ -109,7 +109,7 @@ export const DocumentFeedbackTable = ({
<TableRow key={document.document_id}>
<TableCell className="whitespace-normal break-all">
<a
className="text-blue-600"
className="text-blue-600 dark:text-blue-400"
href={document.link}
target="_blank"
rel="noopener noreferrer"

View File

@@ -144,6 +144,7 @@ function Main() {
/>
{isPaidEnterpriseFeaturesEnabled && (
<Tabs
className="mt-2"
value={tabIndex.toString()}
onValueChange={(val) => setTabIndex(parseInt(val))}
>

View File

@@ -14,7 +14,7 @@ const Page = () => {
Authentication Error
</h2>
<p className="text-text-700 text-center">
We encountered an issue while attempting to log you in.
There was a problem with your login attempt.
</p>
<div className="bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 rounded-lg p-4 shadow-sm">
<h3 className="text-red-800 dark:text-red-400 font-semibold mb-2">
@@ -46,8 +46,11 @@ const Page = () => {
please reach out to your system administrator for assistance.
{NEXT_PUBLIC_CLOUD_ENABLED && (
<span className="block mt-1 text-blue-600">
A member of our team has been automatically notified about this
issue.
If you continue to experience problems please reach out to the
Onyx team at{" "}
<a href="mailto:support@onyx.app" className="text-blue-600">
support@onyx.app
</a>
</span>
)}
</p>

View File

@@ -141,9 +141,6 @@ export function EmailPasswordForm({
name="password"
label="Password"
type="password"
includeForgotPassword={
NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && !isSignup
}
placeholder="**************"
/>

View File

@@ -29,7 +29,7 @@ export default function LoginPage({
useSendAuthRequiredMessage();
return (
<div className="flex flex-col w-full justify-center">
{authUrl && authTypeMetadata && (
{authUrl && authTypeMetadata && authTypeMetadata.authType !== "cloud" && (
<>
<h2 className="text-center text-xl text-strong font-bold">
<LoginText />
@@ -43,24 +43,35 @@ export default function LoginPage({
)}
{authTypeMetadata?.authType === "cloud" && (
<div className="mt-4 w-full justify-center">
<div className="flex items-center w-full my-4">
<div className="flex-grow border-t border-background-300"></div>
<span className="px-4 text-text-500">or</span>
<div className="flex-grow border-t border-background-300"></div>
</div>
<div className="w-full justify-center">
<h2 className="text-center text-xl text-strong font-bold">
<LoginText />
</h2>
<EmailPasswordForm shouldVerify={true} nextUrl={nextUrl} />
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && (
<div className="flex mt-4 justify-between">
<div className="flex mt-4 justify-between">
<Link
href="/auth/forgot-password"
className="text-link font-medium"
className="ml-auto text-link font-medium"
>
Reset Password
</Link>
</div>
)}
{authUrl && authTypeMetadata && (
<>
<div className="flex items-center w-full my-4">
<div className="flex-grow border-t border-background-300"></div>
<span className="px-4 text-text-500">or</span>
<div className="flex-grow border-t border-background-300"></div>
</div>
<SignInButton
authorizeUrl={authUrl}
authType={authTypeMetadata?.authType}
/>
</>
)}
</div>
)}

View File

@@ -40,10 +40,19 @@ const Page = async (props: {
// if user is already logged in, take them to the main app page
if (currentUser && currentUser.is_active && !currentUser.is_anonymous_user) {
console.log("Login page: User is logged in, redirecting to chat", {
userId: currentUser.id,
is_active: currentUser.is_active,
is_anonymous: currentUser.is_anonymous_user,
});
if (authTypeMetadata?.requiresVerification && !currentUser.is_verified) {
return redirect("/auth/waiting-on-verification");
}
return redirect("/chat");
// Add a query parameter to indicate this is a redirect from login
// This will help prevent redirect loops
return redirect("/chat?from=login");
}
// get where to send the user to authenticate

View File

@@ -82,23 +82,22 @@ const Page = async (props: {
</>
)}
{cloud && authUrl && (
<div className="w-full justify-center">
<SignInButton authorizeUrl={authUrl} authType="cloud" />
<div className="flex items-center w-full my-4">
<div className="flex-grow border-t border-background-300"></div>
<span className="px-4 text-text-500">or</span>
<div className="flex-grow border-t border-background-300"></div>
</div>
</div>
)}
<EmailPasswordForm
isSignup
shouldVerify={authTypeMetadata?.requiresVerification}
nextUrl={nextUrl}
defaultEmail={defaultEmail}
/>
{cloud && authUrl && (
<div className="w-full justify-center">
<div className="flex items-center w-full my-4">
<div className="flex-grow border-t border-background-300"></div>
<span className="px-4 text-text-500">or</span>
<div className="flex-grow border-t border-background-300"></div>
</div>
<SignInButton authorizeUrl={authUrl} authType="cloud" />
</div>
)}
</div>
</>
</AuthFlowContainer>

View File

@@ -223,7 +223,6 @@ export function ChatPage({
const settings = useContext(SettingsContext);
const enterpriseSettings = settings?.enterpriseSettings;
const [viewingFilePicker, setViewingFilePicker] = useState(false);
const [toggleDocSelection, setToggleDocSelection] = useState(false);
const [documentSidebarVisible, setDocumentSidebarVisible] = useState(false);
const [proSearchEnabled, setProSearchEnabled] = useState(proSearchToggled);
@@ -1909,15 +1908,28 @@ export function ChatPage({
updateChatState("uploading", currentSessionId());
const [uploadedFiles, error] = await uploadFilesForChat(acceptedFiles);
if (error) {
setPopup({
type: "error",
message: error,
});
}
for (let file of acceptedFiles) {
const formData = new FormData();
formData.append("files", file);
const response = await uploadFile(formData, null);
setCurrentMessageFiles((prev) => [...prev, ...uploadedFiles]);
if (response.length > 0) {
const uploadedFile = response[0];
addSelectedFile(uploadedFile);
// setCurrentMessageFiles((prev) => [...prev, uploadedFile]);
} else {
setPopup({
type: "error",
message: "Failed to upload file",
});
}
}
// if (error) {
// setPopup({
// type: "error",
// message: error,
// });
// }
updateChatState("input", currentSessionId());
};
@@ -3294,7 +3306,7 @@ export function ChatPage({
: "w-[0px]"
}
`}
></div>
/>
</div>
)}
</Dropzone>

View File

@@ -149,7 +149,7 @@ export const FolderDropdown = forwardRef<HTMLDivElement, FolderDropdownProps>(
ref={setNodeRef}
style={style}
{...attributes}
className="overflow-visible mt-2 w-full"
className="overflow-visible pt-2 w-full"
onDragOver={handleDragOver}
onDrop={handleDrop}
>
@@ -159,13 +159,13 @@ export const FolderDropdown = forwardRef<HTMLDivElement, FolderDropdownProps>(
>
<div
ref={ref}
className="flex overflow-visible items-center w-full text-text-darker rounded-md p-1 relative sticky top-0"
className="flex overflow-visible items-center w-full text-text-darker rounded-md p-1 bg-background-sidebar dark:bg-[#000] relative sticky top-0"
style={{ zIndex: 10 - index }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<button
className="flex overflow-hidden items-center flex-grow"
className="flex overflow-hidden bg-background-sidebar dark:bg-[#000] items-center flex-grow"
onClick={() => !isEditing && setIsOpen(!isOpen)}
{...(isEditing ? {} : listeners)}
>

View File

@@ -402,17 +402,16 @@ export function ChatInputBar({
}
}
}
if (!showPrompts && !showSuggestions) {
return;
}
if (e.key === "ArrowDown") {
e.preventDefault();
setTabbingIconIndex((tabbingIconIndex) =>
Math.min(
tabbingIconIndex + 1,
// showPrompts ? filteredPrompts.length :
assistantTagOptions.length
showPrompts ? filteredPrompts.length : assistantTagOptions.length
)
);
} else if (e.key === "ArrowUp") {
@@ -440,13 +439,14 @@ export function ChatInputBar({
ref={suggestionsRef}
className="text-sm absolute w-[calc(100%-2rem)] top-0 transform -translate-y-full"
>
<div className="rounded-lg py-1 sm-1.5 bg-input-background border border-border dark:border-none shadow-lg px-1.5 mt-2 z-10">
<div className="rounded-lg py-1 overflow-y-auto max-h-[200px] sm-1.5 bg-input-background border border-border dark:border-none shadow-lg px-1.5 mt-2 z-10">
{assistantTagOptions.map((currentAssistant, index) => (
<button
key={index}
className={`px-2 ${
tabbingIconIndex == index && "bg-neutral-200"
} rounded items-center rounded-lg content-start flex gap-x-1 py-2 w-full hover:bg-neutral-200/90 cursor-pointer`}
tabbingIconIndex == index &&
"bg-neutral-200 dark:bg-neutral-800"
} rounded items-center rounded-lg content-start flex gap-x-1 py-2 w-full hover:bg-neutral-200/90 dark:hover:bg-neutral-800/90 cursor-pointer`}
onClick={() => {
updatedTaggedAssistant(currentAssistant);
}}
@@ -468,8 +468,8 @@ export function ChatInputBar({
target="_self"
className={`${
tabbingIconIndex == assistantTagOptions.length &&
"bg-neutral-200"
} rounded rounded-lg px-3 flex gap-x-1 py-2 w-full items-center hover:bg-neutral-200/90 cursor-pointer`}
"bg-neutral-200 dark:bg-neutral-800"
} rounded rounded-lg px-3 flex gap-x-1 py-2 w-full items-center hover:bg-neutral-200/90 dark:hover:bg-neutral-800/90 cursor-pointer`}
href="/assistants/new"
>
<FiPlus size={17} />
@@ -484,14 +484,15 @@ export function ChatInputBar({
ref={suggestionsRef}
className="text-sm absolute inset-x-0 top-0 w-full transform -translate-y-full"
>
<div className="rounded-lg py-1.5 bg-input-background dark:border-none border border-border shadow-lg mx-2 px-1.5 mt-2 rounded z-10">
<div className="rounded-lg overflow-y-auto max-h-[200px] py-1.5 bg-input-background dark:border-none border border-border shadow-lg mx-2 px-1.5 mt-2 rounded z-10">
{filteredPrompts.map(
(currentPrompt: InputPrompt, index: number) => (
<button
key={index}
className={`px-2 ${
tabbingIconIndex == index && "bg-background-dark/75"
} rounded content-start flex gap-x-1 py-1.5 w-full hover:bg-background-dark/90 cursor-pointer`}
tabbingIconIndex == index &&
"bg-background-dark/75 dark:bg-neutral-800/75"
} rounded content-start flex gap-x-1 py-1.5 w-full hover:bg-background-dark/90 dark:hover:bg-neutral-800/90 cursor-pointer`}
onClick={() => {
updateInputPrompt(currentPrompt);
}}
@@ -509,8 +510,8 @@ export function ChatInputBar({
target="_self"
className={`${
tabbingIconIndex == filteredPrompts.length &&
"bg-background-dark/75"
} px-3 flex gap-x-1 py-2 w-full rounded-lg items-center hover:bg-background-dark/90 cursor-pointer`}
"bg-background-dark/75 dark:bg-neutral-800/75"
} px-3 flex gap-x-1 py-2 w-full rounded-lg items-center hover:bg-background-dark/90 dark:hover:bg-neutral-800/90 cursor-pointer`}
href="/chat/input-prompts"
>
<FiPlus size={17} />

View File

@@ -18,6 +18,7 @@ export default async function Layout({
);
if ("redirect" in data) {
console.log("redirect", data.redirect);
redirect(data.redirect);
}

View File

@@ -36,7 +36,10 @@ export const MemoizedAnchor = memo(
if (match) {
const isUserFileCitation = userFiles?.length && userFiles.length > 0;
if (isUserFileCitation) {
const index = parseInt(match[2], 10) - 1;
const index = Math.min(
parseInt(match[2], 10) - 1,
userFiles?.length - 1
);
const associatedUserFile = userFiles?.[index];
if (!associatedUserFile) {
return <a href={children as string}>{children}</a>;

View File

@@ -342,12 +342,7 @@ export const AIMessage = ({
}
const processed = preprocessLaTeX(content);
// Escape $ that are preceded by a space and followed by a non-$ character
const escapedDollarSigns = processed.replace(/([\s])\$([^\$])/g, "$1\\$$2");
return (
escapedDollarSigns + (!isComplete && !toolCallGenerating ? " [*]() " : "")
);
return processed + (!isComplete && !toolCallGenerating ? " [*]() " : "");
};
const finalContentProcessed = processContent(finalContent as string);

View File

@@ -5,7 +5,7 @@ import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces"
import { destructureValue, structureValue } from "@/lib/llm/utils";
import { setUserDefaultModel } from "@/lib/users/UserSettings";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { useUser } from "@/components/user/UserProvider";
import { Separator } from "@/components/ui/separator";
@@ -209,6 +209,8 @@ export function UserSettingsModal({
setIsLoading(false);
}
};
const pathname = usePathname();
const showPasswordSection = user?.password_configured;
const handleDeleteAllChats = async () => {
@@ -221,7 +223,9 @@ export function UserSettingsModal({
type: "success",
});
refreshChatSessions();
router.push("/chat");
if (pathname.includes("/chat")) {
router.push("/chat");
}
} else {
throw new Error("Failed to delete all chat sessions");
}
@@ -384,7 +388,7 @@ export function UserSettingsModal({
<div className="pt-4 border-t border-border">
{!showDeleteConfirmation ? (
<div className="space-y-3">
<p className="text-sm text-neutral-600 ">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
This will permanently delete all your chat sessions and
cannot be undone.
</p>
@@ -399,7 +403,7 @@ export function UserSettingsModal({
</div>
) : (
<div className="space-y-3">
<p className="text-sm text-neutral-600 ">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Are you sure you want to delete all your chat sessions?
</p>
<div className="flex gap-2">

View File

@@ -160,6 +160,7 @@ export const DocumentsProvider: React.FC<DocumentsProviderProps> = ({
const refreshFolders = async () => {
try {
console.log("fetching folders");
const data = await documentsService.fetchFolders();
setFolders(data);
} catch (error) {

View File

@@ -1,6 +1,6 @@
"use client";
import React, { useMemo, useState, useTransition } from "react";
import React, { useEffect, useMemo, useState, useTransition } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import {
Plus,
@@ -68,11 +68,23 @@ export default function MyDocuments() {
const [sortDirection, setSortDirection] = useState<SortDirection>(
SortDirection.Descending
);
const pageLimit = 10;
const searchParams = useSearchParams();
const router = useRouter();
const { popup, setPopup } = usePopup();
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
useEffect(() => {
const createFolder = searchParams.get("createFolder");
if (createFolder) {
setIsCreateFolderOpen(true);
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.delete("createFolder");
router.replace(`?${newSearchParams.toString()}`);
}
}, [searchParams]);
const [isPending, startTransition] = useTransition();
const [hoveredColumn, setHoveredColumn] = useState<SortType | null>(null);
@@ -118,120 +130,24 @@ export default function MyDocuments() {
};
const handleDeleteItem = async (itemId: number, isFolder: boolean) => {
if (!isFolder) {
// For files, keep the old confirmation
const confirmDelete = window.confirm(
`Are you sure you want to delete this file?`
);
if (confirmDelete) {
try {
await deleteItem(itemId, isFolder);
setPopup({
message: `File deleted successfully`,
type: "success",
});
await refreshFolders();
} catch (error) {
console.error("Error deleting item:", error);
setPopup({
message: `Failed to delete file`,
type: "error",
});
}
}
}
// If it's a folder, the SharedFolderItem component will handle it
};
const handleMoveItem = async (
itemId: number,
currentFolderId: number | null,
isFolder: boolean
) => {
const availableFolders = folders
.filter((folder) => folder.id !== itemId)
.map((folder) => `${folder.id}: ${folder.name}`)
.join("\n");
const promptMessage = `Enter the ID of the destination folder:\n\nAvailable folders:\n${availableFolders}\n\nEnter 0 to move to the root folder.`;
const destinationFolderId = prompt(promptMessage);
if (destinationFolderId !== null) {
const newFolderId = parseInt(destinationFolderId, 10);
if (isNaN(newFolderId)) {
setPopup({
message: "Invalid folder ID",
type: "error",
});
return;
}
try {
await moveItem(
itemId,
newFolderId === 0 ? null : newFolderId,
isFolder
);
setPopup({
message: `${
isFolder ? "Knowledge Group" : "File"
} moved successfully`,
type: "success",
});
await refreshFolders();
} catch (error) {
console.error("Error moving item:", error);
setPopup({
message: "Failed to move item",
type: "error",
});
}
}
};
const handleDownloadItem = async (documentId: string) => {
try {
await downloadItem(documentId);
} catch (error) {
console.error("Error downloading file:", error);
await deleteItem(itemId, isFolder);
setPopup({
message: "Failed to download file",
message: isFolder
? `Folder deleted successfully`
: `File deleted successfully`,
type: "success",
});
await refreshFolders();
} catch (error) {
console.error("Error deleting item:", error);
setPopup({
message: `Failed to delete ${isFolder ? "folder" : "file"}`,
type: "error",
});
}
};
const onRenameItem = async (
itemId: number,
currentName: string,
isFolder: boolean
) => {
const newName = prompt(
`Enter new name for ${isFolder ? "Knowledge Group" : "File"}:`,
currentName
);
if (newName && newName !== currentName) {
try {
await renameItem(itemId, newName, isFolder);
setPopup({
message: `${
isFolder ? "Knowledge Group" : "File"
} renamed successfully`,
type: "success",
});
await refreshFolders();
} catch (error) {
console.error("Error renaming item:", error);
setPopup({
message: `Failed to rename ${isFolder ? "Knowledge Group" : "File"}`,
type: "error",
});
}
}
};
const filteredFolders = useMemo(() => {
return folders
.filter(
@@ -440,11 +356,7 @@ export default function MyDocuments() {
onClick={handleFolderClick}
description={folder.description}
lastUpdated={folder.created_at}
onRename={() => onRenameItem(folder.id, folder.name, true)}
onDelete={() => handleDeleteItem(folder.id, true)}
onMove={() =>
handleMoveItem(folder.id, currentFolder, true)
}
/>
))}
</div>

View File

@@ -590,29 +590,6 @@ export default function UserFolderContent({ folderId }: { folderId: number }) {
{/* Invalid file message */}
{/* Add a visual overlay when dragging files */}
{isDraggingOver && (
<div className="fixed inset-0 bg-neutral-950/10 backdrop-blur-sm z-50 pointer-events-none flex items-center justify-center transition-all duration-200 ease-in-out">
<div className="bg-white dark:bg-neutral-900 rounded-lg p-8 shadow-lg text-center border border-neutral-200 dark:border-neutral-800 max-w-md mx-auto">
<div className="bg-neutral-100 dark:bg-neutral-800 p-4 rounded-full w-20 h-20 mx-auto mb-5 flex items-center justify-center">
<Upload
className="w-10 h-10 text-neutral-600 dark:text-neutral-300"
strokeWidth={1.5}
/>
</div>
<h3 className="text-xl font-medium mb-2 text-neutral-900 dark:text-neutral-50">
Drop files to upload
</h3>
<p className="text-neutral-500 dark:text-neutral-400 text-sm">
Files will be uploaded to{" "}
<span className="font-medium text-neutral-900 dark:text-neutral-200">
{folderDetails?.name || "this folder"}
</span>
</p>
</div>
</div>
)}
<DeleteEntityModal
isOpen={isDeleteModalOpen}
onClose={() => setIsDeleteModalOpen(false)}
@@ -636,9 +613,9 @@ export default function UserFolderContent({ folderId }: { folderId: number }) {
<div className="flex -mt-[1px] flex-col w-full">
<div className="flex items-center mb-3">
<nav className="flex text-lg gap-x-1 items-center">
<nav className="flex text-base md:text-lg gap-x-1 items-center">
<span
className="font-medium leading-tight tracking-tight text-lg text-neutral-800 dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-neutral-100 cursor-pointer flex items-center text-base"
className="font-medium leading-tight tracking-tight text-neutral-800 dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-neutral-100 cursor-pointer flex items-center"
onClick={handleBack}
>
My Documents

View File

@@ -255,7 +255,7 @@ export const FileListItem: React.FC<FileListItemProps> = ({
<PopoverTrigger asChild>
<Button
variant="ghost"
className="group-hover:visible invisible h-8 w-8 p-0"
className="group-hover:visible mobile:visible invisible h-8 w-8 p-0"
>
<MoreHorizontal className="h-4 w-4" />
</Button>

View File

@@ -104,7 +104,7 @@ const DraggableItem: React.FC<{
<div className="w-6 flex items-center justify-center shrink-0">
<div
className={`${
isSelected ? "" : "opacity-0 group-hover:opacity-100"
isSelected ? "" : "desktop:opacity-0 group-hover:opacity-100"
} transition-opacity duration-150`}
onClick={(e) => {
e.stopPropagation();
@@ -199,7 +199,7 @@ const FilePickerFolderItem: React.FC<{
className={`transition-opacity duration-150 ${
isSelected || allFilesSelected
? "opacity-100"
: "opacity-0 group-hover:opacity-100"
: "desktop:opacity-0 group-hover:opacity-100"
}`}
onClick={(e) => {
e.preventDefault();
@@ -324,7 +324,6 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
} = useDocumentsContext();
const router = useRouter();
const [linkUrl, setLinkUrl] = useState("");
const [isCreatingFileFromLink, setIsCreatingFileFromLink] = useState(false);
const [isUploadingFile, setIsUploadingFile] = useState(false);
@@ -395,12 +394,6 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
}
}, [isOpen, selectedFiles, selectedFolders]);
useEffect(() => {
if (isOpen) {
refreshFolders();
}
}, [isOpen, refreshFolders]);
useEffect(() => {
if (currentFolder) {
if (currentFolder === -1) {
@@ -1087,7 +1080,7 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
}
>
<div className="h-[calc(70vh-5rem)] flex overflow-visible flex-col">
<div className="grid overflow-x-visible h-full overflow-y-hidden flex-1 w-full divide-x divide-neutral-200 dark:divide-neutral-700 grid-cols-2">
<div className="grid overflow-x-visible h-full overflow-y-hidden flex-1 w-full divide-x divide-neutral-200 dark:divide-neutral-700 desktop:grid-cols-2">
<div className="w-full h-full pb-4 overflow-hidden ">
<div className="px-6 sticky flex flex-col gap-y-2 z-[1000] top-0 mb-2 flex gap-x-2 w-full pr-4">
<div className="w-full relative">
@@ -1251,16 +1244,16 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
) : folders.length > 0 ? (
<div className="flex-grow overflow-y-auto px-4">
<p className="text-text-subtle dark:text-neutral-400">
No groups found
No folders found
</p>
</div>
) : (
<div className="flex-grow flex-col overflow-y-auto px-4 flex items-start justify-start gap-y-2">
<p className="text-sm text-muted-foreground dark:text-neutral-400">
No groups found
No folders found
</p>
<a
href="/chat/my-documents"
href="/chat/my-documents?createFolder=true"
className="inline-flex items-center text-sm justify-center text-neutral-600 dark:text-neutral-400 hover:underline"
>
<FolderIcon className="mr-2 h-4 w-4" />
@@ -1270,14 +1263,20 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
)}
</div>
<div
className={`w-full h-full flex flex-col ${
className={`mobile:hidden overflow-y-auto w-full h-full flex flex-col ${
isHoveringRight ? "bg-neutral-100 dark:bg-neutral-800/30" : ""
}`}
onDragEnter={() => setIsHoveringRight(true)}
onDragLeave={() => setIsHoveringRight(false)}
>
<div className="px-5 pb-5 flex-1 flex flex-col">
<div className="shrink default-scrollbar flex h-full overflow-y-auto mb-3">
<div className="px-5 h-full flex flex-col">
{/* Top section: scrollable, takes remaining space */}
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-neutral-800 dark:text-neutral-100">
Selected Items
</h3>
</div>
<div className="flex-1 min-h-0 overflow-y-auto">
<SelectedItemsList
uploadingFiles={uploadingFiles}
setPresentingDocument={setPresentingDocument}
@@ -1288,69 +1287,68 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
/>
</div>
<div className="flex flex-col space-y-3">
<div className="flex flex-col space-y-2">
<FileUploadSection
disabled={isUploadingFile || isCreatingFileFromLink}
onUpload={(files: File[]) => {
setIsUploadingFile(true);
setUploadStartTime(Date.now()); // Record start time
{/* Bottom section: fixed height, doesn't flex */}
<div className="flex-none py-2">
<FileUploadSection
disabled={isUploadingFile || isCreatingFileFromLink}
onUpload={(files: File[]) => {
setIsUploadingFile(true);
setUploadStartTime(Date.now()); // Record start time
// Add files to uploading files state
// Start the refresh interval to simulate progress
startRefreshInterval();
// Start the refresh interval to simulate progress
startRefreshInterval();
// Convert File[] to FileList for addUploadedFileToContext
const fileListArray = Array.from(files);
const fileList = new DataTransfer();
fileListArray.forEach((file) => fileList.items.add(file));
// Convert File[] to FileList for addUploadedFileToContext
const fileListArray = Array.from(files);
const fileList = new DataTransfer();
fileListArray.forEach((file) => fileList.items.add(file));
addUploadedFileToContext(fileList.files)
.then(() => refreshFolders())
.finally(() => {
setIsUploadingFile(false);
});
}}
onUrlUpload={async (url: string) => {
setIsCreatingFileFromLink(true);
setUploadStartTime(Date.now()); // Record start time
addUploadedFileToContext(fileList.files)
.then(() => refreshFolders())
.finally(() => {
setIsUploadingFile(false);
});
}}
onUrlUpload={async (url: string) => {
setIsCreatingFileFromLink(true);
setUploadStartTime(Date.now()); // Record start time
// Add URL to uploading files
setUploadingFiles((prev) => [
...prev,
{ name: url, progress: 0 },
]);
// Add URL to uploading files
setUploadingFiles((prev) => [
...prev,
{ name: url, progress: 0 },
]);
// Start the refresh interval to simulate progress
startRefreshInterval();
// Start the refresh interval to simulate progress
startRefreshInterval();
try {
const response: FileResponse[] = await createFileFromLink(
url,
-1
);
try {
const response: FileResponse[] =
await createFileFromLink(url, -1);
if (response.length > 0) {
// Extract domain from URL to help with detection
const urlObj = new URL(url);
if (response.length > 0) {
// Extract domain from URL to help with detection
const urlObj = new URL(url);
const createdFile: FileResponse = response[0];
addSelectedFile(createdFile);
// Make sure to remove the uploading file indicator when done
markFileComplete(url);
}
await refreshFolders();
} catch (e) {
console.error("Error creating file from link:", e);
// Also remove the uploading indicator on error
const createdFile: FileResponse = response[0];
addSelectedFile(createdFile);
// Make sure to remove the uploading file indicator when done
markFileComplete(url);
} finally {
setIsCreatingFileFromLink(false);
}
}}
isUploading={isUploadingFile || isCreatingFileFromLink}
/>
</div>
await refreshFolders();
} catch (e) {
console.error("Error creating file from link:", e);
// Also remove the uploading indicator on error
markFileComplete(url);
} finally {
setIsCreatingFileFromLink(false);
}
}}
isUploading={isUploadingFile || isCreatingFileFromLink}
/>
</div>
</div>
</div>
@@ -1375,6 +1373,7 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
<TooltipTrigger asChild>
<div>
<Button
type="button"
onClick={onSave}
className="px-8 py-2 w-48"
disabled={

View File

@@ -26,7 +26,8 @@ export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
onRemoveFolder,
setPresentingDocument,
}) => {
const hasItems = folders.length > 0 || files.length > 0;
const hasItems =
folders.length > 0 || files.length > 0 || uploadingFiles.length > 0;
const openFile = (file: FileResponse) => {
if (file.link_url) {
window.open(file.link_url, "_blank");
@@ -40,89 +41,143 @@ export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
return (
<div className="h-full w-full flex flex-col">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-neutral-800 dark:text-neutral-100">
Selected Items
</h3>
</div>
<ScrollArea className="h-[200px] flex-grow pr-1">
<div className="space-y-2.5">
{folders.length > 0 && (
<div className="space-y-2.5">
{folders.map((folder: FolderResponse) => (
<div key={folder.id} className="group flex items-center gap-2">
<div
className={cn(
"group flex-1 flex items-center rounded-md border p-2.5",
"bg-neutral-100/80 border-neutral-200 hover:bg-neutral-200/60",
"dark:bg-neutral-800/80 dark:border-neutral-700 dark:hover:bg-neutral-750",
"dark:focus:ring-1 dark:focus:ring-neutral-500 dark:focus:border-neutral-600",
"dark:active:bg-neutral-700 dark:active:border-neutral-600",
"transition-colors duration-150"
)}
>
<div className="flex items-center min-w-0 flex-1">
<FolderIcon className="h-5 w-5 mr-2 text-black dark:text-black shrink-0 fill-black dark:fill-black" />
<span className="text-sm font-medium truncate text-neutral-800 dark:text-neutral-100">
{truncateString(folder.name, 34)}
</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => onRemoveFolder(folder)}
className={cn(
"bg-transparent hover:bg-transparent opacity-0 group-hover:opacity-100",
"h-6 w-6 p-0 rounded-full shrink-0",
"hover:text-neutral-700",
"dark:text-neutral-300 dark:hover:text-neutral-100",
"dark:focus:ring-1 dark:focus:ring-neutral-500",
"dark:active:bg-neutral-500 dark:active:text-white",
"transition-all duration-150 ease-in-out"
)}
aria-label={`Remove folder ${folder.name}`}
>
<X className="h-3 w-3 dark:text-neutral-200" />
</Button>
</div>
))}
</div>
)}
{files.length > 0 && (
<div className="space-y-2.5 ">
{files.map((file: FileResponse) => (
<div className="space-y-2.5 pb-2">
{folders.length > 0 && (
<div className="space-y-2.5">
{folders.map((folder: FolderResponse) => (
<div key={folder.id} className="group flex items-center gap-2">
<div
key={file.id}
className="group w-full flex items-center gap-2"
className={cn(
"group flex-1 flex items-center rounded-md border p-2.5",
"bg-neutral-100/80 border-neutral-200 hover:bg-neutral-200/60",
"dark:bg-neutral-800/80 dark:border-neutral-700 dark:hover:bg-neutral-750",
"dark:focus:ring-1 dark:focus:ring-neutral-500 dark:focus:border-neutral-600",
"dark:active:bg-neutral-700 dark:active:border-neutral-600",
"transition-colors duration-150"
)}
>
<div
className={cn(
"group flex-1 flex items-center rounded-md border p-2.5",
"bg-neutral-50 border-neutral-200 hover:bg-neutral-100",
"dark:bg-neutral-800/70 dark:border-neutral-700 dark:hover:bg-neutral-750",
"dark:focus:ring-1 dark:focus:ring-neutral-500 dark:focus:border-neutral-600",
"dark:active:bg-neutral-700 dark:active:border-neutral-600",
"transition-colors duration-150",
"cursor-pointer"
)}
onClick={() => openFile(file)}
>
<div className="flex items-center min-w-0 flex-1">
{getFileIconFromFileNameAndLink(file.name, file.link_url)}
<span className="text-sm truncate text-neutral-700 dark:text-neutral-200 ml-2.5">
{truncateString(file.name, 34)}
<div className="flex items-center min-w-0 flex-1">
<FolderIcon className="h-5 w-5 mr-2 text-black dark:text-black shrink-0 fill-black dark:fill-black" />
<span className="text-sm font-medium truncate text-neutral-800 dark:text-neutral-100">
{truncateString(folder.name, 34)}
</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => onRemoveFolder(folder)}
className={cn(
"bg-transparent hover:bg-transparent opacity-0 group-hover:opacity-100",
"h-6 w-6 p-0 rounded-full shrink-0",
"hover:text-neutral-700",
"dark:text-neutral-300 dark:hover:text-neutral-100",
"dark:focus:ring-1 dark:focus:ring-neutral-500",
"dark:active:bg-neutral-500 dark:active:text-white",
"transition-all duration-150 ease-in-out"
)}
aria-label={`Remove folder ${folder.name}`}
>
<X className="h-3 w-3 dark:text-neutral-200" />
</Button>
</div>
))}
</div>
)}
{files.length > 0 && (
<div className="space-y-2.5 ">
{files.map((file: FileResponse) => (
<div
key={file.id}
className="group w-full flex items-center gap-2"
>
<div
className={cn(
"group flex-1 flex items-center rounded-md border p-2.5",
"bg-neutral-50 border-neutral-200 hover:bg-neutral-100",
"dark:bg-neutral-800/70 dark:border-neutral-700 dark:hover:bg-neutral-750",
"dark:focus:ring-1 dark:focus:ring-neutral-500 dark:focus:border-neutral-600",
"dark:active:bg-neutral-700 dark:active:border-neutral-600",
"transition-colors duration-150",
"cursor-pointer"
)}
onClick={() => openFile(file)}
>
<div className="flex items-center min-w-0 flex-1">
{getFileIconFromFileNameAndLink(file.name, file.link_url)}
<span className="text-sm truncate text-neutral-700 dark:text-neutral-200 ml-2.5">
{truncateString(file.name, 34)}
</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => onRemoveFile(file)}
className={cn(
"bg-transparent hover:bg-transparent opacity-0 group-hover:opacity-100",
"h-6 w-6 p-0 rounded-full shrink-0",
"hover:text-neutral-700",
"dark:text-neutral-300 dark:hover:text-neutral-100",
"dark:focus:ring-1 dark:focus:ring-neutral-500",
"dark:active:bg-neutral-500 dark:active:text-white",
"transition-all duration-150 ease-in-out"
)}
aria-label={`Remove file ${file.name}`}
>
<X className="h-3 w-3 dark:text-neutral-200" />
</Button>
</div>
))}
</div>
)}
<div className="max-w-full space-y-2.5">
{uploadingFiles
.filter(
(uploadingFile) =>
!files.map((file) => file.name).includes(uploadingFile.name)
)
.map((uploadingFile, index) => (
<div key={index} className="mr-8 flex items-center gap-2">
<div
key={`uploading-${index}`}
className={cn(
"group flex-1 flex items-center rounded-md border p-2.5",
"bg-neutral-50 border-neutral-200 hover:bg-neutral-100",
"dark:bg-neutral-800/70 dark:border-neutral-700 dark:hover:bg-neutral-750",
"dark:focus:ring-1 dark:focus:ring-neutral-500 dark:focus:border-neutral-600",
"dark:active:bg-neutral-700 dark:active:border-neutral-600",
"transition-colors duration-150",
"cursor-pointer"
)}
>
<div className="flex items-center min-w-0 flex-1">
<div className="flex items-center gap-2 min-w-0">
{uploadingFile.name.startsWith("http") ? (
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
) : (
<CircularProgress
progress={uploadingFile.progress}
size={18}
showPercentage={false}
/>
)}
<span className="truncate text-sm text-text-dark dark:text-text-dark">
{uploadingFile.name.startsWith("http")
? `${uploadingFile.name.substring(0, 30)}${
uploadingFile.name.length > 30 ? "..." : ""
}`
: truncateString(uploadingFile.name, 34)}
</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => onRemoveFile(file)}
// onClick={() => onRemoveFile(file)}
className={cn(
"bg-transparent hover:bg-transparent opacity-0 group-hover:opacity-100",
"h-6 w-6 p-0 rounded-full shrink-0",
@@ -132,82 +187,20 @@ export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
"dark:active:bg-neutral-500 dark:active:text-white",
"transition-all duration-150 ease-in-out"
)}
aria-label={`Remove file ${file.name}`}
// aria-label={`Remove file ${file.name}`}
>
<X className="h-3 w-3 dark:text-neutral-200" />
</Button>
</div>
))}
</div>
)}
<div className="max-w-full space-y-2.5">
{uploadingFiles
.filter(
(uploadingFile) =>
!files.map((file) => file.name).includes(uploadingFile.name)
)
.map((uploadingFile, index) => (
<div key={index} className="mr-8 flex items-center gap-2">
<div
key={`uploading-${index}`}
className={cn(
"group flex-1 flex items-center rounded-md border p-2.5",
"bg-neutral-50 border-neutral-200 hover:bg-neutral-100",
"dark:bg-neutral-800/70 dark:border-neutral-700 dark:hover:bg-neutral-750",
"dark:focus:ring-1 dark:focus:ring-neutral-500 dark:focus:border-neutral-600",
"dark:active:bg-neutral-700 dark:active:border-neutral-600",
"transition-colors duration-150",
"cursor-pointer"
)}
>
<div className="flex items-center min-w-0 flex-1">
<div className="flex items-center gap-2 min-w-0">
{uploadingFile.name.startsWith("http") ? (
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
) : (
<CircularProgress
progress={uploadingFile.progress}
size={18}
showPercentage={false}
/>
)}
<span className="truncate text-sm text-text-dark dark:text-text-dark">
{uploadingFile.name.startsWith("http")
? `${uploadingFile.name.substring(0, 30)}${
uploadingFile.name.length > 30 ? "..." : ""
}`
: truncateString(uploadingFile.name, 34)}
</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
// onClick={() => onRemoveFile(file)}
className={cn(
"bg-transparent hover:bg-transparent opacity-0 group-hover:opacity-100",
"h-6 w-6 p-0 rounded-full shrink-0",
"hover:text-neutral-700",
"dark:text-neutral-300 dark:hover:text-neutral-100",
"dark:focus:ring-1 dark:focus:ring-neutral-500",
"dark:active:bg-neutral-500 dark:active:text-white",
"transition-all duration-150 ease-in-out"
)}
// aria-label={`Remove file ${file.name}`}
>
<X className="h-3 w-3 dark:text-neutral-200" />
</Button>
</div>
</div>
))}
</div>
{!hasItems && (
<div className="flex items-center justify-center h-24 text-sm text-neutral-500 dark:text-neutral-400 italic bg-neutral-50/50 dark:bg-neutral-800/30 rounded-md border border-neutral-200/50 dark:border-neutral-700/50">
No items selected
</div>
)}
</div>
))}
</div>
</ScrollArea>
{!hasItems && (
<div className="flex items-center justify-center h-24 text-sm text-neutral-500 dark:text-neutral-400 italic bg-neutral-50/50 dark:bg-neutral-800/30 rounded-md border border-neutral-200/50 dark:border-neutral-700/50">
No items selected
</div>
)}
</div>
</div>
);
};

View File

@@ -26,9 +26,7 @@ interface SharedFolderItemProps {
onClick: (folderId: number) => void;
description?: string;
lastUpdated?: string;
onRename: () => void;
onDelete: () => void;
onMove: () => void;
}
export const SharedFolderItem: React.FC<SharedFolderItemProps> = ({
@@ -36,9 +34,7 @@ export const SharedFolderItem: React.FC<SharedFolderItemProps> = ({
onClick,
description,
lastUpdated,
onRename,
onDelete,
onMove,
}) => {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@@ -99,7 +95,7 @@ export const SharedFolderItem: React.FC<SharedFolderItemProps> = ({
<PopoverTrigger asChild>
<Button
variant="ghost"
className={`group-hover:visible invisible h-8 w-8 p-0 ${
className={`group-hover:visible mobile:visible invisible h-8 w-8 p-0 ${
folder.id === -1 ? "!invisible pointer-events-none" : ""
}`}
>
@@ -108,14 +104,6 @@ export const SharedFolderItem: React.FC<SharedFolderItemProps> = ({
</PopoverTrigger>
<PopoverContent className="!p-0 w-40">
<div className="space-y-0">
{/* <Button variant="menu" onClick={onMove}>
<FiArrowDown className="h-4 w-4" />
Move
</Button>
<Button variant="menu" onClick={onRename}>
<FiEdit className="h-4 w-4" />
Rename
</Button> */}
<Button variant="menu" onClick={handleDeleteClick}>
<FiTrash className="h-4 w-4" />
Delete

View File

@@ -23,21 +23,21 @@ import { Modal } from "@/components/Modal";
import FunctionalHeader from "@/components/chat/Header";
import FixedLogo from "@/components/logo/FixedLogo";
import { useRouter } from "next/navigation";
import Link from "next/link";
function BackToOnyxButton({
documentSidebarVisible,
}: {
documentSidebarVisible: boolean;
}) {
const router = useRouter();
const enterpriseSettings = useContext(SettingsContext)?.enterpriseSettings;
return (
<div className="absolute bottom-0 bg-background w-full flex border-t border-border py-4">
<div className="mx-auto">
<Button onClick={() => router.push("/chat")}>
<Link href="/chat">
Back to {enterpriseSettings?.application_name || "Onyx Chat"}
</Button>
</Link>
</div>
<div
style={{ transition: "width 0.30s ease-out" }}

View File

@@ -290,8 +290,8 @@ const StandardAnswersTable = ({
))}
</div>
</div>
<div className="mx-auto">
<Table className="w-full flex items-stretch">
<div className="flex flex-col w-full mx-auto">
<Table className="w-full">
<TableHeader>
<TableRow>
{columns.map((column) => (
@@ -314,11 +314,13 @@ const StandardAnswersTable = ({
)}
</TableBody>
</Table>
{paginatedStandardAnswers.length === 0 && (
<div className="flex justify-center">
<Text>No matching standard answers found...</Text>
</div>
)}
<div>
{paginatedStandardAnswers.length === 0 && (
<div className="flex justify-center">
<Text>No matching standard answers found...</Text>
</div>
)}
</div>
{paginatedStandardAnswers.length > 0 && (
<>
<div className="mt-4">

View File

@@ -674,3 +674,7 @@ ul > li > p {
.animate-fadeIn {
animation: fadeIn 0.2s ease-out forwards;
}
.container {
margin-bottom: 1rem;
}

View File

@@ -115,17 +115,17 @@ export const ConnectorMultiSelect = ({
<div className="flex flex-col w-full space-y-2 mb-4">
{label && <Label className="text-base font-medium">{label}</Label>}
<p className="text-xs text-neutral-500 ">
<p className="text-xs text-neutral-500 dark:text-neutral-400">
All documents indexed by the selected connectors will be part of this
document set.
</p>
<div className="relative">
<div
className={`flex items-center border border-input rounded-md border border-neutral-200 ${
allConnectorsSelected ? "bg-neutral-50" : ""
} focus-within:ring-1 focus-within:ring-ring focus-within:border-neutral-400 transition-colors`}
className={`flex items-center border border-input rounded-md border-neutral-200 dark:border-neutral-700 ${
allConnectorsSelected ? "bg-neutral-50 dark:bg-neutral-800" : ""
} focus-within:ring-1 focus-within:ring-ring focus-within:border-neutral-400 dark:focus-within:border-neutral-500 transition-colors`}
>
<Search className="absolute left-3 h-4 w-4 text-neutral-500" />
<Search className="absolute left-3 h-4 w-4 text-neutral-500 dark:text-neutral-400" />
<input
ref={inputRef}
type="text"
@@ -141,8 +141,10 @@ export const ConnectorMultiSelect = ({
}}
onKeyDown={handleKeyDown}
placeholder={effectivePlaceholder}
className={`h-9 w-full pl-9 pr-10 py-2 bg-transparent text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50 ${
allConnectorsSelected ? "text-neutral-500" : ""
className={`h-9 w-full pl-9 pr-10 py-2 bg-transparent dark:bg-transparent text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50 ${
allConnectorsSelected
? "text-neutral-500 dark:text-neutral-400"
: ""
}`}
disabled={isInputDisabled}
/>
@@ -151,10 +153,10 @@ export const ConnectorMultiSelect = ({
{open && !allConnectorsSelected && (
<div
ref={dropdownRef}
className="absolute z-50 w-full mt-1 rounded-md border border-neutral-200 bg-white shadow-md default-scrollbar max-h-[300px] overflow-auto"
className="absolute z-50 w-full mt-1 rounded-md border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-md default-scrollbar max-h-[300px] overflow-auto"
>
{filteredUnselectedConnectors.length === 0 ? (
<div className="py-4 text-center text-xs text-neutral-500">
<div className="py-4 text-center text-xs text-neutral-500 dark:text-neutral-400">
{searchQuery
? "No matching connectors found"
: "No more connectors available"}
@@ -164,7 +166,7 @@ export const ConnectorMultiSelect = ({
{filteredUnselectedConnectors.map((connector) => (
<div
key={connector.cc_pair_id}
className="flex items-center justify-between py-2 px-3 cursor-pointer hover:bg-neutral-50 text-xs"
className="flex items-center justify-between py-2 px-3 cursor-pointer hover:bg-neutral-50 dark:hover:bg-neutral-800 text-xs"
onClick={() => selectConnector(connector.cc_pair_id)}
>
<div className="flex items-center truncate mr-2">
@@ -185,12 +187,12 @@ export const ConnectorMultiSelect = ({
</div>
{selectedConnectors.length > 0 ? (
<div className="mt-3 ">
<div className="mt-3">
<div className="flex flex-wrap gap-1.5">
{selectedConnectors.map((connector) => (
<div
key={connector.cc_pair_id}
className="flex items-center bg-white rounded-md border border-neutral-300 transition-all px-2 py-1 max-w-full group text-xs"
className="flex items-center bg-white dark:bg-neutral-800 rounded-md border border-neutral-300 dark:border-neutral-700 transition-all px-2 py-1 max-w-full group text-xs"
>
<div className="flex items-center overflow-hidden">
<div className="flex-shrink-0 text-xs">
@@ -204,7 +206,7 @@ export const ConnectorMultiSelect = ({
</div>
</div>
<button
className="ml-1 flex-shrink-0 rounded-full w-4 h-4 flex items-center justify-center bg-neutral-100 text-neutral-500 hover:bg-neutral-200 hover:text-neutral-700 transition-colors group-hover:bg-neutral-200"
className="ml-1 flex-shrink-0 rounded-full w-4 h-4 flex items-center justify-center bg-neutral-100 dark:bg-neutral-700 text-neutral-500 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-600 hover:text-neutral-700 dark:hover:text-neutral-300 transition-colors group-hover:bg-neutral-200 dark:group-hover:bg-neutral-600"
onClick={() => removeConnector(connector.cc_pair_id)}
aria-label="Remove connector"
>
@@ -215,7 +217,7 @@ export const ConnectorMultiSelect = ({
</div>
</div>
) : (
<div className="mt-3 p-3 border border-dashed border-neutral-300 rounded-md bg-neutral-50 text-neutral-500 text-xs">
<div className="mt-3 p-3 border border-dashed border-neutral-300 dark:border-neutral-700 rounded-md bg-neutral-50 dark:bg-neutral-800 text-neutral-500 dark:text-neutral-400 text-xs">
No connectors selected. Search and select connectors above.
</div>
)}
@@ -224,7 +226,7 @@ export const ConnectorMultiSelect = ({
<ErrorMessage
name={name}
component="div"
className="text-red-500 text-xs mt-1"
className="text-red-500 dark:text-red-400 text-xs mt-1"
/>
)}
</div>

View File

@@ -92,8 +92,9 @@ export function Modal({
${className || ""}
flex
flex-col
${heightOverride ? `h-${heightOverride}` : "max-h-[90vh]"}
${hideOverflow ? "overflow-hidden" : "overflow-auto"}
${hideOverflow ? "overflow-hidden" : "overflow-visible"}
`}
>
{onOutsideClick && !hideCloseButton && (

View File

@@ -69,7 +69,7 @@ const MultiSelectDropdown = ({
};
return (
<div className="flex flex-col space-y-4 mb-4">
<div className="flex flex-col text-white space-y-4 mb-4">
<Label>{label}</Label>
{creatable ? (
<CreatableSelect

View File

@@ -27,9 +27,9 @@ export function TokenDisplay({
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center space-x-3 bg-neutral-100 dark:bg-neutral-800 rounded-full px-4 py-1.5">
<div className="relative w-36 h-2 bg-neutral-200 dark:bg-neutral-700 rounded-full overflow-hidden">
<div className="hidden sm:block relative w-24 h-2 bg-neutral-200 dark:bg-neutral-700 rounded-full overflow-hidden">
<div
className={`absolute top-0 left-0 h-full rounded-full ${
className={` absolute top-0 left-0 h-full rounded-full ${
tokenPercentage >= 100
? "bg-yellow-500 dark:bg-yellow-600"
: "bg-green-500 dark:bg-green-600"

View File

@@ -201,7 +201,6 @@ export function TextFormField({
maxWidth,
removeLabel,
min,
includeForgotPassword,
onChange,
width,
vertical,
@@ -229,7 +228,6 @@ export function TextFormField({
explanationLink?: string;
small?: boolean;
min?: number;
includeForgotPassword?: boolean;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
width?: string;
vertical?: boolean;
@@ -339,14 +337,6 @@ export function TextFormField({
placeholder={placeholder}
autoComplete={autoCompleteDisabled ? "off" : undefined}
/>
{includeForgotPassword && (
<Link
href="/auth/forgot-password"
className="absolute right-3 top-1/2 mt-[3px] transform -translate-y-1/2 text-xs text-blue-500 cursor-pointer"
>
Forgot password?
</Link>
)}
</div>
{explanationText && (

View File

@@ -16,7 +16,6 @@ interface TextViewProps {
presentingDocument: MinimalOnyxDocument;
onClose: () => void;
}
export default function TextView({
presentingDocument,
onClose,
@@ -27,6 +26,13 @@ export default function TextView({
const [fileName, setFileName] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [fileType, setFileType] = useState("application/octet-stream");
const [renderCount, setRenderCount] = useState(0);
// Log render count on each render
useEffect(() => {
setRenderCount((prevCount) => prevCount + 1);
console.log(`TextView component rendered ${renderCount + 1} times`);
}, []);
// Detect if a given MIME type is one of the recognized markdown formats
const isMarkdownFormat = (mimeType: string): boolean => {
@@ -63,6 +69,7 @@ export default function TextView({
};
const fetchFile = useCallback(async () => {
console.log("fetching file");
setIsLoading(true);
const fileId =
presentingDocument.document_id.split("__")[1] ||
@@ -107,13 +114,14 @@ export default function TextView({
// Keep the slight delay for a smoother loading experience
setTimeout(() => {
setIsLoading(false);
console.log("finished loading");
}, 1000);
}
}, [presentingDocument]);
useEffect(() => {
fetchFile();
}, [fetchFile]);
}, []);
const handleDownload = () => {
const link = document.createElement("a");

View File

@@ -45,7 +45,7 @@ export default function CreateEntityModal({
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogContent className="max-w-[95%] sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>

View File

@@ -437,19 +437,20 @@ export function CompactDocumentCard({
url?: string;
updatePresentingDocument: (document: OnyxDocument) => void;
}) {
console.log("document", document);
return (
<div
onClick={() => {
openDocument(document, updatePresentingDocument);
}}
className="max-w-[200px] gap-y-0 cursor-pointer pb-0 pt-0 mt-0 flex gap-y-0 flex-col content-start items-start gap-0 "
className="max-w-[250px] gap-y-1 cursor-pointer pb-0 pt-0 mt-0 flex gap-y-0 flex-col content-start items-start gap-0 "
>
<div className="text-sm !pb-0 !mb-0 font-semibold flex items-center gap-x-1 text-text-900 pt-0 mt-0 truncate w-full">
<div className="text-sm flex gap-x-2 !pb-0 !mb-0 font-semibold flex items-center gap-x-1 text-text-900 pt-0 mt-0 w-full">
{icon}
{(document.semantic_identifier || document.document_id).slice(0, 40)}
{(document.semantic_identifier || document.document_id).length > 40 &&
"..."}
<p className="gap-0 p-0 m-0 line-clamp-2">
{(document.semantic_identifier || document.document_id).slice(0, 40)}
{(document.semantic_identifier || document.document_id).length > 40 &&
"..."}
</p>
</div>
{document.blurb && (
<div className="text-xs mb-0 text-neutral-600 dark:text-neutral-300 line-clamp-2">
@@ -479,7 +480,7 @@ export function CompactQuestionCard({
return (
<div
onClick={() => openQuestion(question)}
className="max-w-[250px] gap-y-0 cursor-pointer pb-0 pt-0 mt-0 flex gap-y-0 flex-col content-start items-start gap-0"
className="max-w-[350px] gap-y-1 cursor-pointer pb-0 pt-0 mt-0 flex gap-y-0 flex-col content-start items-start gap-0"
>
<div className="text-sm !pb-0 !mb-0 font-semibold flex items-center gap-x-1 text-text-900 pt-0 mt-0 truncate w-full">
Question

View File

@@ -9,6 +9,7 @@ import {
} from "@/components/ui/tooltip";
import { openDocument } from "@/lib/search/utils";
import { SubQuestionDetail } from "@/app/chat/interfaces";
import { getFileIconFromFileNameAndLink } from "@/lib/assistantIconUtils";
export interface DocumentCardProps {
document: LoadedOnyxDocument;
@@ -39,6 +40,13 @@ export function Citation({
if (!document_info && !question_info) {
return <>{children}</>;
}
const icon = document_info?.document
? getFileIconFromFileNameAndLink(
document_info.document.semantic_identifier || "",
document_info.document.link || ""
)
: null;
return (
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -72,7 +80,7 @@ export function Citation({
<CompactDocumentCard
updatePresentingDocument={document_info.updatePresentingDocument}
url={document_info.url}
icon={document_info.icon}
icon={icon}
document={document_info.document}
/>
) : (

View File

@@ -112,7 +112,24 @@ export async function fetchChatData(searchParams: {
? `${fullUrl}?${searchParamsString}`
: fullUrl;
if (!NEXT_PUBLIC_ENABLE_CHROME_EXTENSION) {
// Check the referrer to prevent redirect loops
const referrer = headersList.get("referer") || "";
const isComingFromLogin = referrer.includes("/auth/login");
// Also check for the from=login query parameter
const isRedirectedFromLogin = searchParams["from"] === "login";
console.log(
`Auth check: authDisabled=${authDisabled}, user=${!!user}, referrer=${referrer}, fromLogin=${isRedirectedFromLogin}`
);
// Only redirect if we're not already coming from the login page
if (
!NEXT_PUBLIC_ENABLE_CHROME_EXTENSION &&
!isComingFromLogin &&
!isRedirectedFromLogin
) {
console.log("Redirecting to login from chat page");
return {
redirect: `/auth/login?next=${encodeURIComponent(redirectUrl)}`,
};

View File

@@ -77,7 +77,8 @@ export const SERVER_SIDE_ONLY__CLOUD_ENABLED =
process.env.NEXT_PUBLIC_CLOUD_ENABLED?.toLowerCase() === "true";
export const NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED =
process.env.NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED?.toLowerCase() === "true";
process.env.NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED?.toLowerCase() === "true" &&
!NEXT_PUBLIC_CLOUD_ENABLED;
export const NEXT_PUBLIC_TEST_ENV =
process.env.NEXT_PUBLIC_TEST_ENV?.toLowerCase() === "true";

View File

@@ -130,12 +130,10 @@ export async function renameItem(
}
export async function downloadItem(documentId: string): Promise<Blob> {
const response = await fetch(
`/api/chat/file/${encodeURIComponent(documentId)}`,
{
method: "GET",
}
);
const fileId = documentId.split("__")[1] || documentId;
const response = await fetch(`/api/chat/file/${encodeURIComponent(fileId)}`, {
method: "GET",
});
if (!response.ok) {
throw new Error("Failed to fetch file");
}