mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-13 03:32:42 +00:00
Compare commits
2 Commits
bo/custom_
...
v0.24.0-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
285aba3c20 | ||
|
|
a4a399bc31 |
@@ -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
|
||||
|
||||
@@ -29,12 +29,11 @@ function LLMProviderUpdateModal({
|
||||
llmProviderDescriptor?.name ||
|
||||
"Custom LLM Provider";
|
||||
|
||||
const hasAdvancedOptions = llmProviderDescriptor?.name != "azure";
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${llmProviderDescriptor ? "Configure" : "Setup"} ${providerName}`}
|
||||
onOutsideClick={() => onClose()}
|
||||
hideOverflow={true}
|
||||
>
|
||||
<div className="max-h-[70vh] overflow-y-auto px-4">
|
||||
{llmProviderDescriptor ? (
|
||||
@@ -44,7 +43,6 @@ function LLMProviderUpdateModal({
|
||||
existingLlmProvider={existingLlmProvider}
|
||||
shouldMarkAsDefault={shouldMarkAsDefault}
|
||||
setPopup={setPopup}
|
||||
hasAdvancedOptions={hasAdvancedOptions}
|
||||
/>
|
||||
) : (
|
||||
<CustomLLMProviderUpdateForm
|
||||
|
||||
@@ -35,10 +35,12 @@ function LLMProviderUpdateModal({
|
||||
existingLlmProvider?.name ||
|
||||
"Custom LLM Provider";
|
||||
|
||||
const hasAdvancedOptions = llmProviderDescriptor?.name != "azure";
|
||||
|
||||
return (
|
||||
<Modal title={`Setup ${providerName}`} onOutsideClick={() => onClose()}>
|
||||
<Modal
|
||||
title={`Setup ${providerName}`}
|
||||
onOutsideClick={() => onClose()}
|
||||
hideOverflow={true}
|
||||
>
|
||||
<div className="max-h-[70vh] overflow-y-auto px-4">
|
||||
{llmProviderDescriptor ? (
|
||||
<LLMProviderUpdateForm
|
||||
@@ -47,7 +49,6 @@ function LLMProviderUpdateModal({
|
||||
existingLlmProvider={existingLlmProvider}
|
||||
shouldMarkAsDefault={shouldMarkAsDefault}
|
||||
setPopup={setPopup}
|
||||
hasAdvancedOptions={hasAdvancedOptions}
|
||||
/>
|
||||
) : (
|
||||
<CustomLLMProviderUpdateForm
|
||||
|
||||
@@ -29,7 +29,6 @@ export function LLMProviderUpdateForm({
|
||||
setPopup,
|
||||
hideSuccess,
|
||||
firstTimeConfiguration = false,
|
||||
hasAdvancedOptions = false,
|
||||
}: {
|
||||
llmProviderDescriptor: WellKnownLLMProviderDescriptor;
|
||||
onClose: () => void;
|
||||
@@ -40,7 +39,6 @@ export function LLMProviderUpdateForm({
|
||||
|
||||
// Set this when this is the first time the user is setting Onyx up.
|
||||
firstTimeConfiguration?: boolean;
|
||||
hasAdvancedOptions?: boolean;
|
||||
}) {
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
@@ -302,7 +300,7 @@ export function LLMProviderUpdateForm({
|
||||
}
|
||||
})}
|
||||
|
||||
{hasAdvancedOptions && !firstTimeConfiguration && (
|
||||
{!firstTimeConfiguration && (
|
||||
<>
|
||||
<Separator />
|
||||
|
||||
@@ -364,52 +362,49 @@ export function LLMProviderUpdateForm({
|
||||
/>
|
||||
))}
|
||||
|
||||
{hasAdvancedOptions && (
|
||||
<>
|
||||
<Separator />
|
||||
<AdvancedOptionsToggle
|
||||
showAdvancedOptions={showAdvancedOptions}
|
||||
setShowAdvancedOptions={setShowAdvancedOptions}
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<>
|
||||
{llmProviderDescriptor.llm_names.length > 0 && (
|
||||
<div className="w-full">
|
||||
<MultiSelectField
|
||||
selectedInitially={
|
||||
formikProps.values.display_model_names
|
||||
}
|
||||
name="display_model_names"
|
||||
label="Display Models"
|
||||
subtext="Select the models to make available to users. Unselected models will not be available."
|
||||
options={llmProviderDescriptor.llm_names.map(
|
||||
(name) => ({
|
||||
value: name,
|
||||
// don't clean up names here to give admins descriptive names / handle duplicates
|
||||
// like us.anthropic.claude-3-7-sonnet-20250219-v1:0 and anthropic.claude-3-7-sonnet-20250219-v1:0
|
||||
label: name,
|
||||
})
|
||||
)}
|
||||
onChange={(selected) =>
|
||||
formikProps.setFieldValue(
|
||||
"display_model_names",
|
||||
selected
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IsPublicGroupSelector
|
||||
formikProps={formikProps}
|
||||
objectName="LLM Provider"
|
||||
publicToWhom="all users"
|
||||
enforceGroupSelection={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<Separator />
|
||||
<AdvancedOptionsToggle
|
||||
showAdvancedOptions={showAdvancedOptions}
|
||||
setShowAdvancedOptions={setShowAdvancedOptions}
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<>
|
||||
{llmProviderDescriptor.llm_names.length > 0 && (
|
||||
<div className="w-full">
|
||||
<MultiSelectField
|
||||
selectedInitially={
|
||||
formikProps.values.display_model_names
|
||||
}
|
||||
name="display_model_names"
|
||||
label="Display Models"
|
||||
subtext="Select the models to make available to users. Unselected models will not be available."
|
||||
options={llmProviderDescriptor.llm_names.map(
|
||||
(name) => ({
|
||||
value: name,
|
||||
// don't clean up names here to give admins descriptive names / handle duplicates
|
||||
// like us.anthropic.claude-3-7-sonnet-20250219-v1:0 and anthropic.claude-3-7-sonnet-20250219-v1:0
|
||||
label: name,
|
||||
})
|
||||
)}
|
||||
onChange={(selected) =>
|
||||
formikProps.setFieldValue(
|
||||
"display_model_names",
|
||||
selected
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<IsPublicGroupSelector
|
||||
formikProps={formikProps}
|
||||
objectName="LLM Provider"
|
||||
publicToWhom="Users"
|
||||
enforceGroupSelection={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -417,7 +412,11 @@ export function LLMProviderUpdateForm({
|
||||
{testError && <Text className="text-error mt-2">{testError}</Text>}
|
||||
|
||||
<div className="flex w-full mt-4">
|
||||
<Button type="submit" variant="submit">
|
||||
<Button
|
||||
onClick={() => alert(JSON.stringify(formikProps.errors))}
|
||||
type="submit"
|
||||
variant="submit"
|
||||
>
|
||||
{isTesting ? (
|
||||
<LoadingAnimation text="Testing" />
|
||||
) : existingLlmProvider ? (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,6 +18,7 @@ export default async function Layout({
|
||||
);
|
||||
|
||||
if ("redirect" in data) {
|
||||
console.log("redirect", data.redirect);
|
||||
redirect(data.redirect);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
@@ -79,9 +79,21 @@ export default function MyDocuments() {
|
||||
);
|
||||
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);
|
||||
|
||||
|
||||
@@ -599,29 +599,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)}
|
||||
@@ -645,9 +622,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
|
||||
|
||||
@@ -257,7 +257,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>
|
||||
|
||||
@@ -120,7 +120,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();
|
||||
@@ -215,7 +215,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();
|
||||
@@ -411,12 +411,6 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
|
||||
}
|
||||
}, [isOpen, selectedFiles, selectedFolders]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
refreshFolders();
|
||||
}
|
||||
}, [isOpen, refreshFolders]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentFolder) {
|
||||
if (currentFolder === -1) {
|
||||
@@ -1103,7 +1097,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">
|
||||
@@ -1267,16 +1261,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" />
|
||||
@@ -1286,14 +1280,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}
|
||||
@@ -1304,69 +1304,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>
|
||||
|
||||
@@ -33,7 +33,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");
|
||||
@@ -47,89 +48,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",
|
||||
@@ -139,82 +194,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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -101,7 +101,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" : ""
|
||||
}`}
|
||||
>
|
||||
@@ -110,14 +110,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
|
||||
|
||||
@@ -74,7 +74,6 @@ export const IsPublicGroupSelector = <T extends IsPublicGroupSelectorFormType>({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Separator />
|
||||
{isAdmin && (
|
||||
<>
|
||||
<BooleanFormField
|
||||
|
||||
@@ -24,6 +24,7 @@ interface ModalProps {
|
||||
removeBottomPadding?: boolean;
|
||||
removePadding?: boolean;
|
||||
increasedPadding?: boolean;
|
||||
hideOverflow?: boolean;
|
||||
}
|
||||
|
||||
export function Modal({
|
||||
@@ -43,6 +44,7 @@ export function Modal({
|
||||
removeBottomPadding,
|
||||
removePadding,
|
||||
increasedPadding,
|
||||
hideOverflow,
|
||||
}: ModalProps) {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
@@ -92,7 +94,7 @@ export function Modal({
|
||||
flex
|
||||
flex-col
|
||||
${heightOverride ? `h-${heightOverride}` : "max-h-[90vh]"}
|
||||
overflow-auto
|
||||
${hideOverflow ? "overflow-hidden" : "overflow-auto"}
|
||||
`}
|
||||
>
|
||||
{onOutsideClick && !hideCloseButton && (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -174,7 +174,6 @@ export const CustomTooltip = ({
|
||||
: {}
|
||||
}
|
||||
>
|
||||
lll
|
||||
{content}
|
||||
</div>
|
||||
</div>,
|
||||
|
||||
@@ -113,7 +113,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)}`,
|
||||
};
|
||||
|
||||
@@ -131,12 +131,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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user