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
114 changes: 114 additions & 0 deletions apps/web/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,11 @@ button {
background: #f5f9ef;
}

.doc-row.selected {
background: #eff6ec;
box-shadow: inset 3px 0 0 var(--accent);
}

.doc-row:last-child {
border-bottom: 0;
}
Expand All @@ -1094,6 +1099,81 @@ button {
justify-content: flex-end;
}

.document-detail {
background: #f7f8f0;
border: 1px solid var(--line);
border-radius: 14px;
margin-top: 16px;
padding: 18px;
}

.document-detail-head {
align-items: start;
display: flex;
gap: 16px;
justify-content: space-between;
}

.document-detail-head h3 {
font-size: 20px;
margin: 0;
overflow-wrap: anywhere;
}

.job-list {
display: grid;
gap: 10px;
margin-top: 16px;
}

.job-row {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 10px;
display: grid;
gap: 10px;
grid-template-columns: minmax(0, 1.5fr) auto auto;
padding: 12px;
}

.job-row div {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 8px;
min-width: 0;
}

.job-row strong {
color: var(--muted);
font-size: 11px;
min-width: 0;
overflow-wrap: anywhere;
}

.job-row span {
color: var(--muted);
font-size: 12px;
}

.job-row p {
color: #9b2c0f;
font-size: 12px;
grid-column: 1 / -1;
margin: 0;
overflow-wrap: anywhere;
}

.job-empty {
background: var(--surface);
border: 1px dashed var(--line);
border-radius: 10px;
color: var(--muted);
font-size: 13px;
margin-top: 16px;
padding: 14px;
}

