Skip to content

Commit 2feb2d4

Browse files
committed
Implement Rich Text and Accurate Review Timing
LiveReview Pre-Commit Check: ran (iter:4, coverage:100%)
1 parent 1c103db commit 2feb2d4

4 files changed

Lines changed: 72 additions & 8 deletions

File tree

internal/appcore/review_state.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type ReviewState struct {
2121
FriendlyName string `json:"friendlyName"`
2222
GeneratedTime string `json:"generatedTime"`
2323
RepositoryPath string `json:"repositoryPath,omitempty"`
24-
StartedAt time.Time `json:"-"`
24+
StartedAt time.Time `json:"startedAt"`
2525

2626
// Status
2727
Status string `json:"status"` // "in_progress", "completed", "failed", "blocked"

internal/staticserve/static/app.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,16 @@ async function initApp() {
10231023
}
10241024
}, [slidesEnabled, slideShowOpen]);
10251025

1026+
useEffect(() => {
1027+
const startedAt = reviewData?.startedAt || reviewData?.StartedAt;
1028+
if (!startedAt) return;
1029+
const startedAtMs = new Date(startedAt).getTime();
1030+
if (isNaN(startedAtMs)) return;
1031+
const offsetMs = Date.now() - startedAtMs;
1032+
reviewStartMsRef.current = getPerformanceNow() - offsetMs;
1033+
setPerformanceNowMs(getPerformanceNow());
1034+
}, [reviewData?.startedAt, reviewData?.StartedAt]);
1035+
10261036
useEffect(() => {
10271037
if (status === 'completed' || status === 'failed') {
10281038
if (reviewCompletedMsRef.current === null) {

internal/staticserve/static/components/Quiz.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ import { waitForPreact } from './utils.js';
55

66
const OPTION_LETTERS = ['A', 'B', 'C', 'D'];
77

8+
function escapeHTML(text) {
9+
return text
10+
.replace(/&/g, '&')
11+
.replace(/</g, '&lt;')
12+
.replace(/>/g, '&gt;');
13+
}
14+
15+
function renderQuizContent(text) {
16+
if (!text) return '';
17+
let result = escapeHTML(text);
18+
result = result.replace(/```([\s\S]*?)```/g, (_, code) => {
19+
return '<pre class="quiz-code-block"><code>' + code.trim() + '</code></pre>';
20+
});
21+
result = result.replace(/`([^`]+)`/g, '<code class="quiz-code-inline">$1</code>');
22+
return result;
23+
}
24+
825
export async function createQuiz() {
926
const { html, useState } = await waitForPreact();
1027

@@ -45,7 +62,9 @@ export async function createQuiz() {
4562
`}
4663
${quiz.map((q, qIdx) => html`
4764
<div class="quiz-question" key=${qIdx}>
48-
<div class="quiz-question-text">${qIdx + 1}. ${q.question}</div>
65+
<div class="quiz-question-text">
66+
${qIdx + 1 + '. '}<span dangerouslySetInnerHTML=${{ __html: renderQuizContent(q.question) }}></span>
67+
</div>
4968
<div class="quiz-options">
5069
${(q.options || []).map((opt, oIdx) => {
5170
const isChosen = selected[qIdx] === oIdx;
@@ -61,13 +80,13 @@ export async function createQuiz() {
6180
disabled=${submitted}
6281
>
6382
<span class="quiz-option-letter">${OPTION_LETTERS[oIdx] || oIdx + 1}</span>
64-
<span class="quiz-option-text">${opt}</span>
83+
<span class="quiz-option-text" dangerouslySetInnerHTML=${{ __html: renderQuizContent(opt) }}></span>
6584
</button>
6685
`;
6786
})}
6887
</div>
6988
${submitted && q.explanation && html`
70-
<div class="quiz-explanation">${q.explanation}</div>
89+
<div class="quiz-explanation" dangerouslySetInnerHTML=${{ __html: renderQuizContent(q.explanation) }}></div>
7190
`}
7291
</div>
7392
`)}

internal/staticserve/static/styles.css

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4170,14 +4170,17 @@ body {
41704170
margin-bottom: var(--space-md);
41714171
border-bottom: 1px solid var(--border-subtle);
41724172
overflow-x: auto;
4173+
scrollbar-width: thin;
41734174
}
41744175

41754176
.toolbar-performance {
41764177
display: flex;
41774178
align-items: center;
4178-
flex-wrap: wrap;
4179-
gap: 8px;
4179+
flex-wrap: nowrap;
4180+
gap: 6px;
41804181
min-width: 0;
4182+
overflow: hidden;
4183+
flex-shrink: 1;
41814184
}
41824185

41834186
.performance-pill {
@@ -4188,6 +4191,8 @@ body {
41884191
border-radius: 999px;
41894192
background: rgba(255, 255, 255, 0.035);
41904193
border: 1px solid rgba(255, 255, 255, 0.08);
4194+
flex-shrink: 0;
4195+
white-space: nowrap;
41914196
}
41924197

41934198
.performance-pill-label {
@@ -4208,6 +4213,7 @@ body {
42084213
display: flex;
42094214
gap: 8px;
42104215
margin-left: auto;
4216+
flex-shrink: 0;
42114217
}
42124218

42134219
/* Standardized action button in toolbar */
@@ -4428,8 +4434,10 @@ body {
44284434

44294435
.quiz-option.chosen {
44304436
border-color: var(--accent-blue);
4431-
background: var(--bg-active);
4432-
color: var(--text-primary);
4437+
border-left: 3px solid var(--accent-blue);
4438+
background: linear-gradient(135deg, rgba(0, 120, 212, 0.2), rgba(0, 120, 212, 0.1));
4439+
color: #eaf6ff;
4440+
box-shadow: 0 0 12px rgba(0, 120, 212, 0.15);
44334441
}
44344442

44354443
.quiz-option.correct {
@@ -4463,6 +4471,33 @@ body {
44634471
line-height: 1.4;
44644472
}
44654473

4474+
.quiz-code-inline {
4475+
display: inline;
4476+
padding: 1px 5px;
4477+
background: rgba(0, 120, 212, 0.15);
4478+
border: 1px solid rgba(0, 120, 212, 0.3);
4479+
border-radius: 3px;
4480+
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Menlo, Consolas, monospace;
4481+
font-size: 0.92em;
4482+
color: var(--accent-blue-light, #4fc3f7);
4483+
word-break: break-word;
4484+
}
4485+
4486+
.quiz-code-block {
4487+
display: block;
4488+
margin: 6px 0;
4489+
padding: 8px 12px;
4490+
background: rgba(0, 0, 0, 0.25);
4491+
border: 1px solid var(--border-medium);
4492+
border-radius: 4px;
4493+
overflow-x: auto;
4494+
white-space: pre;
4495+
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Menlo, Consolas, monospace;
4496+
font-size: 0.88em;
4497+
color: var(--text-primary);
4498+
line-height: 1.45;
4499+
}
4500+
44664501
.quiz-explanation {
44674502
margin-top: 10px;
44684503
padding: 10px 12px;

0 commit comments

Comments
 (0)