4949
5050 <div ref =" terminalBody" class =" terminal-body" >
5151 <div v-if =" tab === 'ai'" class =" ai-chat" >
52- <div v-for =" (msg, i) in messages" :key =" i" class =" chat-row" >
52+ <!-- 단계 진행 스테퍼 -->
53+ <div v-if =" !isAdminRole" class =" hint-stepper" >
54+ <div class =" step-track" >
55+ <span
56+ v-for =" n in 5"
57+ :key =" n"
58+ class =" step-dot"
59+ :class =" { done: n <= problemHintsUsed }"
60+ ></span >
61+ </div >
62+ <span class =" step-label" >{{ problemHintsUsed }}/5 단계</span >
63+ </div >
64+
65+ <!-- 첫 진입 온보딩 -->
66+ <div v-if =" messages.length === 0 && !isLoading" class =" ai-empty" >
67+ <div class =" ai-empty-avatar" >
68+ <img src =" @/assets/images/AIAssistant.svg" alt =" AI" />
69+ </div >
70+ <p class =" ai-empty-title" >아직 받은 힌트가 없어요</p >
71+ <p class =" ai-empty-sub" >
72+ 막히는 부분이 있으면 아래 'AI조교 힌트받기'를 눌러보세요. 1단계부터
73+ 시작해 단계가 올라갈수록 더 구체적으로 도와드립니다.
74+ </p >
75+ </div >
76+
77+ <!-- 메시지 -->
78+ <div v-for =" (msg, i) in renderMessages" :key =" i" class =" chat-row" >
5379 <div class =" avatar" :class =" { thinking: msg.thinking }" >
5480 <img src =" @/assets/images/AIAssistant.svg" alt =" AI" />
5581 </div >
5884 생각 중...
5985 </div >
6086
61- <div
62- v-else
63- class =" bubble"
64- :class =" { error: msg.error }"
65- v-text =" msg.text"
66- ></div >
87+ <div v-else-if =" msg.error" class =" bubble error" >{{ msg.text }}</div >
88+
89+ <div v-else class =" bubble hint-bubble" >
90+ <div v-if =" msg.parsed.level" class =" hint-level-badge" >
91+ {{ msg.parsed.level }}단계
92+ </div >
93+
94+ <template v-if =" msg .parsed .sections .length " >
95+ <div
96+ v-for =" (sec, si) in msg.parsed.sections"
97+ :key =" si"
98+ class =" hint-section"
99+ :class =" sectionClass(sec.label)"
100+ >
101+ <div class =" hint-section-label" >{{ sec.label }}</div >
102+ <div class =" hint-section-text" >{{ sec.text }}</div >
103+ </div >
104+ </template >
105+ <div v-else class =" hint-body" >{{ msg.parsed.body }}</div >
106+ </div >
67107 </div >
68108 </div >
69109
@@ -114,6 +154,14 @@ export default {
114154 if (this .isAdminRole ) return false // 관리자는 소진 개념이 없음
115155 return this .problemHintsRemaining <= 0
116156 },
157+
158+ // 각 메시지 텍스트를 단계 배지 + 섹션(진단/힌트/점검) 구조로 파싱
159+ renderMessages () {
160+ return this .messages .map ((m ) => ({
161+ ... m,
162+ parsed: m .thinking || m .error ? null : this .parseHint (m .text ),
163+ }))
164+ },
117165 },
118166
119167 methods: {
@@ -242,6 +290,44 @@ export default {
242290 this .eventSource = null
243291 },
244292
293+ // "[N단계]" + "▸ 라벨\n본문" 형식을 구조로 파싱 (스트리밍 중 부분 텍스트도 안전)
294+ parseHint (text ) {
295+ const result = { level: null , sections: [], body: " " }
296+ if (! text) return result
297+ let rest = text
298+ const lv = rest .match (/ ^ \s * \[ (\d + )단계\] \s * / )
299+ if (lv) {
300+ result .level = Number (lv[1 ])
301+ rest = rest .slice (lv[0 ].length )
302+ }
303+ if (rest .indexOf (" ▸" ) !== - 1 ) {
304+ rest
305+ .split (" ▸" )
306+ .map ((s ) => s .trim ())
307+ .filter ((s ) => s)
308+ .forEach ((part ) => {
309+ const nl = part .indexOf (" \n " )
310+ if (nl === - 1 ) {
311+ result .sections .push ({ label: part .trim (), text: " " })
312+ } else {
313+ result .sections .push ({
314+ label: part .slice (0 , nl).trim (),
315+ text: part .slice (nl + 1 ).trim (),
316+ })
317+ }
318+ })
319+ } else {
320+ result .body = rest .trim ()
321+ }
322+ return result
323+ },
324+
325+ sectionClass (label ) {
326+ if (label .indexOf (" 진단" ) !== - 1 ) return " sec-diagnosis"
327+ if (label .indexOf (" 점검" ) !== - 1 ) return " sec-check"
328+ return " sec-hint"
329+ },
330+
245331 startDrag () {
246332 this .isDragging = true
247333 },
@@ -436,7 +522,7 @@ export default {
436522.ai-chat {
437523 display : flex ;
438524 flex-direction : column ;
439- gap : 20 px ;
525+ gap : 16 px ;
440526}
441527
442528.chat-row {
@@ -446,15 +532,15 @@ export default {
446532}
447533
448534.avatar {
449- width : 52 px ;
450- height : 52 px ;
535+ width : 38 px ;
536+ height : 38 px ;
451537 flex-shrink : 0 ;
452538 background-color : var (--ai-assistant-avatar-bg );
453539 border-radius : 50% ;
454540 display : flex ;
455541 align-items : center ;
456542 justify-content : center ;
457- padding : 10 px ;
543+ padding : 8 px ;
458544}
459545
460546.avatar img {
@@ -484,12 +570,148 @@ export default {
484570 font-style : italic ;
485571}
486572
487- .result-message {
488- color : var (--ai-assistant-muted-text-color );
573+ /* 단계 진행 스테퍼 */
574+ .hint-stepper {
575+ display : flex ;
576+ align-items : center ;
577+ gap : 10px ;
578+ margin-bottom : 2px ;
579+ }
580+
581+ .step-track {
582+ display : flex ;
583+ gap : 4px ;
584+ flex : 1 ;
585+ }
586+
587+ .step-dot {
588+ flex : 1 ;
589+ height : 4px ;
590+ border-radius : 2px ;
591+ background : var (--ai-assistant-header-border-color );
592+ }
593+
594+ .step-dot.done {
595+ background : #5b64ed ;
596+ }
597+
598+ .step-label {
599+ font-size : 11px ;
600+ font-weight : 600 ;
601+ color : var (--ai-assistant-subtle-text-color );
602+ white-space : nowrap ;
603+ }
604+
605+ /* 첫 진입 온보딩 */
606+ .ai-empty {
607+ display : flex ;
608+ flex-direction : column ;
609+ align-items : center ;
610+ text-align : center ;
611+ gap : 6px ;
612+ padding : 16px 24px 8px ;
613+ }
614+
615+ .ai-empty-avatar {
616+ width : 48px ;
617+ height : 48px ;
618+ border-radius : 50% ;
619+ background-color : var (--ai-assistant-avatar-bg );
620+ display : flex ;
621+ align-items : center ;
622+ justify-content : center ;
623+ padding : 11px ;
624+ margin-bottom : 2px ;
625+ }
626+
627+ .ai-empty-avatar img {
628+ width : 100% ;
629+ height : 100% ;
630+ filter : var (--ai-assistant-avatar-icon-filter );
631+ }
632+
633+ .ai-empty-title {
634+ margin : 0 ;
635+ font-size : 14px ;
636+ font-weight : 700 ;
637+ color : var (--ai-assistant-bubble-text-color );
638+ }
639+
640+ .ai-empty-sub {
641+ margin : 0 ;
642+ max-width : 340px ;
643+ font-size : 12.5px ;
644+ line-height : 1.6 ;
645+ color : var (--ai-assistant-subtle-text-color );
646+ }
647+
648+ /* 힌트 말풍선: 구조 요소 사이 여백이 pre-wrap으로 빈 줄처럼 보이지 않도록 normal */
649+ .hint-bubble {
650+ white-space : normal ;
651+ }
652+
653+ /* 단계 배지 */
654+ .hint-level-badge {
655+ display : inline-block ;
656+ font-size : 11px ;
657+ font-weight : 700 ;
658+ color : #fff ;
659+ background : #5b64ed ;
660+ padding : 2px 10px ;
661+ border-radius : 99px ;
662+ margin-bottom : 10px ;
663+ }
664+
665+ /* 힌트 섹션 (진단 / 힌트 / 점검 포인트) */
666+ .hint-section {
667+ margin-bottom : 12px ;
668+ }
669+
670+ .hint-section :last-child {
671+ margin-bottom : 0 ;
672+ }
673+
674+ .hint-section-label {
675+ font-size : 11px ;
676+ font-weight : 700 ;
677+ letter-spacing : 0.02em ;
678+ margin-bottom : 4px ;
679+ color : var (--ai-assistant-subtle-text-color );
680+ }
681+
682+ .hint-section-text {
489683 font-size : 14px ;
684+ line-height : 1.6 ;
685+ white-space : pre-wrap ;
490686}
491687
492- .empty-message {
688+ .sec-diagnosis .hint-section-label {
689+ color : #c2751a ;
690+ }
691+
692+ .sec-hint .hint-section-label {
693+ color : #5b64ed ;
694+ }
695+
696+ /* 점검 포인트 = 강조 콜아웃 */
697+ .sec-check {
698+ background : rgba (91 , 100 , 237 , 0.07 );
699+ border-left : 3px solid #5b64ed ;
700+ padding : 8px 12px ;
701+ border-radius : 0 8px 8px 0 ;
702+ }
703+
704+ .sec-check .hint-section-label {
705+ color : #5b64ed ;
706+ }
707+
708+ .hint-body {
709+ font-size : 14px ;
710+ line-height : 1.6 ;
711+ white-space : pre-wrap ;
712+ }
713+
714+ .result-message {
493715 color : var (--ai-assistant-muted-text-color );
494716 font-size : 14px ;
495717}
0 commit comments