Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
reviews:
auto_review:
enabled: true
labels:
- "ready-for-review"
base_branches:
- ".*" # allows all branches
11 changes: 11 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,17 @@ import { useApp } from '@/app/lib/context/AppContext';
const { sidebarCollapsed, setSidebarCollapsed, toggleSidebar } = useApp();
```

## Code Quality Guidelines

Follow these rules when writing or modifying code in this repository:

- **File size limit**: Do not let any file exceed **500 LOC**. If a file is approaching or has crossed this limit, split it into smaller modules (extract sub-components, hooks, utilities, or types into their own files).
- **Single Responsibility Principle (SRP)**: Each component, hook, function, or module should do one thing and have one reason to change. If a component handles data fetching, business logic, and UI rendering all together, split it — extract data fetching into a hook, business logic into a utility, and keep the component focused on presentation.
- **Don't Repeat Yourself (DRY)**: Before writing new logic, search the codebase for existing implementations. Reuse and extend rather than duplicate. If you spot the same pattern emerging in 2+ places, extract it into a shared helper, hook, or component in `app/lib/` or `app/components/`.
- **Reuse existing components and icons**: Always check `app/components/` and `app/components/icons/` before creating a new component or icon. Prefer composing or extending existing primitives over authoring new ones. New icons go in `app/components/icons/` as hand-authored React components — do not inline SVGs in feature code.
- **Reuse existing utilities and hooks**: Check `app/lib/utils/`, `app/lib/utils.ts`, and `app/hooks/` before adding new helpers. Domain-specific utilities belong under `app/lib/utils/<domain>/`.
- **Reuse existing types**: Shared types live in `app/lib/types/` and `app/lib/models.ts` — import from there instead of redefining shapes locally.

## API Client & Error Handling

The BFF layer uses [apiClient.ts](app/lib/apiClient.ts) which forwards requests from Next.js route handlers to the backend at `BACKEND_URL` (defaults to `http://localhost:8000`). Key patterns:
Expand Down
5 changes: 1 addition & 4 deletions app/(main)/configurations/prompt-editor/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import { isGpt5Model } from "@/app/lib/models";
import { DEFAULT_CONFIG } from "@/app/lib/constants";

function PromptEditorContent() {

Check warning on line 35 in app/(main)/configurations/prompt-editor/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-build

Function 'PromptEditorContent' has a complexity of 13. Maximum allowed is 10

Check warning on line 35 in app/(main)/configurations/prompt-editor/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-build

Function 'PromptEditorContent' has too many statements (42). Maximum allowed is 20
const toast = useToast();
const searchParams = useSearchParams();
const { sidebarCollapsed } = useApp();
Expand Down Expand Up @@ -253,7 +253,7 @@
savedConfigs,
]);

