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
18 changes: 18 additions & 0 deletions apps/web/app/api/web/documents/[documentId]/jobs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextRequest } from "next/server";
import { proxyPrivate } from "@/lib/server-gateway";

type RouteContext = {
params: Promise<{
documentId: string;
}>;
};

export async function GET(request: NextRequest, context: RouteContext) {
const { documentId } = await context.params;

return proxyPrivate({
method: "GET",
path: `/api/documents/${documentId}/jobs`,
request
});
}
34 changes: 33 additions & 1 deletion apps/web/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,22 @@ button {
border-color: var(--ink);
}

.btn-small {
border-radius: 8px;
font-size: 12px;
min-height: 34px;
padding: 7px 10px;
}

.btn-ghost.danger {
color: #9b2c0f;
}

.btn-ghost.danger:hover {
border-color: #ff9b7a;
color: #7c210b;
}

.nav {
background: rgba(243, 241, 231, 0.84);
backdrop-filter: blur(14px);
Expand Down Expand Up @@ -1049,7 +1065,7 @@ button {
border-bottom: 1px solid var(--line);
display: grid;
gap: 12px;
grid-template-columns: 1.4fr 0.8fr 0.8fr 0.8fr;
grid-template-columns: minmax(0, 1.4fr) 0.8fr 0.8fr 0.5fr auto;
padding: 14px 16px;
transition: background 160ms ease;
}
Expand All @@ -1062,6 +1078,22 @@ button {
border-bottom: 0;
}

.doc-row strong,
.doc-row span {
min-width: 0;
}

.doc-row strong {
overflow-wrap: anywhere;
}

.doc-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
}

.status-badge {
border-radius: 999px;
display: inline-flex;
Expand Down
67 changes: 66 additions & 1 deletion apps/web/components/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
import { FormEvent, useEffect, useMemo, useState } from "react";
import { AnimatedNumber } from "@/components/AnimatedNumber";
import { Logo } from "@/components/Chrome";
import { ApiError, AnswerResponse, DocumentSummary, TenantSummary, UserSummary, askQuestion, getMe, listDocuments, logout, queueDocument } from "@/lib/api";
import { ApiError, AnswerResponse, DocumentSummary, TenantSummary, UserSummary, askQuestion, deleteDocument, getMe, listDocumentJobs, listDocuments, logout, queueDocument, retryIngestionJob } from "@/lib/api";

const navItems = ["Chat", "Documents", "Usage", "Settings"];

Expand All @@ -14,6 +14,11 @@ type WorkspaceStatus = {
message: string;
};

type DocumentAction = {
documentId: string;
type: "delete" | "retry";
};

export function Workspace() {
const router = useRouter();
const [user, setUser] = useState<UserSummary | null>(null);
Expand All @@ -24,6 +29,7 @@ export function Workspace() {
const [isLoading, setIsLoading] = useState(true);
const [isUploading, setIsUploading] = useState(false);
const [isAsking, setIsAsking] = useState(false);
const [documentAction, setDocumentAction] = useState<DocumentAction | null>(null);

const usageMetrics = useMemo(
() => [
Expand Down Expand Up @@ -129,6 +135,47 @@ export function Workspace() {
}
}

async function handleDeleteDocument(document: DocumentSummary) {
if (!confirm(`Delete "${document.title}" from this workspace?`)) {
return;
}

setStatus(null);
setDocumentAction({ documentId: document.id, type: "delete" });

try {
await deleteDocument(document.id);
setDocuments((currentDocuments) => currentDocuments.filter((item) => item.id !== document.id));
setStatus({ tone: "success", message: "Document deleted." });
} catch (error) {
setStatus({ tone: "error", message: resolveWorkspaceError(error) });
} finally {
setDocumentAction(null);
}
}

async function handleRetryDocument(document: DocumentSummary) {
setStatus(null);
setDocumentAction({ documentId: document.id, type: "retry" });

try {
const jobs = await listDocumentJobs(document.id);
const failedJob = jobs.find((job) => job.status === "failed");

if (!failedJob) {
throw new Error("No failed ingestion job found for this document.");
}

await retryIngestionJob(document.id, failedJob.id);
setDocuments(await listDocuments());
setStatus({ tone: "success", message: "Ingestion retry queued." });
} catch (error) {
setStatus({ tone: "error", message: resolveWorkspaceError(error) });
} finally {
setDocumentAction(null);
}
}

async function handleLogout() {
await logout();
router.replace("/login");
Expand Down Expand Up @@ -233,6 +280,16 @@ export function Workspace() {
<span>{document.content_type}</span>
<span className={`status-badge ${document.status}`}>{document.status}</span>
<span className="mono">v{document.current_version}</span>
<div className="doc-actions">
{document.status === "failed" ? (
<button className="btn btn-ghost btn-small" disabled={isDocumentActionActive(documentAction, document.id)} onClick={() => void handleRetryDocument(document)} type="button">
{isDocumentAction(documentAction, document.id, "retry") ? "Retrying..." : "Retry"}
</button>
) : null}
<button className="btn btn-ghost btn-small danger" disabled={isDocumentActionActive(documentAction, document.id)} onClick={() => void handleDeleteDocument(document)} type="button">
{isDocumentAction(documentAction, document.id, "delete") ? "Deleting..." : "Delete"}
</button>
</div>
</div>
)) : (
<div className="doc-row">
Expand All @@ -257,6 +314,14 @@ export function Workspace() {
);
}

function isDocumentAction(action: DocumentAction | null, documentId: string, type: DocumentAction["type"]) {
return action?.documentId === documentId && action.type === type;
}

function isDocumentActionActive(action: DocumentAction | null, documentId: string) {
return action?.documentId === documentId;
}

function getFormValue(form: FormData, key: string) {
const value = form.get(key);

Expand Down
20 changes: 20 additions & 0 deletions apps/web/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,20 @@ export type DocumentSummary = {
updated_at: string;
};

export type IngestionJobSummary = {
id: string;
tenant_id: string;
document_id: string;
document_version_id: string;
status: string;
attempts: number;
error_code: string | null;
error_message: string | null;
queued_at: string;
started_at: string | null;
finished_at: string | null;
};

export type CitationResponse = {
document_id: string;
document_title: string;
Expand Down Expand Up @@ -130,6 +144,12 @@ export async function listDocuments() {
});
}

export async function listDocumentJobs(documentId: string) {
return request<IngestionJobSummary[]>(`/api/web/documents/${documentId}/jobs`, {
method: "GET"
});
}

export async function askQuestion(payload: QuestionRequest) {
return request<AnswerResponse>("/api/web/questions", {
method: "POST",
Expand Down
Loading