Skip to content

Commit c71e31b

Browse files
author
jammy
committed
feat: refine ai-literacy evidence UI and fix nickname update flow
1 parent 96d2532 commit c71e31b

14 files changed

Lines changed: 310 additions & 32 deletions

File tree

packages/backend/prisma/content/java/lessons/java-1-4.en.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@
132132
{
133133
"title": "Using Arrays.equals()",
134134
"explanation": "**Execution order:**\n1. Call `Arrays.equals(arr1, arr2)`.\n2. Compare length -> Pass.\n3. Compare contents (1==1, 2==2, 3==3).\n4. All are equal, so **true**.",
135+
"illustrations": [
136+
{
137+
"src": "/lesson-illustrations/java-arrays-equals.svg",
138+
"alt": "Arrays.equals compares array values by index and returns true",
139+
"caption": "arr1 (0x001) and arr2 (0x002) have different addresses, but all indexed values are equal."
140+
}
141+
],
135142
"keyInsight": "Array comparison = using Arrays.equals()",
136143
"visualizationType": "javaMemory",
137144
"code": "System.out.println(Arrays.equals(arr1, arr2));",
@@ -140,6 +147,7 @@
140147
"true"
141148
],
142149
"warning": null,
150+
"note": "Why true? Arrays.equals(arr1, arr2) compares element values in order (1, 2, 3), not object addresses.",
143151
"stack": [
144152
{
145153
"name": "arr1",
@@ -192,7 +200,8 @@
192200
"content": "[3, 4]"
193201
}
194202
],
195-
"output": []
203+
"output": [],
204+
"note": null
196205
}
197206
},
198207
{

packages/backend/prisma/content/java/lessons/java-1-4.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@
132132
{
133133
"title": "Arrays.equals() 사용",
134134
"explanation": "**실행 순서:**\n1. `Arrays.equals(arr1, arr2)` 호출.\n2. 길이 비교 -> 통과.\n3. 내용 비교 (1==1, 2==2, 3==3).\n4. 모두 같으므로 **true**.",
135+
"illustrations": [
136+
{
137+
"src": "/lesson-illustrations/java-arrays-equals.svg",
138+
"alt": "Arrays.equals가 두 배열의 같은 인덱스 값을 순서대로 비교해 true가 되는 구조",
139+
"caption": "arr1(0x001), arr2(0x002)는 주소는 다르지만 각 인덱스의 값이 모두 같아서 true가 됩니다."
140+
}
141+
],
135142
"keyInsight": "배열 비교 = Arrays.equals() 사용",
136143
"visualizationType": "javaMemory",
137144
"code": "System.out.println(Arrays.equals(arr1, arr2));",
@@ -140,6 +147,7 @@
140147
"true"
141148
],
142149
"warning": null,
150+
"note": "왜 true일까요? Arrays.equals(arr1, arr2)는 주소가 아니라 각 요소 값(1,2,3)을 순서대로 비교하기 때문입니다.",
143151
"stack": [
144152
{
145153
"name": "arr1",
@@ -192,7 +200,8 @@
192200
"content": "[3, 4]"
193201
}
194202
],
195-
"output": []
203+
"output": [],
204+
"note": null
196205
}
197206
},
198207
{

packages/backend/prisma/content/java/lessons/java-1-4.zh.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@
132132
{
133133
"title": "使用 Arrays.equals()",
134134
"explanation": "**执行顺序:**\n1. Call `Arrays.equals(arr1, arr2)`.\n2. Compare length -> Pass.\n3. Compare contents (1==1, 2==2, 3==3).\n4. All are equal, so **true**.",
135+
"illustrations": [
136+
{
137+
"src": "/lesson-illustrations/java-arrays-equals.svg",
138+
"alt": "Arrays.equals 按索引比较数组元素并返回 true",
139+
"caption": "arr1(0x001)和 arr2(0x002)地址不同,但每个索引位置的值都相同。"
140+
}
141+
],
135142
"keyInsight": "数组比较 = 使用 Arrays.equals()",
136143
"visualizationType": "javaMemory",
137144
"code": "System.out.println(Arrays.equals(arr1, arr2));",
@@ -140,6 +147,7 @@
140147
"true"
141148
],
142149
"warning": null,
150+
"note": "为什么是 true?Arrays.equals(arr1, arr2) 比较的是元素值是否按顺序相等(1、2、3),不是对象地址。",
143151
"stack": [
144152
{
145153
"name": "arr1",
@@ -192,7 +200,8 @@
192200
"content": "[3, 4]"
193201
}
194202
],
195-
"output": []
203+
"output": [],
204+
"note": null
196205
}
197206
},
198207
{

packages/backend/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ const capacitorOrigins = ['capacitor://localhost', 'https://localhost', 'http://
5858
const allowedOrigins = [...config.server.corsOrigins, ...capacitorOrigins];
5959

6060
app.register(cors, {
61+
methods: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
62+
allowedHeaders: ['Content-Type', 'Authorization'],
6163
origin: config.server.isDev ? true : (origin, callback) => {
6264
if (!origin) return callback(null, true);
6365
if (allowedOrigins.includes(origin)) {

packages/backend/src/types/lesson-content.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ export interface LessonStep {
3333
occurrence?: number; // 동일 코드 라인 구분 (기본값: 1)
3434
title?: string;
3535
explanation: string;
36+
illustrations?: Array<{
37+
src: string;
38+
alt?: string;
39+
caption?: string;
40+
}>;
3641
highlight?: number[];
3742
highlightOffset?: number[]; // step.line 기준 상대 오프셋
3843
misconception?: string;
Lines changed: 56 additions & 0 deletions
Loading

packages/frontend/src/features/courses/components/LessonCodePanel.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { TerminalOutput, type TerminalLine } from '@/features/visualizers/shared
1616
interface LessonCodePanelProps {
1717
code: string;
1818
highlightLine: number;
19+
pointerLine?: number;
1920
terminalLines: TerminalLine[];
2021
onSelectionChange?: (selection: CodeSelection) => void;
2122
defaultRatio?: number;
@@ -29,6 +30,7 @@ interface LessonCodePanelProps {
2930
export function LessonCodePanel({
3031
code,
3132
highlightLine,
33+
pointerLine,
3234
terminalLines,
3335
onSelectionChange,
3436
defaultRatio = 0.5,
@@ -106,6 +108,7 @@ export function LessonCodePanel({
106108
<CodeMirrorEditor
107109
code={code}
108110
highlightLine={highlightLine}
111+
pointerLine={pointerLine}
109112
onSelectionChange={onSelectionChange}
110113
bottomPadding={0}
111114
mobileFontSizeOffset={2}

packages/frontend/src/features/courses/components/LessonUnifiedView.tsx

Lines changed: 121 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,43 @@ function asString(value: unknown): string | undefined {
5252
return typeof value === 'string' ? value : undefined;
5353
}
5454

55+
interface AiEvidenceData {
56+
phase?: string;
57+
expectedPath?: string;
58+
actualPath?: string;
59+
patch?: string;
60+
output?: string;
61+
checklist: string[];
62+
lineRef?: number;
63+
codeRef?: string;
64+
}
65+
66+
function getAiEvidenceData(
67+
stepRecord: Record<string, unknown> | undefined,
68+
lineRef?: number
69+
): AiEvidenceData {
70+
if (!stepRecord) {
71+
return { checklist: [], lineRef };
72+
}
73+
74+
const state = isRecord(stepRecord.algorithmState) ? stepRecord.algorithmState : undefined;
75+
const rawChecklist = state?.checklist;
76+
const checklist = Array.isArray(rawChecklist)
77+
? rawChecklist.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
78+
: [];
79+
80+
return {
81+
phase: asString(state?.phase),
82+
expectedPath: asString(state?.expectedPath),
83+
actualPath: asString(state?.actualPath),
84+
patch: asString(state?.patch),
85+
output: asString(state?.output),
86+
checklist,
87+
lineRef,
88+
codeRef: asString(stepRecord.code),
89+
};
90+
}
91+
5592
export function LessonUnifiedView({
5693
code,
5794
steps,
@@ -62,6 +99,7 @@ export function LessonUnifiedView({
6299
}: LessonUnifiedViewProps) {
63100
const { t } = useTranslation();
64101
const isMobile = useIsMobile();
102+
const isAiLiteracy = languageId === 'ai-literacy';
65103
const [activeVizTab, setActiveVizTab] = useState<'flow' | 'memory' | 'jsMemory'>('flow');
66104
const [isConceptOpen, setIsConceptOpen] = useState(false);
67105

@@ -70,10 +108,15 @@ export function LessonUnifiedView({
70108
steps,
71109
lessonId,
72110
onQuiz,
111+
forceSingleRound: isAiLiteracy,
73112
});
74113

75114
const currentStep = steps[nav.actualStepIndex];
76115
const currentStepRecord = currentStep as Record<string, unknown> | undefined;
116+
const currentStepIllustrations = Array.isArray(currentStepRecord?.illustrations)
117+
? (currentStepRecord.illustrations as Array<{ src: string; alt?: string; caption?: string }>)
118+
: undefined;
119+
const aiEvidence = getAiEvidenceData(currentStepRecord, currentStep?.line);
77120
const isExplanationRound = nav.round === 'explanation';
78121
const { showMemoryTab, showJsMemoryTab } = useMemo(() => {
79122
const vizSteps = nav.vizStepIndices.map(i => steps[i]);
@@ -86,7 +129,9 @@ export function LessonUnifiedView({
86129
};
87130
}, [languageId, nav.vizStepIndices, steps]);
88131
const hasVizTabs = showMemoryTab || showJsMemoryTab;
89-
const flowLanguage = languageId === 'python-practical' ? 'python' : (languageId || 'c');
132+
const flowLanguage = languageId === 'python-practical'
133+
? 'python'
134+
: (languageId === 'ai-literacy' ? 'javascript' : (languageId || 'c'));
90135
const rawConceptType = asString(currentStepRecord?.conceptVisualizationType) || asString(currentStepRecord?.visualizationType);
91136
const conceptType = rawConceptType && CONCEPT_TYPES.has(rawConceptType) ? rawConceptType : undefined;
92137
const conceptState = isRecord(currentStepRecord?.conceptState) ? currentStepRecord.conceptState : undefined;
@@ -167,16 +212,23 @@ export function LessonUnifiedView({
167212
borderColor: 'var(--theme-lesson-panel-border)',
168213
}}
169214
>
170-
<div className="flex gap-1">
171-
<span className={`round-tab px-2.5 py-1 text-xs md:text-sm font-bold rounded-full ${isExplanationRound ? 'round-tab-active' : 'round-tab-inactive'}`}>
172-
{t('lesson.explanation')}
173-
</span>
174-
{nav.hasVizRound && (
175-
<span className={`round-tab px-2.5 py-1 text-xs md:text-sm font-bold rounded-full ${!isExplanationRound ? 'round-tab-active' : 'round-tab-inactive'}`}>
176-
{t('lesson.visualization')}
215+
{!isAiLiteracy && (
216+
<div className="flex gap-1">
217+
<span className={`round-tab px-2.5 py-1 text-xs md:text-sm font-bold rounded-full ${isExplanationRound ? 'round-tab-active' : 'round-tab-inactive'}`}>
218+
{t('lesson.explanation')}
177219
</span>
178-
)}
179-
</div>
220+
{nav.hasVizRound && (
221+
<span className={`round-tab px-2.5 py-1 text-xs md:text-sm font-bold rounded-full ${!isExplanationRound ? 'round-tab-active' : 'round-tab-inactive'}`}>
222+
{t('lesson.visualization')}
223+
</span>
224+
)}
225+
</div>
226+
)}
227+
{isAiLiteracy && (
228+
<span className="text-xs md:text-sm font-semibold opacity-80">
229+
AI Verification
230+
</span>
231+
)}
180232
<span className="ml-auto text-xs md:text-sm font-semibold opacity-60">
181233
{nav.stepIndex + 1}/{nav.totalInRound} · L{currentStep?.line || 1}
182234
</span>
@@ -199,7 +251,65 @@ export function LessonUnifiedView({
199251
<StepExplanation
200252
explanation={currentStep?.explanation || ''}
201253
stepIndex={nav.stepIndex}
254+
illustrations={currentStepIllustrations}
202255
/>
256+
{isAiLiteracy && (
257+
<div className="mt-4 rounded-xl border border-[var(--theme-lesson-panel-border)] bg-[var(--theme-lesson-panel-bg)] overflow-hidden">
258+
<div className="px-3 py-2 text-sm font-semibold border-b border-[var(--theme-lesson-panel-border)]">
259+
Evidence
260+
</div>
261+
<div className="px-3 py-3 space-y-2.5 text-xs">
262+
<div className="flex items-center gap-2">
263+
<span className="font-semibold opacity-70 min-w-[56px]">Phase</span>
264+
<span className="rounded-md border border-[var(--theme-lesson-panel-border)] px-2 py-1 font-semibold">
265+
{aiEvidence.phase || 'inspect'}
266+
</span>
267+
</div>
268+
{aiEvidence.codeRef && (
269+
<div className="rounded-lg border border-[var(--theme-lesson-panel-border)] px-2.5 py-2">
270+
<div className="font-semibold opacity-70 mb-1">Reference</div>
271+
<div className="font-mono break-all text-[11px]">{aiEvidence.codeRef}</div>
272+
</div>
273+
)}
274+
{(aiEvidence.expectedPath || aiEvidence.actualPath) && (
275+
<div className="rounded-lg border border-[var(--theme-lesson-panel-border)] px-2.5 py-2">
276+
<div className="font-semibold opacity-70 mb-1">Path Check</div>
277+
{aiEvidence.expectedPath && (
278+
<div className="font-mono break-all">expect: {aiEvidence.expectedPath}</div>
279+
)}
280+
{aiEvidence.actualPath && (
281+
<div className="font-mono break-all">real: {aiEvidence.actualPath}</div>
282+
)}
283+
{aiEvidence.expectedPath && aiEvidence.actualPath && (
284+
<div className="font-mono mt-1 opacity-80">{aiEvidence.expectedPath} to {aiEvidence.actualPath}</div>
285+
)}
286+
</div>
287+
)}
288+
{aiEvidence.patch && (
289+
<div className="rounded-lg border border-[var(--theme-lesson-panel-border)] px-2.5 py-2">
290+
<div className="font-semibold opacity-70 mb-1">Patch</div>
291+
<div className="font-mono break-all">{aiEvidence.patch}</div>
292+
</div>
293+
)}
294+
{aiEvidence.output && (
295+
<div className="rounded-lg border border-[var(--theme-lesson-panel-border)] px-2.5 py-2">
296+
<div className="font-semibold opacity-70 mb-1">Output</div>
297+
<div className="font-mono break-all">{aiEvidence.output}</div>
298+
</div>
299+
)}
300+
{aiEvidence.checklist.length > 0 && (
301+
<div className="rounded-lg border border-[var(--theme-lesson-panel-border)] px-2.5 py-2">
302+
<div className="font-semibold opacity-70 mb-1">Checklist</div>
303+
<div className="space-y-1">
304+
{aiEvidence.checklist.map((item) => (
305+
<div key={item} className="font-mono">- {item}</div>
306+
))}
307+
</div>
308+
</div>
309+
)}
310+
</div>
311+
</div>
312+
)}
203313
</motion.div>
204314
) : (
205315
<motion.div
@@ -300,6 +410,7 @@ export function LessonUnifiedView({
300410
<LessonCodePanel
301411
code={code}
302412
highlightLine={currentStep?.line || 1}
413+
pointerLine={isAiLiteracy ? currentStep?.line : undefined}
303414
terminalLines={terminalLines}
304415
onSelectionChange={onSelectionChange}
305416
orientation={isMobile ? 'vertical' : 'horizontal'}

0 commit comments

Comments
 (0)