const handleSaveConfig = async () => {

Check warning on line 256 in app/(main)/configurations/prompt-editor/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-build

Async arrow function has a complexity of 23. Maximum allowed is 10

Check warning on line 256 in app/(main)/configurations/prompt-editor/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-build

Async arrow function has too many statements (37). Maximum allowed is 20
if (!currentConfigName.trim()) {
toast.error("Please enter a configuration name");
return;
Expand Down Expand Up @@ -405,10 +405,7 @@
style={{ backgroundColor: colors.bg.secondary }}
>
<div className="flex flex-col items-center gap-3">
<div className="animate-spin rounded-full border-4 border-solid w-9 h-9 border-bg-primary border-t-accent-primary" />
<p className="text-sm text-text-secondary">
Loading configuration...
</p>
<Loader size="sm" message="Loading configuration..." />
</div>
</div>
) : (
Expand Down
174 changes: 129 additions & 45 deletions app/(main)/document/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ import {
import { DocumentListing } from "@/app/components/document/DocumentListing";
import { DocumentPreview } from "@/app/components/document/DocumentPreview";
import { UploadDocumentModal } from "@/app/components/document/UploadDocumentModal";
import DeleteDocumentModal from "@/app/components/document/DeleteDocumentModal";
import Modal from "@/app/components/Modal";
import {
DEFAULT_PAGE_LIMIT,
MAX_DOCUMENT_SIZE_BYTES,
MAX_DOCUMENT_SIZE_MB,
MAX_DOCUMENT_UPLOAD_BATCH,
} from "@/app/lib/constants";
import { Document } from "@/app/lib/types/document";

Expand All @@ -30,10 +33,14 @@ export default function DocumentPage() {
null,
);
const [isLoadingDocument, setIsLoadingDocument] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadPhase, setUploadPhase] = useState<UploadPhase>("uploading");
const [currentUploadIndex, setCurrentUploadIndex] = useState(0);
const [documentToDelete, setDocumentToDelete] = useState<Document | null>(
null,
);
const abortUploadRef = useRef<(() => void) | null>(null);
const { activeKey: apiKey, isAuthenticated } = useAuth();

Expand All @@ -57,78 +64,126 @@ export default function DocumentPage() {
});

const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const files = Array.from(event.target.files ?? []);
event.target.value = "";
if (files.length === 0) return;

const remaining = MAX_DOCUMENT_UPLOAD_BATCH - selectedFiles.length;
if (remaining <= 0) return;

const accepted: File[] = [];
let oversizedCount = 0;
for (const file of files.slice(0, remaining)) {
if (file.size > MAX_DOCUMENT_SIZE_BYTES) {
oversizedCount += 1;
continue;
}
accepted.push(file);
}

if (file.size > MAX_DOCUMENT_SIZE_BYTES) {
if (oversizedCount > 0) {
toast.error(
`File size exceeds ${MAX_DOCUMENT_SIZE_MB} MB limit. Please select a smaller file within ${MAX_DOCUMENT_SIZE_MB} MB.`,
`${oversizedCount} file${oversizedCount > 1 ? "s" : ""} exceed the ${MAX_DOCUMENT_SIZE_MB} MB limit and were skipped.`,
);
}
if (files.length > remaining) {
toast.warning(
`You can upload up to ${MAX_DOCUMENT_UPLOAD_BATCH} documents at a time.`,
);
event.target.value = "";
return;
}

setSelectedFile(file);
if (accepted.length > 0) {
setSelectedFiles((prev) => [...prev, ...accepted]);
}
};

const handleUpload = async () => {
if (!isAuthenticated || !selectedFile) return;
const handleRemoveSelectedFile = (index: number) => {
setSelectedFiles((prev) => prev.filter((_, i) => i !== index));
};

setIsUploading(true);
const uploadOneFile = async (file: File): Promise<boolean> => {
setUploadProgress(0);
setUploadPhase("uploading");

try {
const formData = new FormData();
formData.append("src", selectedFile);
const formData = new FormData();
formData.append("src", file);

const { promise, abort } = uploadWithProgress<{ data?: { id: string } }>(
"/api/document",
apiKey?.key ?? "",
formData,
(percent, phase) => {
setUploadProgress(percent);
setUploadPhase(phase);
},
);
abortUploadRef.current = abort;
const { promise, abort } = uploadWithProgress<{ data?: { id: string } }>(
"/api/document",
apiKey?.key ?? "",
formData,
(percent, phase) => {
setUploadProgress(percent);
setUploadPhase(phase);
},
);
abortUploadRef.current = abort;

try {
const data = await promise;
if (selectedFile && data.data?.id) {
if (data.data?.id) {
const fileSizeMap = JSON.parse(
localStorage.getItem("document_file_sizes") || "{}",
);
fileSizeMap[data.data.id] = selectedFile.size;
fileSizeMap[data.data.id] = file.size;
localStorage.setItem(
"document_file_sizes",
JSON.stringify(fileSizeMap),
);
Comment thread
Ayush8923 marked this conversation as resolved.
}

refetch();
setSelectedFile(null);
setIsModalOpen(false);

toast.success("Document uploaded successfully!");
return true;
} catch (error) {
console.error("Upload error:", error);
toast.error(
`Failed to upload document: ${error instanceof Error ? error.message : "Unknown error"}`,
`Failed to upload "${file.name}": ${error instanceof Error ? error.message : "Unknown error"}`,
);
return false;
} finally {
setIsUploading(false);
abortUploadRef.current = null;
}
};

const handleDeleteDocument = async (documentId: string) => {
const handleUpload = async () => {
if (!isAuthenticated || selectedFiles.length === 0) return;

setIsUploading(true);
setCurrentUploadIndex(0);

let successCount = 0;
for (let i = 0; i < selectedFiles.length; i += 1) {
setCurrentUploadIndex(i);
const ok = await uploadOneFile(selectedFiles[i]);
if (ok) successCount += 1;
}

refetch();
setSelectedFiles([]);
setIsModalOpen(false);
setIsUploading(false);
setCurrentUploadIndex(0);

if (successCount > 0) {
toast.success(
successCount === 1
? "Document uploaded successfully!"
: `${successCount} documents uploaded successfully!`,
);
}
};

const handleRequestDelete = (documentId: string) => {
if (!isAuthenticated) {
toast.error("Please log in to continue");
return;
}
const doc = documents.find((d) => d.id === documentId);
if (doc) setDocumentToDelete(doc);
};

if (!confirm("Are you sure you want to delete this document?")) {
return;
}
const handleConfirmDelete = async () => {
if (!documentToDelete) return;
const documentId = documentToDelete.id;
setDocumentToDelete(null);

try {
await apiFetch(`/api/document/${documentId}`, apiKey?.key ?? "", {
Expand Down Expand Up @@ -180,7 +235,7 @@ export default function DocumentPage() {
};

return (
<div className="w-full h-screen flex flex-col bg-bg-secondary">
<div className="w-full h-screen flex flex-col bg-bg-primary">
<div className="flex flex-1 overflow-hidden">
<Sidebar collapsed={sidebarCollapsed} activeRoute="/document" />

Expand All @@ -190,13 +245,13 @@ export default function DocumentPage() {
subtitle="Manage your uploaded documents"
/>

<div className="flex-1 overflow-hidden flex bg-bg-secondary">
<div className="w-1/3 border-r border-r-status-default-border overflow-hidden">
<div className="flex-1 overflow-hidden flex bg-bg-primary">
<div className="w-full lg:w-1/3 lg:border-r border-border overflow-hidden">
<DocumentListing
documents={documents}
selectedDocument={selectedDocument}
onSelect={handleSelectDocument}
onDelete={handleDeleteDocument}
onDelete={handleRequestDelete}
onUploadNew={() => setIsModalOpen(true)}
isLoading={isLoading}
isLoadingMore={isLoadingMore}
Expand All @@ -205,7 +260,7 @@ export default function DocumentPage() {
/>
</div>

<div className="flex-1 overflow-y-auto">
<div className="hidden lg:block flex-1 overflow-y-auto">
<DocumentPreview
document={selectedDocument}
isLoading={isLoadingDocument}
Expand All @@ -215,22 +270,51 @@ export default function DocumentPage() {
</div>
</div>

{/* Mobile/tablet — preview rendered in a modal */}
<div className="lg:hidden">
<Modal
open={!!selectedDocument || isLoadingDocument}
onClose={() => setSelectedDocument(null)}
title={selectedDocument?.fname ?? "Document Preview"}
maxWidth="max-w-3xl"
maxHeight="max-h-[90vh]"
>
<div className="h-[80vh]">
<DocumentPreview
document={selectedDocument}
isLoading={isLoadingDocument}
/>
</div>
</Modal>
</div>

<UploadDocumentModal
open={isModalOpen}
selectedFile={selectedFile}
selectedFiles={selectedFiles}
isUploading={isUploading}
uploadProgress={uploadProgress}
uploadPhase={uploadPhase}
currentUploadIndex={currentUploadIndex}
onFileSelect={handleFileSelect}
onRemoveFile={handleRemoveSelectedFile}
onClearFiles={() => setSelectedFiles([])}
onUpload={handleUpload}
onClose={() => {
abortUploadRef.current?.();
setIsModalOpen(false);
setSelectedFile(null);
setSelectedFiles([]);
setUploadProgress(0);
setUploadPhase("uploading");
setCurrentUploadIndex(0);
}}
/>

<DeleteDocumentModal
open={!!documentToDelete}
fileName={documentToDelete?.fname}
onClose={() => setDocumentToDelete(null)}
onConfirm={handleConfirmDelete}
/>
</div>
);
}
4 changes: 2 additions & 2 deletions app/(main)/evaluations/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ export default function EvaluationReport() {
job.status.toLowerCase() !== "failed";

const segmentedClass =
"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all cursor-pointer border border-transparent text-text-primary hover:bg-black/4 hover:shadow-[0_0_0_1px_rgba(0,0,0,0.06)] data-[selected=true]:bg-bg-primary data-[selected=true]:border-border data-[selected=true]:shadow-[0_1px_2px_rgba(0,0,0,0.08)] data-[selected=true]:hover:bg-bg-primary data-[selected=true]:hover:shadow-[0_1px_2px_rgba(0,0,0,0.08)]";
"inline-flex items-center gap-1.5 px-4 py-1.5 text-xs font-semibold rounded-full transition-colors cursor-pointer text-accent-primary/70 hover:text-accent-primary data-[selected=true]:bg-accent-primary data-[selected=true]:text-white data-[selected=true]:shadow-[0_1px_2px_rgba(0,0,0,0.12)] data-[selected=true]:hover:bg-accent-hover";

return (
<div className="w-full h-screen flex flex-col bg-bg-secondary">
Expand Down Expand Up @@ -292,7 +292,7 @@ export default function EvaluationReport() {
</div>

<div className="flex flex-wrap items-center gap-2 sm:gap-3 shrink-0 relative z-10">
<div className="inline-flex rounded-lg p-0.5 bg-bg-secondary">
<div className="inline-flex rounded-full p-1 bg-accent-primary/10">
<button
type="button"
onClick={() => setExportFormat("row")}
Expand Down
Loading
Loading