.status-badge {
border-radius: 999px;
display: inline-flex;
Expand All @@ -1114,11 +1194,37 @@ button {
color: #8a5b00;
}

.status-badge.uploaded {
background: #f0eee4;
color: #5a655e;
}

.status-badge.failed {
background: #ffe1d8;
color: #b43218;
}

.status-badge.queued {
background: #eaf0ff;
color: #3159bb;
}

.status-badge.running {
background: #fff2ce;
color: #8a5b00;
}

.status-badge.completed {
background: #e0f7e7;
color: #0b7d52;
}

.status-badge.cancelled,
.status-badge.deleted {
background: #f0eee4;
color: #5a655e;
}

.cursor-layer {
inset: 0;
pointer-events: none;
Expand Down Expand Up @@ -1270,6 +1376,14 @@ button {
grid-template-columns: 1fr;
}

.doc-actions {
justify-content: flex-start;
}

.job-row {
grid-template-columns: 1fr;
}

.feature-card.wide {
grid-column: span 1;
}
Expand Down
82 changes: 79 additions & 3 deletions 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, deleteDocument, getMe, listDocumentJobs, listDocuments, logout, queueDocument, retryIngestionJob } from "@/lib/api";
import { ApiError, AnswerResponse, DocumentSummary, IngestionJobSummary, TenantSummary, UserSummary, askQuestion, deleteDocument, getMe, listDocumentJobs, listDocuments, logout, queueDocument, retryIngestionJob } from "@/lib/api";

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

Expand All @@ -30,6 +30,15 @@ export function Workspace() {
const [isUploading, setIsUploading] = useState(false);
const [isAsking, setIsAsking] = useState(false);
const [documentAction, setDocumentAction] = useState<DocumentAction | null>(null);
const [selectedDocumentId, setSelectedDocumentId] = useState<string | null>(null);
const [selectedDocumentJobs, setSelectedDocumentJobs] = useState<IngestionJobSummary[]>([]);
const [isLoadingJobs, setIsLoadingJobs] = useState(false);
const [jobHistoryError, setJobHistoryError] = useState<string | null>(null);

const selectedDocument = useMemo(
() => documents.find((document) => document.id === selectedDocumentId) ?? null,
[documents, selectedDocumentId]
);

const usageMetrics = useMemo(
() => [
Expand Down Expand Up @@ -102,7 +111,8 @@ export function Workspace() {
content
}
);
setDocuments(await listDocuments());
const nextDocuments = await listDocuments();
setDocuments(nextDocuments);
setStatus({ tone: "success", message: "Document queued for indexing." });
event.currentTarget.reset();
} catch (error) {
Expand Down Expand Up @@ -146,6 +156,11 @@ export function Workspace() {
try {
await deleteDocument(document.id);
setDocuments((currentDocuments) => currentDocuments.filter((item) => item.id !== document.id));
if (selectedDocumentId === document.id) {
setSelectedDocumentId(null);
setSelectedDocumentJobs([]);
setJobHistoryError(null);
}
setStatus({ tone: "success", message: "Document deleted." });
} catch (error) {
setStatus({ tone: "error", message: resolveWorkspaceError(error) });
Expand All @@ -168,6 +183,9 @@ export function Workspace() {

await retryIngestionJob(document.id, failedJob.id);
setDocuments(await listDocuments());
if (selectedDocumentId === document.id) {
setSelectedDocumentJobs(await listDocumentJobs(document.id));
}
setStatus({ tone: "success", message: "Ingestion retry queued." });
} catch (error) {
setStatus({ tone: "error", message: resolveWorkspaceError(error) });
Expand All @@ -176,6 +194,21 @@ export function Workspace() {
}
}

async function handleSelectDocument(document: DocumentSummary) {
setSelectedDocumentId(document.id);
setSelectedDocumentJobs([]);
setJobHistoryError(null);
setIsLoadingJobs(true);

try {
setSelectedDocumentJobs(await listDocumentJobs(document.id));
} catch (error) {
setJobHistoryError(resolveWorkspaceError(error));
} finally {
setIsLoadingJobs(false);
}
}

async function handleLogout() {
await logout();
router.replace("/login");
Expand Down Expand Up @@ -275,12 +308,15 @@ export function Workspace() {
<p className="eyebrow">Documents</p>
<div className="doc-table">
{documents.length > 0 ? documents.map((document) => (
<div className="doc-row" key={document.id}>
<div className={`doc-row ${selectedDocumentId === document.id ? "selected" : ""}`} key={document.id}>
<strong>{document.title}</strong>
<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">
<button className="btn btn-ghost btn-small" disabled={isLoadingJobs} onClick={() => void handleSelectDocument(document)} type="button">
{isLoadingJobs && selectedDocumentId === document.id ? "Loading..." : "Details"}
</button>
{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"}
Expand All @@ -298,6 +334,39 @@ export function Workspace() {
</div>
)}
</div>
{selectedDocument ? (
<div className="document-detail">
<div className="document-detail-head">
<div>
<p className="eyebrow">Job history</p>
<h3>{selectedDocument.title}</h3>
</div>
<span className={`status-badge ${selectedDocument.status}`}>{selectedDocument.status}</span>
</div>
{jobHistoryError ? (
<div className="auth-message error" role="alert">{jobHistoryError}</div>
) : null}
{isLoadingJobs ? (
<div className="job-empty">Loading ingestion jobs...</div>
) : selectedDocumentJobs.length > 0 ? (
<div className="job-list">
{selectedDocumentJobs.map((job) => (
<div className="job-row" key={job.id}>
<div>
<span className={`status-badge ${job.status}`}>{job.status}</span>
<strong className="mono">{job.id}</strong>
</div>
<span>attempts {job.attempts}</span>
<span>{formatDate(job.finished_at ?? job.started_at ?? job.queued_at)}</span>
{job.error_message ? <p>{job.error_code ? `${job.error_code}: ` : ""}{job.error_message}</p> : null}
</div>
))}
</div>
) : (
<div className="job-empty">No ingestion jobs found for this document.</div>
)}
</div>
) : null}
</section>
<section className="stats-grid" id="usage">
{usageMetrics.map((metric) => (
Expand Down Expand Up @@ -336,6 +405,13 @@ function slugify(value: string) {
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "document";
}

function formatDate(value: string) {
return new Intl.DateTimeFormat("en", {
dateStyle: "medium",
timeStyle: "short"
}).format(new Date(value));
}

function resolveWorkspaceError(error: unknown) {
if (error instanceof ApiError) {
return error.message;
Expand Down
Loading