engresearch's picture
Upload folder using huggingface_hub
7f88bdf verified
"use client";
import { useState } from "react";
type DraftSection = {
section: string;
content: string;
generatedAt: string;
};
function humanize(section: string) {
return section
.toLowerCase()
.split("_")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ");
}
export default function DraftViewer({
tenderId,
drafts,
onRefresh,
}: {
tenderId: string;
drafts: DraftSection[];
onRefresh: () => Promise<number>;
}) {
const [generating, setGenerating] = useState(false);
const [exporting, setExporting] = useState(false);
const [exportUrl, setExportUrl] = useState<string | null>(null);
const [expanded, setExpanded] = useState<string | null>(drafts[0]?.section ?? null);
async function generateDrafts() {
setGenerating(true);
try {
const res = await fetch(`/api/tenders/${tenderId}/drafts/batch`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ tone: "FORMAL" }),
});
const json = await res.json();
if (!res.ok || !json.ok) {
throw new Error(json.error?.message || "Draft generation failed");
}
const isQueued = res.status === 202 || Boolean(json.data?.queued);
const maxPollAttempts = isQueued ? 30 : 3;
for (let attempt = 0; attempt < maxPollAttempts; attempt += 1) {
const count = await onRefresh();
if (count > 0 && (!isQueued || attempt > 0)) {
break;
}
if (attempt < maxPollAttempts - 1) {
await new Promise((resolve) => setTimeout(resolve, 1500));
}
}
} catch {
// Error handled silently for now
} finally {
setGenerating(false);
}
}
async function exportDocx() {
setExporting(true);
try {
const res = await fetch(`/api/tenders/${tenderId}/drafts/export`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
});
const json = await res.json();
if (res.ok && json.ok && json.data?.downloadUrl) {
setExportUrl(json.data.downloadUrl);
}
} catch {
// Error handled silently
} finally {
setExporting(false);
}
}
if (drafts.length === 0) {
return (
<div className="empty-state">
<div className="empty-state-icon">✍️</div>
<h3>No drafts yet</h3>
<p>Generate AI-drafted proposal sections based on your compliance analysis.</p>
<button
className="btn btn-primary"
style={{ marginTop: "1rem" }}
onClick={generateDrafts}
disabled={generating}
>
{generating ? "Generating..." : "Generate Proposals"}
</button>
</div>
);
}
return (
<div>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "1rem", flexWrap: "wrap", gap: "0.5rem" }}>
<div>
<span className="badge badge-success">{drafts.length} sections</span>
</div>
<div style={{ display: "flex", gap: "0.5rem" }}>
<button
className="btn btn-outline btn-sm"
onClick={generateDrafts}
disabled={generating}
>
{generating ? "Generating..." : "↻ Regenerate"}
</button>
<button
className="btn btn-primary btn-sm"
onClick={exportDocx}
disabled={exporting}
>
{exporting ? "Exporting..." : "📥 Export DOCX"}
</button>
</div>
</div>
{exportUrl && (
<div
style={{
padding: "0.75rem 1rem",
background: "var(--success-subtle)",
borderRadius: "var(--radius-md)",
marginBottom: "1rem",
}}
>
<a href={exportUrl} target="_blank" rel="noopener noreferrer" style={{ fontWeight: 600 }}>
📥 Download DOCX
</a>
</div>
)}
<div className="draft-list">
{drafts.map((draft) => (
<div key={draft.section} className="draft-section-card">
<div
className="draft-section-header"
onClick={() => setExpanded(expanded === draft.section ? null : draft.section)}
>
<span>{humanize(draft.section)}</span>
<span style={{ color: "var(--ink-muted)", fontSize: "0.85rem" }}>
{expanded === draft.section ? "▼" : "▶"}
</span>
</div>
{expanded === draft.section && (
<div className="draft-section-content">{draft.content}</div>
)}
</div>
))}
</div>
</div>
);
}