diff --git a/frontend/index.html b/frontend/index.html index 7f9c8ce..481ae84 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1128,6 +1128,158 @@ margin: 16px 0; } + .thesis-layout { + display: grid; + grid-template-columns: minmax(300px, 1fr) minmax(360px, 1.2fr); + gap: 20px; + align-items: start; + } + + @media (max-width: 900px) { + .thesis-layout { + grid-template-columns: 1fr; + } + } + + .thesis-col { + min-width: 0; + } + + .thesis-line-list { + display: flex; + flex-direction: column; + gap: 8px; + } + + .thesis-line-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px; + background-color: #0d1622; + border: 1px solid #263548; + border-radius: 8px; + } + + .thesis-version-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + width: 100%; + text-align: left; + padding: 12px; + background-color: #0d1622; + border: 1px solid #263548; + border-radius: 8px; + color: var(--text-primary); + font-weight: 400; + } + + .thesis-version-row:hover { + background-color: #152233; + border-color: #35506a; + transform: none; + box-shadow: none; + } + + .thesis-version-row.active { + border-color: #3b82c4; + background-color: #152233; + box-shadow: inset 0 0 0 1px #3b82c455; + } + + .thesis-line-name { + font-size: 14px; + font-weight: 500; + color: #e5edf5; + } + + .thesis-line-meta { + font-size: 12px; + color: #8ea2b7; + margin-top: 3px; + } + + .approval-pill { + flex-shrink: 0; + font-family: 'IBM Plex Mono', monospace; + font-size: 12px; + font-weight: 600; + color: #93c5fd; + background-color: #3b82c422; + border-radius: 999px; + padding: 4px 10px; + } + + .approval-meter { + display: flex; + align-items: baseline; + gap: 6px; + } + + .approval-meter-count { + font-family: 'IBM Plex Mono', monospace; + font-size: 18px; + font-weight: 700; + color: #3b82c4; + } + + .approval-meter-label { + font-size: 12px; + color: #8ea2b7; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .thesis-block { + margin-bottom: 18px; + } + + .thesis-block-label { + font-size: 11px; + font-weight: 600; + color: #8ea2b7; + text-transform: uppercase; + letter-spacing: 0.08em; + margin-bottom: 8px; + } + + .thesis-throughline { + font-size: 15px; + line-height: 1.6; + color: #e5edf5; + } + + .thesis-list { + margin: 0; + padding-left: 20px; + display: flex; + flex-direction: column; + gap: 8px; + color: #c7d3e0; + line-height: 1.5; + font-size: 13px; + } + + .thesis-list-title { + color: #e5edf5; + font-weight: 500; + } + + .thesis-list-detail { + margin-top: 3px; + font-size: 12px; + color: #8ea2b7; + } + + .thesis-review-form { + margin-top: 8px; + border-top: 1px solid #263548; + padding-top: 16px; + } + .confirmation-dialog { position: fixed; top: 0; @@ -8638,6 +8790,465 @@ ); }; + const THESIS_STATUS_BADGE = { + canonical: 'badge-funded', + in_review: 'badge-meeting', + draft: 'badge-other', + superseded: 'badge-low', + archived: 'badge-low' + }; + + const thesisStatusClass = (status) => THESIS_STATUS_BADGE[status] || 'badge-other'; + + const formatThesisStatus = (status) => String(status || '').replace(/_/g, ' '); + + const ThesisVersionDetail = ({ token, versionId, user, onShowToast, onReviewed }) => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [reviewForm, setReviewForm] = useState({ decision: 'approve', feedback: '' }); + const [submitting, setSubmitting] = useState(false); + const isAdmin = user?.role === 'admin'; + + const loadVersion = useCallback(async () => { + try { + setLoading(true); + const result = await api(`/api/thesis/versions/${versionId}`, {}, token); + setData(result); + setError(''); + } catch (err) { + setError(getErrorMessage(err, 'Failed to load thesis version')); + } finally { + setLoading(false); + } + }, [token, versionId]); + + useEffect(() => { + loadVersion(); + }, [loadVersion]); + + const handleSubmitReview = async (e) => { + e.preventDefault(); + if (!isAdmin) return; + setSubmitting(true); + try { + await api(`/api/thesis/versions/${versionId}/review`, { + method: 'POST', + body: JSON.stringify({ + decision: reviewForm.decision, + feedback: reviewForm.feedback + }) + }, token); + setReviewForm({ decision: 'approve', feedback: '' }); + onShowToast('Review submitted', 'success'); + await loadVersion(); + if (onReviewed) onReviewed(); + } catch (err) { + onShowToast(getErrorMessage(err, 'Failed to submit review'), 'error'); + } finally { + setSubmitting(false); + } + }; + + if (loading) return
; + if (error) return
{error}
; + if (!data) return
No content
; + + const body = data.body || {}; + const pillars = Array.isArray(body.pillars) ? body.pillars : []; + const claims = Array.isArray(body.claims) ? body.claims : []; + const proofPoints = Array.isArray(body.proof_points) ? body.proof_points : []; + const objections = Array.isArray(body.objections) ? body.objections : []; + const segmentCuts = Array.isArray(body.segment_cuts) ? body.segment_cuts : []; + const reviews = Array.isArray(data.reviews) ? data.reviews : []; + const approvals = data.approvals ?? 0; + const required = data.required ?? 0; + + const renderTextOrLabel = (item) => { + if (item == null) return '-'; + if (typeof item === 'string') return item; + return item.text || item.label || item.claim || item.title || JSON.stringify(item); + }; + + return ( +
+
+
+ {data.line_key} · v{data.version_no} + {formatThesisStatus(data.status)} +
+
+ {approvals} / {required} + approvals +
+
+ + {body.throughline && ( +
+
Throughline
+
{body.throughline}
+
+ )} + + {pillars.length > 0 && ( +
+
Pillars
+ +
+ )} + + {claims.length > 0 && ( +
+
Claims
+ +
+ )} + + {proofPoints.length > 0 && ( +
+
Proof Points
+ +
+ )} + + {objections.length > 0 && ( +
+
Objections
+ +
+ )} + + {segmentCuts.length > 0 && ( +
+
Segment Cuts
+ +
+ )} + +
+
Reviews & Feedback
+ {reviews.length === 0 ? ( +
No reviews yet.
+ ) : ( +
+ {reviews.map((r, i) => ( +
+
+
+
+ {formatThesisStatus(r.decision)} + {r.reviewer_user_id || 'reviewer'} +
+
{formatDateLong(r.created_at)}
+ {r.feedback &&
{r.feedback}
} +
+
+ ))} +
+ )} +
+ + {isAdmin ? ( +
+
Submit Review
+
+ + +
+
+ +