Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ OPENAI_API_KEY=sk-...
# ===== Database (Local Dev) =====
# Production uses Supabase Postgres
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/enterprise_rag
SERVER_DB_POOL_SIZE=3
SERVER_DB_MAX_OVERFLOW=2
WORKER_DB_POOL_SIZE=1
WORKER_DB_MAX_OVERFLOW=0
DB_POOL_TIMEOUT_SECONDS=30

# ===== Redis (Required for queue + cache) =====
REDIS_URL=redis://localhost:6379/0
Expand Down
7 changes: 7 additions & 0 deletions .github/trivy/Dockerfile
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
FROM aquasec/trivy:0.70.0

RUN adduser -D trivyuser \
&& mkdir -p /home/trivyuser/.cache/trivy \
&& chown -R trivyuser:trivyuser /home/trivyuser

ENV HOME=/home/trivyuser
USER trivyuser
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
- name: Run Trivy FS Scan
run: |
docker run --rm \
--user root \
-v "${{ github.workspace }}:/workspace" \
-w /workspace \
local-trivy fs . \
Expand Down
7 changes: 5 additions & 2 deletions client/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ RUN npm run build

# Production stage
FROM nginx:alpine AS production
COPY --from=build /app/dist /usr/share/nginx/html
COPY --from=build --chown=nginx:nginx /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
RUN touch /var/run/nginx.pid \
&& chown -R nginx:nginx /var/cache/nginx /var/run/nginx.pid /usr/share/nginx/html
USER nginx
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]
2 changes: 1 addition & 1 deletion client/nginx.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
server {
listen 80;
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
Expand Down
6 changes: 4 additions & 2 deletions client/src/components/chat/ChatSessionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ export default function ChatSessionList({
{!loading && items.length === 0 ? <p className="text-sm text-app-muted">No saved chats yet.</p> : null}
<div className="space-y-2">
{items.map((item) => {
const docName = item.document_id
? documents.find((doc) => doc.id === item.document_id)?.filename ?? "Document"
const documentIds = item.document_ids?.length ? item.document_ids : item.document_id ? [item.document_id] : [];
const firstDocName = documentIds[0]
? documents.find((doc) => doc.id === documentIds[0])?.filename ?? "Document"
: "All documents";
const docName = documentIds.length > 1 ? `${firstDocName} + ${documentIds.length - 1} more` : firstDocName;
return (
<button
key={item.id}
Expand Down
53 changes: 50 additions & 3 deletions client/src/components/layout/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,27 @@ export type AppShellContextValue = {
documents: DocumentRecord[];
loading: boolean;
activeDocument: DocumentRecord | null;
selectedDocumentIds: string[];
setSelectedDocumentIds: (documentIds: string[]) => void;
setActiveDocumentId: (documentId: string | null) => void;
setUsageToday: (usage: UsageToday) => void;
refreshWorkspace: () => Promise<void>;
refreshDocuments: () => Promise<void>;
};

const activeDocumentStorageKey = "enterprise-rag:active-document";
const selectedDocumentsStorageKey = "enterprise-rag:selected-documents";

function restoreSelectedDocumentIds(): string[] {
try {
const raw = localStorage.getItem(selectedDocumentsStorageKey);
const parsed = raw ? (JSON.parse(raw) as unknown) : [];
return Array.isArray(parsed) ? parsed.map((item) => String(item)).filter(Boolean) : [];
} catch {
localStorage.removeItem(selectedDocumentsStorageKey);
return [];
}
}

export default function AppShell() {
const { accessToken, user, signOut } = useAuth();
Expand All @@ -39,9 +53,21 @@ export default function AppShell() {
const [documents, setDocuments] = useState<DocumentRecord[]>([]);
const [sidebarQuery, setSidebarQuery] = useState("");
const [activeDocumentId, setActiveDocumentId] = useState<string | null>(() => localStorage.getItem(activeDocumentStorageKey));
const [selectedDocumentIds, setSelectedDocumentIdsState] = useState<string[]>(restoreSelectedDocumentIds);
const [loading, setLoading] = useState(true);
const [refreshingUsage, setRefreshingUsage] = useState(false);

const setSelectedDocumentIds = useCallback((documentIds: string[]) => {
const unique = Array.from(new Set(documentIds.filter(Boolean)));
setSelectedDocumentIdsState(unique);
setActiveDocumentId(unique[0] ?? null);
}, []);

const setActiveDocumentSelection = useCallback((documentId: string | null) => {
setActiveDocumentId(documentId);
setSelectedDocumentIdsState(documentId ? [documentId] : []);
}, []);

const refreshWorkspace = useCallback(async () => {
if (!accessToken) {
return;
Expand Down Expand Up @@ -111,14 +137,22 @@ export default function AppShell() {
return;
}

const stillExists = documents.some((doc) => doc.id === activeDocumentId && (doc.status === "indexed" || doc.status === "ready"));
const readyDocumentIds = new Set(
documents.filter((doc) => doc.status === "indexed" || doc.status === "ready").map((doc) => doc.id),
);
const stillExists = readyDocumentIds.has(activeDocumentId);

if (!stillExists) {
setActiveDocumentId(null);
localStorage.removeItem(activeDocumentStorageKey);
}
setSelectedDocumentIdsState((current) => current.filter((documentId) => readyDocumentIds.has(documentId)));
}, [activeDocumentId, documents]);

useEffect(() => {
localStorage.setItem(selectedDocumentsStorageKey, JSON.stringify(selectedDocumentIds));
}, [selectedDocumentIds]);

const activeDocument = useMemo(
() => documents.find((doc) => doc.id === activeDocumentId) ?? null,
[activeDocumentId, documents],
Expand All @@ -136,7 +170,16 @@ export default function AppShell() {
};

const handleSelectDocument = (documentId: string) => {
setActiveDocumentId(documentId);
setSelectedDocumentIds([documentId]);
navigate("/app/chat");
};

const handleToggleDocument = (documentId: string) => {
setSelectedDocumentIds(
selectedDocumentIds.includes(documentId)
? selectedDocumentIds.filter((selectedId) => selectedId !== documentId)
: [...selectedDocumentIds, documentId],
);
navigate("/app/chat");
};

Expand Down Expand Up @@ -174,7 +217,9 @@ export default function AppShell() {
documents,
loading,
activeDocument,
setActiveDocumentId,
selectedDocumentIds,
setSelectedDocumentIds,
setActiveDocumentId: setActiveDocumentSelection,
setUsageToday,
refreshWorkspace,
refreshDocuments,
Expand All @@ -187,9 +232,11 @@ export default function AppShell() {
workspace={workspace}
documents={documents}
activeDocumentId={activeDocumentId}
selectedDocumentIds={selectedDocumentIds}
query={sidebarQuery}
onQueryChange={setSidebarQuery}
onSelectDocument={handleSelectDocument}
onToggleDocument={handleToggleDocument}
onGoUpload={() => navigate("/app/upload")}
onRetryDocument={(documentId) => {
void handleRetryDocument(documentId);
Expand Down
39 changes: 37 additions & 2 deletions client/src/components/sidebar/DocumentSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FileText, Search, UploadCloud } from "lucide-react";
import { Check, FileText, Search, UploadCloud } from "lucide-react";
import type { ReactNode } from "react";

import type { DocumentRecord, WorkspaceMe } from "../../lib/api";
Expand All @@ -8,9 +8,11 @@ type SidebarProps = {
workspace: WorkspaceMe | null;
documents: DocumentRecord[];
activeDocumentId: string | null;
selectedDocumentIds: string[];
query: string;
onQueryChange: (value: string) => void;
onSelectDocument: (documentId: string) => void;
onToggleDocument: (documentId: string) => void;
onGoUpload: () => void;
onRetryDocument: (documentId: string) => void;
onReindexDocument: (documentId: string) => void;
Expand Down Expand Up @@ -62,9 +64,11 @@ export default function DocumentSidebar({
workspace,
documents,
activeDocumentId,
selectedDocumentIds,
query,
onQueryChange,
onSelectDocument,
onToggleDocument,
onGoUpload,
onRetryDocument,
onReindexDocument,
Expand Down Expand Up @@ -106,7 +110,9 @@ export default function DocumentSidebar({
key={doc.id}
document={doc}
active={activeDocumentId === doc.id}
selected={selectedDocumentIds.includes(doc.id)}
onClick={() => onSelectDocument(doc.id)}
onToggle={() => onToggleDocument(doc.id)}
onReindex={() => onReindexDocument(doc.id)}
/>
))}
Expand Down Expand Up @@ -145,14 +151,18 @@ function Section({ title, count, children }: { title: string; count: number; chi
function DocRow({
document,
active,
selected,
onClick,
onToggle,
disabled,
onRetry,
onReindex,
}: {
document: DocumentRecord;
active: boolean;
selected?: boolean;
onClick?: () => void;
onToggle?: () => void;
disabled?: boolean;
onRetry?: () => void;
onReindex?: () => void;
Expand All @@ -171,7 +181,32 @@ function DocRow({
)}
>
<div className="flex items-start gap-2">
<FileText size={16} className="mt-0.5 flex-none text-app-muted" />
{onToggle ? (
<span
role="checkbox"
aria-checked={selected}
tabIndex={0}
className={cn(
"mt-0.5 flex h-4 w-4 flex-none items-center justify-center rounded border text-[10px] font-semibold",
selected ? "border-app-accent bg-app-accent text-white" : "border-app-border bg-white text-transparent",
)}
onClick={(event) => {
event.stopPropagation();
onToggle();
}}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
event.stopPropagation();
onToggle();
}
}}
>
<Check size={11} />
</span>
) : (
<FileText size={16} className="mt-0.5 flex-none text-app-muted" />
)}
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-app-text">{document.filename}</p>
<p className="mt-1 inline-flex items-center gap-1.5 text-xs text-app-muted">
Expand Down
7 changes: 5 additions & 2 deletions client/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ export type ChatSessionMetadata = {
id: string;
title: string;
document_id: string | null;
document_ids: string[];
created_at: string;
updated_at: string;
ended_at: string | null;
Expand All @@ -274,6 +275,7 @@ export type ChatSessionListItem = {
id: string;
title: string;
document_id: string | null;
document_ids: string[];
updated_at: string;
ended_at: string | null;
};
Expand All @@ -287,6 +289,7 @@ export type ChatSessionDetail = {
id: string;
title: string;
document_id: string | null;
document_ids: string[];
messages: ChatSessionMessage[];
started_at: string;
ended_at: string | null;
Expand Down Expand Up @@ -878,7 +881,7 @@ export function apiGetObservability(token: string): Promise<ObservabilityRespons

export function apiCreateChatSession(
token: string,
payload: { document_id?: string | null; title?: string; messages: ChatSessionMessage[] },
payload: { document_id?: string | null; document_ids?: string[]; title?: string; messages: ChatSessionMessage[] },
): Promise<ChatSessionMetadata> {
return apiRequest<ChatSessionMetadata>("/chats/sessions", token, {
method: "POST",
Expand All @@ -889,7 +892,7 @@ export function apiCreateChatSession(
export function apiUpdateChatSession(
token: string,
sessionId: string,
payload: { title?: string; messages?: ChatSessionMessage[]; ended?: boolean },
payload: { document_id?: string | null; document_ids?: string[]; title?: string; messages?: ChatSessionMessage[]; ended?: boolean },
): Promise<ChatSessionMetadata> {
return apiRequest<ChatSessionMetadata>(`/chats/sessions/${sessionId}`, token, {
method: "PATCH",
Expand Down
Loading
Loading