세션 화면 디자인 개편
This commit is contained in:
parent
04a5f485ed
commit
12bdc59497
13 changed files with 1430 additions and 386 deletions
|
|
@ -9,6 +9,9 @@ import type { ReactNode, SVGProps } from "react";
|
|||
|
||||
export type IconName =
|
||||
| "home"
|
||||
| "clock"
|
||||
| "hourglass"
|
||||
| "spark"
|
||||
| "session"
|
||||
| "review"
|
||||
| "users"
|
||||
|
|
@ -50,6 +53,24 @@ export interface IconProps extends Omit<SVGProps<SVGSVGElement>, "name"> {
|
|||
|
||||
// 각 아이콘의 path/children (24x24 viewBox 기준). google 만 멀티컬러 예외.
|
||||
const PATHS: Record<IconName, ReactNode> = {
|
||||
clock: (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M12 7v5l3 2" />
|
||||
</>
|
||||
),
|
||||
hourglass: (
|
||||
<>
|
||||
<path d="M6 3h12M6 21h12" />
|
||||
<path d="M7 3c0 4 1.5 6.2 5 9-3.5 2.8-5 5-5 9M17 3c0 4-1.5 6.2-5 9 3.5 2.8 5 5 5 9" />
|
||||
</>
|
||||
),
|
||||
spark: (
|
||||
<>
|
||||
<path d="m12 3 1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3Z" />
|
||||
<path d="m19 15 .8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8L19 15Z" />
|
||||
</>
|
||||
),
|
||||
refresh: (
|
||||
<>
|
||||
<path d="M21 12a9 9 0 1 1-2.64-6.36L21 8" />
|
||||
|
|
|
|||
|
|
@ -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" ? (
|
||||
<>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue