Compare commits

...

4 Commits

Author SHA1 Message Date
SubashMohan
c54752e3ba fix non accepted files popup 2025-10-23 18:19:45 +05:30
SubashMohan
bd32795804 fix file item issues 2025-10-23 18:19:45 +05:30
SubashMohan
7aa6b01ac0 Update Truncated component styling for better text handling 2025-10-23 18:19:45 +05:30
SubashMohan
93084a3a39 fix project bugs 2025-10-23 18:19:45 +05:30
8 changed files with 155 additions and 70 deletions

View File

@@ -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();

View 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} />;
}

View File

@@ -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);

View 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");
},

View File

@@ -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(

View File

@@ -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>
))}

View File

@@ -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 */}

View File

@@ -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>