From d5f69bb86133828fd2ac91b8ac76c962a0eef99e Mon Sep 17 00:00:00 2001
From: ak-asu
Date: Sun, 22 Mar 2026 09:04:10 -0700
Subject: [PATCH] preview ui changes
---
.../src/components/forge/ForgeChatPanel.tsx | 161 ++++++------
.../components/forge/RequirementsPanel.tsx | 243 ++++++++++++++++--
2 files changed, 307 insertions(+), 97 deletions(-)
diff --git a/frontend/src/components/forge/ForgeChatPanel.tsx b/frontend/src/components/forge/ForgeChatPanel.tsx
index dae90f9..1c438ce 100644
--- a/frontend/src/components/forge/ForgeChatPanel.tsx
+++ b/frontend/src/components/forge/ForgeChatPanel.tsx
@@ -333,8 +333,8 @@ function ClarificationCard({
{q.question}
- {/* Option pills */}
-
+ {/* Option pills — uniform grid so all chips share equal width */}
+
{q.options.map((opt, optIdx) => {
const isSelected = selectedOptIdx === optIdx;
return (
@@ -344,10 +344,10 @@ function ClarificationCard({
disabled={disabled}
onClick={() => handleOptionClick(qIdx, optIdx)}
style={{
- padding: '4px 10px',
- borderRadius: '100px',
+ padding: '6px 10px',
+ borderRadius: '7px',
border: `0.5px solid ${isSelected ? 'rgba(45,212,191,0.35)' : 'var(--lp-border)'}`,
- background: isSelected ? 'var(--lp-accent-dim)' : 'transparent',
+ background: isSelected ? 'var(--lp-accent-dim)' : 'var(--lp-elevated)',
color: isSelected ? 'var(--lp-accent)' : 'var(--lp-text-secondary)',
fontFamily: 'var(--font-inter), system-ui, sans-serif',
fontSize: '11px',
@@ -356,6 +356,8 @@ function ClarificationCard({
transition: 'background 120ms ease, border-color 120ms ease, color 120ms ease',
outline: 'none',
lineHeight: 1.4,
+ textAlign: 'center',
+ width: '100%',
}}
aria-pressed={isSelected}
>
@@ -1064,42 +1066,47 @@ function InputBar({
)}
-
);
}
@@ -1244,8 +1259,8 @@ export default function ForgeChatPanel() {
`}
/g, '>');
+
+ // Step 2: fenced code blocks (protect from further processing)
+ const codeBlocks: string[] = [];
+ s = s.replace(/```(?:[a-zA-Z]*)\n?([\s\S]*?)```/g, (_m, code: string) => {
+ const idx = codeBlocks.length;
+ codeBlocks.push(
+ `
${code}
`
+ );
+ return `\x00CB${idx}\x00`;
+ });
+
+ // Step 3: inline code
+ s = s.replace(/`([^`\n]+)`/g, (_m, code: string) =>
+ `
${code}`
+ );
+
+ // Step 4: headers
+ s = s
+ .replace(/^#### (.+)$/gm, '
$1
')
+ .replace(/^### (.+)$/gm, '
$1
')
+ .replace(/^## (.+)$/gm, '
$1
')
+ .replace(/^# (.+)$/gm, '
$1
');
+
+ // Step 5: bold + italic
+ s = s
+ .replace(/\*\*\*([^*\n]+)\*\*\*/g, '
$1')
+ .replace(/\*\*([^*\n]+)\*\*/g, '
$1')
+ .replace(/\*([^*\n]+)\*/g, '
$1');
+
+ // Step 6: horizontal rules
+ s = s.replace(/^---$/gm, '
');
+
+ // Step 7: blockquotes (> is already > after escaping)
+ s = s.replace(/^> (.+)$/gm,
+ '
$1
'
+ );
+
+ // Step 8: list items
+ s = s.replace(/^[*-] (.+)$/gm,
+ '
$1'
+ );
+ s = s.replace(/^\d+\. (.+)$/gm,
+ '
$1'
+ );
+ // Wrap consecutive li elements
+ s = s.replace(/(
]*>[\s\S]*?<\/li>\n?)+/g,
+ m => ``
+ );
+
+ // Step 9: paragraphs — split by double newlines
+ const blocks = s.split(/\n\n+/);
+ s = blocks.map(block => {
+ block = block.trim();
+ if (!block) return '';
+ if (/^<(h[1-6]|ul|ol|pre|hr|blockquote)/.test(block) || block.startsWith('\x00CB')) return block;
+ return `${block.replace(/\n/g, '
')}
`;
+ }).join('\n');
+
+ // Step 10: restore code blocks
+ codeBlocks.forEach((block, idx) => {
+ s = s.replace(`\x00CB${idx}\x00`, block);
+ });
+
+ return s;
+}
+
// ── Requirements panel ────────────────────────────────────────────────────────
export default function RequirementsPanel() {
@@ -46,6 +120,12 @@ export default function RequirementsPanel() {
const [textareaFocused, setTextareaFocused] = useState(false);
const [genButtonPulsed, setGenButtonPulsed] = useState(false);
+ const [previewMode, setPreviewMode] = useState(false);
+
+ // Refs for scroll-position sync between edit and preview
+ const editRef = useRef(null);
+ const previewRef = useRef(null);
+ const scrollRatioRef = useRef(0);
const reqStatus = stageStatus.requirements;
const isDone = reqStatus === 'done';
@@ -63,8 +143,33 @@ export default function RequirementsPanel() {
prevDoneRef.current = isDone;
}, [isDone]);
+ function captureScrollRatio(el: HTMLElement | null) {
+ if (!el) return;
+ const max = el.scrollHeight - el.clientHeight;
+ scrollRatioRef.current = max > 0 ? el.scrollTop / max : 0;
+ }
+
+ function applyScrollRatio(el: HTMLElement | null) {
+ if (!el) return;
+ const max = el.scrollHeight - el.clientHeight;
+ el.scrollTop = max * scrollRatioRef.current;
+ }
+
+ function togglePreview() {
+ if (!previewMode) {
+ // Going to preview — capture edit scroll position
+ captureScrollRatio(editRef.current);
+ setPreviewMode(true);
+ setTimeout(() => applyScrollRatio(previewRef.current), 30);
+ } else {
+ // Going to edit — capture preview scroll position
+ captureScrollRatio(previewRef.current);
+ setPreviewMode(false);
+ setTimeout(() => applyScrollRatio(editRef.current), 30);
+ }
+ }
+
function handleSubmitPrd() {
- // Re-analyze: reset constraints and stage so the user can re-send via chat.
if (!prdText.trim() || !currentProjectId) return;
setConstraints([]);
setStageStatus('requirements', 'locked');
@@ -139,33 +244,119 @@ export default function RequirementsPanel() {
- {/* ── PRD textarea ─────────────────────────────────────────────────────── */}
-