세션 화면 디자인 개편

This commit is contained in:
Yun Chan 2026-07-31 09:19:42 +09:00
parent 04a5f485ed
commit 12bdc59497
13 changed files with 1430 additions and 386 deletions

View file

@ -1959,8 +1959,8 @@ export default function Session() {
voiceAvailable,
micOn,
});
const elapsedLabel = formatElapsed(elapsed);
const remainingLabel = formatElapsed(remainingSeconds);
const elapsedLabel = formatTimecode(elapsed);
const remainingLabel = formatTimecode(remainingSeconds);
const limitMinutesLabel = Math.round(sessionLimitSeconds / 60);
const warningMinutesLabel = Math.max(1, Math.round(sessionWarningSeconds / 60));
const selectedTheoryOption =
@ -2544,15 +2544,24 @@ export default function Session() {
<div className="sx-session-progress__grid">
<span>
<b>{elapsedLabel}</b>
<small> </small>
<small>
<Icon name="clock" size={18} />
</small>
</span>
<span className={inWarningWindow || timeUp ? "is-warning" : ""}>
<b>{timeUp ? "정리 시간" : remainingLabel}</b>
<small>{limitMinutesLabel} · </small>
<small>
<Icon name="hourglass" size={18} />
</small>
</span>
<span>
<b>{turnCount}</b>
<small> </small>
<small>
<Icon name="review" size={18} />
</small>
</span>
</div>
{goalsAchieved && !timeUp && !sessionEnded ? (
@ -2610,6 +2619,7 @@ export default function Session() {
</div>
<div className="sx-stage__now">
<Icon name="users" size={20} />
<span>: {stageStateText[avatarState]}</span>
<small>{avatarExpressionLabel}</small>
</div>
@ -2783,8 +2793,7 @@ export default function Session() {
{feedbackMode !== "immersive" ? (
<section className={surfaceClassName("sx-panel sx-signal")}>
<div className="sx-signal__head">
{/* D1 — 신호 상태는 아래 sx-signal__one-dot 이 색으로 전달 → 라벨 dot 제거 */}
<Kicker dot={false}> </Kicker>
<Kicker dot={false}> </Kicker>
</div>
{liveSignal ? (
@ -2811,148 +2820,135 @@ export default function Session() {
<span className="sx-signal__one-when"></span>
)}
</div>
) : (
<p className={surfaceClassName("sx-signal__rest", { variant: "inset", flat: true })}>
. .
</p>
)}
) : null}
<div className="sx-signal__wave" aria-hidden="true">
<i />
<i />
<i />
<i />
<i />
<i />
<i />
<i />
</div>
<div className="sx-signal__rows">
<div className="sx-signal__status" aria-label="연결 상태">
<span>
<b> </b>
<Icon name="mic" size={23} />
<b></b>
<small>{voiceInputStatus}</small>
</span>
<span>
<b> </b>
<Icon name="spark" size={23} />
<b>AI </b>
<small>{responseStatus}</small>
</span>
<span>
<b> </b>
<small>
{feedbackMode === "ambient"
? "상태 신호"
: feedbackMode === "coached"
? "코칭"
: "몰입"}
</small>
</span>
</div>
<div className="sx-signal__seq">
<span className="sx-signal__seq-label"> </span>
<span className="sx-signal__seq-dots">
{signalSeq.map((t, i) => (
<i
key={i}
className={
(t === "pos" ? "is-pos" : t === "warn" ? "is-warn" : "") +
(i === signalSeq.length - 1 ? " is-now" : "")
}
/>
))}
</span>
</div>
{feedbackMode === "coached" ? (
<div
className={
"sx-coach-card" +
(coachTone === "pos" ? " is-pos" : coachTone === "warn" ? " is-warn" : "") +
(coachLoading ? " is-loading" : "") +
(coachIsDegraded ? " is-degraded" : "")
}
>
<div className="sx-coach-avatar" aria-hidden="true">
<span className="sx-coach-avatar__lens" />
<span className="sx-coach-avatar__face">
<i />
<i />
</span>
</div>
<div className={surfaceClassName("sx-coach-bubble", { variant: "inset", flat: true })}>
<div className="sx-coach-bubble__meta">
<b>{coachIsDegraded ? "대체 코칭" : "AI 코치"}</b>
<span>{coachStatusText}</span>
</div>
{coachIsDegraded ? (
<p className="sx-coach-degraded-note">{coachDegradedNote}</p>
) : null}
{coachSyncWarning ? (
<p
{signalSeq.length > 0 ? (
<div className="sx-signal__seq">
<span className="sx-signal__seq-label"> </span>
<span className="sx-signal__seq-dots">
{signalSeq.map((t, i) => (
<i
key={i}
className={
"sx-coach-persistence-note" +
(coachPersistenceSource === "runtime" ? " is-runtime" : "")
(t === "pos" ? "is-pos" : t === "warn" ? "is-warn" : "") +
(i === signalSeq.length - 1 ? " is-now" : "")
}
role="status"
>
{coachSyncWarning}
</p>
) : null}
<div className="sx-coach-quota" aria-live="polite">
<span> </span>
<span className="sx-coach-quota__dots" aria-hidden="true">
{coachQuotaSlots.map((slot) => (
<i
key={slot}
className={slot < coachQuotaRemaining ? "is-filled" : ""}
/>
))}
</span>
<b>
{coachQuotaRemaining}/{coachQuotaMax}
</b>
</div>
{coachCreditPulseText ? (
<div
className={
"sx-coach-credit-pulse" +
(coachCreditPulse?.event_type === "recharge" ? " is-recharge" : "")
}
aria-live="polite"
>
{coachCreditPulseText}
</div>
) : null}
{coachLoading ? (
<p> .</p>
) : coachError ? (
<p>{coachError}</p>
) : coachSuggestion ? (
<>
<strong>{coachSuggestion.title}</strong>
<p>{coachSuggestion.message}</p>
{coachSuggestion.next_utterance ? (
<blockquote>{coachSuggestion.next_utterance}</blockquote>
) : null}
<div className="sx-coach-bubble__actions">
<button type="button" onClick={() => setCoachEvidenceOpen(true)}>
</button>
<button type="button" onClick={() => openCoachHistory(null)}>
</button>
</div>
</>
) : coachQuotaRemaining <= 0 ? (
<p> . 1 .</p>
) : (
<p> .</p>
)}
</div>
/>
))}
</span>
</div>
) : null}
<div
className={
"sx-coach-card" +
(coachTone === "pos" ? " is-pos" : coachTone === "warn" ? " is-warn" : "") +
(coachLoading ? " is-loading" : "") +
(coachIsDegraded ? " is-degraded" : "")
}
>
<div className="sx-coach-avatar" aria-hidden="true">
<span className="sx-coach-avatar__lens" />
<span className="sx-coach-avatar__face">
<i />
<i />
</span>
</div>
<div className="sx-coach-summary">
<div className="sx-coach-bubble__meta">
<b>{coachIsDegraded ? "대체 코칭" : "AI 코치"}</b>
<span>{coachStatusText}</span>
</div>
<div className="sx-coach-quota" aria-live="polite">
<span> </span>
<span className="sx-coach-quota__dots" aria-hidden="true">
{coachQuotaSlots.map((slot) => (
<i
key={slot}
className={slot < coachQuotaRemaining ? "is-filled" : ""}
/>
))}
</span>
<b>
{coachQuotaRemaining}/{coachQuotaMax}
</b>
</div>
</div>
<div
className={surfaceClassName("sx-coach-bubble", {
variant: "inset",
flat: true,
})}
>
{coachIsDegraded ? (
<p className="sx-coach-degraded-note">{coachDegradedNote}</p>
) : null}
{coachSyncWarning ? (
<p
className={
"sx-coach-persistence-note" +
(coachPersistenceSource === "runtime" ? " is-runtime" : "")
}
role="status"
>
{coachSyncWarning}
</p>
) : null}
{coachCreditPulseText ? (
<div
className={
"sx-coach-credit-pulse" +
(coachCreditPulse?.event_type === "recharge" ? " is-recharge" : "")
}
aria-live="polite"
>
{coachCreditPulseText}
</div>
) : null}
{coachLoading ? (
<p> .</p>
) : coachError ? (
<p>{coachError}</p>
) : coachSuggestion ? (
<>
<strong>{coachSuggestion.title}</strong>
<p>{coachSuggestion.message}</p>
{coachSuggestion.next_utterance ? (
<blockquote>{coachSuggestion.next_utterance}</blockquote>
) : null}
<div className="sx-coach-bubble__actions">
<button type="button" onClick={() => setCoachEvidenceOpen(true)}>
</button>
<button type="button" onClick={() => openCoachHistory(null)}>
</button>
</div>
</>
) : coachQuotaRemaining <= 0 ? (
<p>
. 1
.
</p>
) : (
<p> .</p>
)}
</div>
</div>
<div className="sx-signal__defer">
{feedbackMode === "coached" ? (
<>