| "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 { |
| |
| } 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 { |
| |
| } 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> |
| ); |
| } |
|
|