mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-26 12:15:48 +00:00
Compare commits
4 Commits
experiment
...
fix/projec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c54752e3ba | ||
|
|
bd32795804 | ||
|
|
7aa6b01ac0 | ||
|
|
93084a3a39 |
@@ -23,6 +23,7 @@ import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { useProjectsContext } from "@/app/chat/projects/ProjectsContext";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import FileTypeIcon from "./FileTypeIcon";
|
||||
|
||||
// Small helper to render an icon + label row
|
||||
const Row = ({ children }: { children: React.ReactNode }) => (
|
||||
@@ -82,7 +83,10 @@ export function FilePickerContents({
|
||||
String(f.status) === UserFileStatus.DELETING ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-text-02" />
|
||||
) : (
|
||||
<SvgFileText className="h-4 w-4 stroke-text-02" />
|
||||
<FileTypeIcon
|
||||
fileName={f.name}
|
||||
className="h-4 w-4 stroke-text-02"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 min-w-0">
|
||||
@@ -102,7 +106,7 @@ export function FilePickerContents({
|
||||
internal
|
||||
icon={SvgExternalLink}
|
||||
tooltip="View file"
|
||||
className="opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity duration-150 p-0 bg-transparent hover:bg-transparent"
|
||||
className="opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity duration-150 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -128,7 +132,7 @@ export function FilePickerContents({
|
||||
internal
|
||||
icon={SvgTrash}
|
||||
tooltip="Delete file"
|
||||
className="absolute flex items-center justify-center opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity duration-150 p-0 bg-transparent hover:bg-transparent"
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center justify-center opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity duration-150 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
23
web/src/app/chat/components/files/FileTypeIcon.tsx
Normal file
23
web/src/app/chat/components/files/FileTypeIcon.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import SvgImage from "@/icons/image";
|
||||
import SvgFileText from "@/icons/file-text";
|
||||
import { getFileExtension, isImageExtension } from "./files_utils";
|
||||
|
||||
interface FileTypeIconProps {
|
||||
fileName: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function FileTypeIcon({
|
||||
fileName,
|
||||
className,
|
||||
}: FileTypeIconProps) {
|
||||
const ext = getFileExtension(fileName).toLowerCase();
|
||||
const isImage = isImageExtension(ext);
|
||||
if (isImage) {
|
||||
return <SvgImage className={className} />;
|
||||
}
|
||||
return <SvgFileText className={className} />;
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { Loader2, X } from "lucide-react";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useProjectsContext } from "../../projects/ProjectsContext";
|
||||
import FilePicker from "../files/FilePicker";
|
||||
import type {
|
||||
ProjectFile,
|
||||
CategorizedFiles,
|
||||
} from "../../projects/projectsService";
|
||||
import type { ProjectFile } from "../../projects/projectsService";
|
||||
import { UserFileStatus } from "../../projects/projectsService";
|
||||
import { ChatFileType } from "@/app/chat/interfaces";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { MinimalOnyxDocument } from "@/lib/search/interfaces";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
@@ -26,11 +22,11 @@ import UserFilesModalContent from "@/components/modals/UserFilesModalContent";
|
||||
import { useEscape } from "@/hooks/useKeyPress";
|
||||
import CoreModal from "@/refresh-components/modals/CoreModal";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import SvgFileText from "@/icons/file-text";
|
||||
import SvgFolderOpen from "@/icons/folder-open";
|
||||
import SvgAddLines from "@/icons/add-lines";
|
||||
import SvgFiles from "@/icons/files";
|
||||
import Truncated from "@/refresh-components/texts/Truncated";
|
||||
import FileTypeIcon from "../files/FileTypeIcon";
|
||||
|
||||
export function FileCard({
|
||||
file,
|
||||
@@ -96,7 +92,10 @@ export function FileCard({
|
||||
{isProcessing || file.status === UserFileStatus.UPLOADING ? (
|
||||
<Loader2 className="h-5 w-5 text-text-01 animate-spin" />
|
||||
) : (
|
||||
<SvgFileText className="h-5 w-5 stroke-text-02" />
|
||||
<FileTypeIcon
|
||||
fileName={file.name}
|
||||
className="h-5 w-5 stroke-text-02"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col overflow-hidden">
|
||||
@@ -257,6 +256,17 @@ export default function ProjectContextPanel({
|
||||
recentFiles={allRecentFiles}
|
||||
onFileClick={handleFileClick}
|
||||
onPickRecent={async (file) => {
|
||||
if (
|
||||
file.status === UserFileStatus.UPLOADING ||
|
||||
file.status === UserFileStatus.DELETING
|
||||
) {
|
||||
setPopup({
|
||||
message:
|
||||
"Cannot add file while it is being uploaded or deleted.",
|
||||
type: "warning",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!currentProjectId) return;
|
||||
if (!linkFileToProject) return;
|
||||
linkFileToProject(currentProjectId, file);
|
||||
|
||||
@@ -924,7 +924,11 @@ export function useChatController({
|
||||
return;
|
||||
}
|
||||
updateChatStateAction(getCurrentSessionId(), "uploading");
|
||||
const uploadedMessageFiles = await beginUpload(Array.from(acceptedFiles));
|
||||
const uploadedMessageFiles = await beginUpload(
|
||||
Array.from(acceptedFiles),
|
||||
null,
|
||||
setPopup
|
||||
);
|
||||
setCurrentMessageFiles((prev) => [...prev, ...uploadedMessageFiles]);
|
||||
updateChatStateAction(getCurrentSessionId(), "input");
|
||||
},
|
||||
|
||||
@@ -301,6 +301,36 @@ export const ProjectsProvider: React.FC<ProjectsProviderProps> = ({
|
||||
return tempIdMap;
|
||||
};
|
||||
|
||||
const removeOptimisticFilesByTempIds = useCallback(
|
||||
(optimisticTempIds: Set<string>, projectId?: number | null) => {
|
||||
// Remove from recent optimistic list
|
||||
setAllRecentFiles((prev) =>
|
||||
prev.filter((f) => !f.temp_id || !optimisticTempIds.has(f.temp_id))
|
||||
);
|
||||
|
||||
// Remove from current message files if present
|
||||
setCurrentMessageFiles((prev) =>
|
||||
prev.filter((f) => !f.temp_id || !optimisticTempIds.has(f.temp_id))
|
||||
);
|
||||
|
||||
// Remove from project optimistic list
|
||||
if (projectId) {
|
||||
setAllCurrentProjectFiles((prev) =>
|
||||
prev.filter((f) => !f.temp_id || !optimisticTempIds.has(f.temp_id))
|
||||
);
|
||||
|
||||
// Clear the tracked optimistic files for this project
|
||||
let projectIdToFiles: ProjectFile[] =
|
||||
projectToUploadFilesMapRef.current.get(projectId) || [];
|
||||
projectIdToFiles = projectIdToFiles.filter(
|
||||
(f: ProjectFile) => !f.temp_id || !optimisticTempIds.has(f.temp_id)
|
||||
);
|
||||
projectToUploadFilesMapRef.current.set(projectId, projectIdToFiles);
|
||||
}
|
||||
},
|
||||
[projectToUploadFilesMapRef]
|
||||
);
|
||||
|
||||
const beginUpload = useCallback(
|
||||
async (
|
||||
files: File[],
|
||||
@@ -360,10 +390,12 @@ export const ProjectsProvider: React.FC<ProjectsProviderProps> = ({
|
||||
if (unsupported.length > 0 || nonAccepted.length > 0) {
|
||||
const detailsParts: string[] = [];
|
||||
if (unsupported.length > 0) {
|
||||
detailsParts.push(`Unsupported: ${unsupported.join(", ")}`);
|
||||
detailsParts.push(
|
||||
`Unsupported file types: ${unsupported.join(", ")}`
|
||||
);
|
||||
}
|
||||
if (nonAccepted.length > 0) {
|
||||
detailsParts.push(`Not accepted: ${nonAccepted.join(", ")}`);
|
||||
detailsParts.push(`Files too large: ${nonAccepted.join(", ")}`);
|
||||
}
|
||||
setPopup?.({
|
||||
type: "warning",
|
||||
@@ -383,6 +415,7 @@ export const ProjectsProvider: React.FC<ProjectsProviderProps> = ({
|
||||
.map((f) => f.temp_id as string)
|
||||
)
|
||||
);
|
||||
removeOptimisticFilesByTempIds(new Set(failedTempIds), projectId);
|
||||
if (failedTempIds.length > 0) {
|
||||
onFailure?.(failedTempIds);
|
||||
}
|
||||
@@ -404,26 +437,7 @@ export const ProjectsProvider: React.FC<ProjectsProviderProps> = ({
|
||||
.filter((id): id is string => Boolean(id))
|
||||
);
|
||||
|
||||
// Remove from recent optimistic list
|
||||
setAllRecentFiles((prev) =>
|
||||
prev.filter((f) => !f.temp_id || !optimisticTempIds.has(f.temp_id))
|
||||
);
|
||||
|
||||
// Remove from current message files if present
|
||||
setCurrentMessageFiles((prev) =>
|
||||
prev.filter((f) => !f.temp_id || !optimisticTempIds.has(f.temp_id))
|
||||
);
|
||||
|
||||
// Remove from project optimistic list
|
||||
if (projectId) {
|
||||
setAllCurrentProjectFiles((prev) =>
|
||||
prev.filter(
|
||||
(f) => !f.temp_id || !optimisticTempIds.has(f.temp_id)
|
||||
)
|
||||
);
|
||||
// Clear the tracked optimistic files for this project
|
||||
projectToUploadFilesMapRef.current.delete(projectId);
|
||||
}
|
||||
removeOptimisticFilesByTempIds(optimisticTempIds, projectId);
|
||||
|
||||
setPopup?.({
|
||||
type: "error",
|
||||
@@ -440,7 +454,12 @@ export const ProjectsProvider: React.FC<ProjectsProviderProps> = ({
|
||||
});
|
||||
return optimisticFiles;
|
||||
},
|
||||
[currentProjectId, refreshCurrentProjectDetails, refreshRecentFiles]
|
||||
[
|
||||
currentProjectId,
|
||||
refreshCurrentProjectDetails,
|
||||
refreshRecentFiles,
|
||||
removeOptimisticFilesByTempIds,
|
||||
]
|
||||
);
|
||||
|
||||
const uploadFiles = useCallback(
|
||||
|
||||
@@ -16,6 +16,7 @@ import { SvgProps } from "@/icons";
|
||||
import SvgExternalLink from "@/icons/external-link";
|
||||
import SvgFileText from "@/icons/file-text";
|
||||
import SvgImage from "@/icons/image";
|
||||
import FileTypeIcon from "@/app/chat/components/files/FileTypeIcon";
|
||||
import SvgTrash from "@/icons/trash";
|
||||
import SvgCheck from "@/icons/check";
|
||||
import Truncated from "@/refresh-components/texts/Truncated";
|
||||
@@ -273,15 +274,10 @@ export default function UserFilesModalContent({
|
||||
<SvgCheck className="stroke-text-02" />
|
||||
</div>
|
||||
) : (
|
||||
(() => {
|
||||
const ext = getFileExtension(f.name).toLowerCase();
|
||||
const isImage = isImageExtension(ext);
|
||||
return isImage ? (
|
||||
<SvgImage className="h-5 w-5 stroke-text-02" />
|
||||
) : (
|
||||
<SvgFileText className="h-5 w-5 stroke-text-02" />
|
||||
);
|
||||
})()
|
||||
<FileTypeIcon
|
||||
fileName={f.name}
|
||||
className="h-5 w-5 stroke-text-02"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -289,7 +285,12 @@ export default function UserFilesModalContent({
|
||||
<div className="p-spacing-inline-mini flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className="max-w-[250px] min-w-0 flex-none">
|
||||
<Truncated text04 secondaryAction nowrap>
|
||||
<Truncated
|
||||
text04
|
||||
secondaryAction
|
||||
nowrap
|
||||
className="truncate"
|
||||
>
|
||||
{f.name}
|
||||
</Truncated>
|
||||
</div>
|
||||
@@ -332,20 +333,23 @@ export default function UserFilesModalContent({
|
||||
</Text>
|
||||
)}
|
||||
{!showRemove && <div className="p-spacing-inline"></div>}
|
||||
{showRemove &&
|
||||
String(f.status) !== UserFileStatus.UPLOADING &&
|
||||
String(f.status) !== UserFileStatus.DELETING && (
|
||||
<IconButton
|
||||
internal
|
||||
icon={SvgTrash}
|
||||
tooltip="Remove from project"
|
||||
className="opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity duration-150 p-0 bg-transparent hover:bg-transparent shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove && onRemove(f);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showRemove && (
|
||||
<div className="h-6 w-6">
|
||||
{String(f.status) !== UserFileStatus.UPLOADING &&
|
||||
String(f.status) !== UserFileStatus.DELETING && (
|
||||
<IconButton
|
||||
internal
|
||||
icon={SvgTrash}
|
||||
tooltip="Remove file"
|
||||
className="opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity duration-150 shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove && onRemove(f);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -470,20 +470,23 @@ function AppSidebarInner() {
|
||||
}
|
||||
/>
|
||||
}
|
||||
actionOnHover
|
||||
>
|
||||
{projects.map((project) => (
|
||||
<ProjectFolderButton key={project.id} project={project} />
|
||||
))}
|
||||
|
||||
<SidebarTab
|
||||
leftIcon={SvgFolderPlus}
|
||||
onClick={() =>
|
||||
toggleModal(ModalIds.CreateProjectModal, true)
|
||||
}
|
||||
lowlight
|
||||
>
|
||||
New Project
|
||||
</SidebarTab>
|
||||
{projects.length < 1 && (
|
||||
<SidebarTab
|
||||
leftIcon={SvgFolderPlus}
|
||||
onClick={() =>
|
||||
toggleModal(ModalIds.CreateProjectModal, true)
|
||||
}
|
||||
lowlight
|
||||
>
|
||||
New Project
|
||||
</SidebarTab>
|
||||
)}
|
||||
</SidebarSection>
|
||||
|
||||
{/* Recents */}
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface SidebarSectionProps {
|
||||
children?: React.ReactNode;
|
||||
action?: React.ReactNode;
|
||||
className?: string;
|
||||
actionOnHover?: boolean;
|
||||
}
|
||||
|
||||
export function SidebarSection({
|
||||
@@ -16,14 +17,31 @@ export function SidebarSection({
|
||||
children,
|
||||
action,
|
||||
className,
|
||||
actionOnHover = false,
|
||||
}: SidebarSectionProps) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-spacing-inline", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-spacing-inline",
|
||||
actionOnHover && "group",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="px-spacing-interline py-spacing-inline sticky top-[0rem] bg-background-tint-02 z-10 flex flex-row items-center justify-between">
|
||||
<Text secondaryBody text02>
|
||||
{title}
|
||||
</Text>
|
||||
{action && <div className="flex-shrink-0">{action}</div>}
|
||||
{action && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex-shrink-0 transition-opacity duration-150",
|
||||
actionOnHover &&
|
||||
"opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto"
|
||||
)}
|
||||
>
|
||||
{action}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col">{children}</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user