워크시트 검수 UI와 세션 다크톤 정리

This commit is contained in:
Yun Chan 2026-06-28 23:53:37 +09:00
parent f7aae885b2
commit 30441bd93b
7 changed files with 404 additions and 121 deletions

View file

@ -10,6 +10,7 @@ from pydantic import BaseModel, Field
from .. import session_persistence from .. import session_persistence
from ..deps import Principal, Role, require_role from ..deps import Principal, Role, require_role
from ..runtime_policy import require_runtime_fallback_allowed from ..runtime_policy import require_runtime_fallback_allowed
from ..session_read_model import StageLabel, stage_label
from ..services import session_metrics from ..services import session_metrics
from ..store import InProcSession, store from ..store import InProcSession, store
@ -26,7 +27,7 @@ class TeacherSessionSummary(BaseModel):
persona_name: str persona_name: str
session_no: int session_no: int
status: str status: str
stage: str stage: StageLabel
turn_count: int turn_count: int
learner_turn_count: int learner_turn_count: int
client_turn_count: int client_turn_count: int
@ -40,6 +41,8 @@ class TeacherSessionSummary(BaseModel):
class TeacherSessionReviewStatusRequest(BaseModel): class TeacherSessionReviewStatusRequest(BaseModel):
status: Literal["viewed", "closed"] = "closed" status: Literal["viewed", "closed"] = "closed"
note: str = Field(default="", max_length=2000) note: str = Field(default="", max_length=2000)
worksheet_status: Literal["approved", "changes_requested", "rejected", "pending"] | None = None
worksheet_note: str | None = Field(default=None, max_length=2000)
class TeacherSessionReviewStatusResponse(BaseModel): class TeacherSessionReviewStatusResponse(BaseModel):
@ -49,13 +52,16 @@ class TeacherSessionReviewStatusResponse(BaseModel):
reviewer_id: str | None = None reviewer_id: str | None = None
reviewed_at: str | None = None reviewed_at: str | None = None
updated_at: str | None = None updated_at: str | None = None
worksheet_status: Literal["pending", "approved", "changes_requested", "rejected"] = "pending"
worksheet_note: str = ""
worksheet_reviewed_at: str | None = None
class TeacherGrowthPoint(BaseModel): class TeacherGrowthPoint(BaseModel):
session_id: str session_id: str
session_no: int session_no: int
persona_code: str persona_code: str
stage: str stage: StageLabel
started_at: str started_at: str
ended_at: str | None = None ended_at: str | None = None
score: float | None = None score: float | None = None
@ -118,7 +124,7 @@ def _growth_point(point: session_metrics.SessionGrowthPoint) -> TeacherGrowthPoi
session_id=point.session_id, session_id=point.session_id,
session_no=point.session_no, session_no=point.session_no,
persona_code=point.persona_code, persona_code=point.persona_code,
stage=point.stage, stage=stage_label(point.stage),
started_at=point.started_at, started_at=point.started_at,
ended_at=point.ended_at, ended_at=point.ended_at,
score=point.score, score=point.score,
@ -161,6 +167,15 @@ def _review_status_value(record: dict[str, object] | None) -> Literal["pending",
return "pending" return "pending"
def _worksheet_review_status_value(
record: dict[str, object] | None,
) -> Literal["pending", "approved", "changes_requested", "rejected"]:
value = str((record or {}).get("worksheet_status") or "pending")
if value in {"approved", "changes_requested", "rejected"}:
return value # type: ignore[return-value]
return "pending"
def _summary( def _summary(
sess: InProcSession, sess: InProcSession,
review_status: dict[str, object] | None = None, review_status: dict[str, object] | None = None,
@ -175,7 +190,7 @@ def _summary(
persona_name=sess.persona.display_name, persona_name=sess.persona.display_name,
session_no=sess.session_no, session_no=sess.session_no,
status="ended" if sess.ended else "active", status="ended" if sess.ended else "active",
stage=sess.state.stage.value, stage=stage_label(sess.state.stage),
turn_count=len(sess.turns), turn_count=len(sess.turns),
learner_turn_count=learner_turns, learner_turn_count=learner_turns,
client_turn_count=client_turns, client_turn_count=client_turns,
@ -286,6 +301,8 @@ async def update_session_review_status(
status=request.status, status=request.status,
note=request.note, note=request.note,
principal=principal, principal=principal,
worksheet_status=request.worksheet_status,
worksheet_note=request.worksheet_note,
) )
if saved is None: if saved is None:
raise HTTPException( raise HTTPException(
@ -299,4 +316,7 @@ async def update_session_review_status(
reviewer_id=str(saved.get("reviewer_id") or "") or None, reviewer_id=str(saved.get("reviewer_id") or "") or None,
reviewed_at=str(saved.get("reviewed_at") or "") or None, reviewed_at=str(saved.get("reviewed_at") or "") or None,
updated_at=str(saved.get("updated_at") or "") or None, updated_at=str(saved.get("updated_at") or "") or None,
worksheet_status=_worksheet_review_status_value(saved),
worksheet_note=str(saved.get("worksheet_note") or ""),
worksheet_reviewed_at=str(saved.get("worksheet_reviewed_at") or "") or None,
) )

View file

@ -212,6 +212,9 @@ class TeacherDashboardGrowthTest(unittest.IsolatedAsyncioTestCase):
"reviewer_id": principal.user_id, "reviewer_id": principal.user_id,
"status": "closed", "status": "closed",
"note": "다음 회기에서 반영 질문을 늘리도록 지도", "note": "다음 회기에서 반영 질문을 늘리도록 지도",
"worksheet_status": "changes_requested",
"worksheet_note": "보호요인 근거를 한 줄 더 보강",
"worksheet_reviewed_at": "2026-06-27T10:05:00Z",
"reviewed_at": "2026-06-27T10:00:00Z", "reviewed_at": "2026-06-27T10:00:00Z",
"updated_at": "2026-06-27T10:00:00Z", "updated_at": "2026-06-27T10:00:00Z",
}, },
@ -225,13 +228,21 @@ class TeacherDashboardGrowthTest(unittest.IsolatedAsyncioTestCase):
teacher.TeacherSessionReviewStatusRequest( teacher.TeacherSessionReviewStatusRequest(
status="closed", status="closed",
note="다음 회기에서 반영 질문을 늘리도록 지도", note="다음 회기에서 반영 질문을 늘리도록 지도",
worksheet_status="changes_requested",
worksheet_note="보호요인 근거를 한 줄 더 보강",
), ),
principal, principal,
) )
save_status.assert_awaited_once() save_status.assert_awaited_once()
save_kwargs = save_status.await_args.kwargs
self.assertEqual(save_kwargs["worksheet_status"], "changes_requested")
self.assertIn("보호요인", save_kwargs["worksheet_note"])
self.assertEqual(response.status, "closed") self.assertEqual(response.status, "closed")
self.assertIn("반영 질문", response.note) self.assertIn("반영 질문", response.note)
self.assertEqual(response.worksheet_status, "changes_requested")
self.assertIn("보호요인", response.worksheet_note)
self.assertEqual(response.worksheet_reviewed_at, "2026-06-27T10:05:00Z")
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1851,7 +1851,7 @@ export interface components {
/** Session Id */ /** Session Id */
session_id: string; session_id: string;
/** Stage */ /** Stage */
stage: string; stage?: ("라포" | "탐색" | "개입" | "정리") | null;
}; };
/** HTTPValidationError */ /** HTTPValidationError */
HTTPValidationError: { HTTPValidationError: {
@ -2287,7 +2287,7 @@ export interface components {
/** Session Id */ /** Session Id */
session_id: string; session_id: string;
/** Stage */ /** Stage */
stage: string; stage?: ("라포" | "탐색" | "개입" | "정리") | null;
suggestion: components["schemas"]["LiveCoachSuggestion"]; suggestion: components["schemas"]["LiveCoachSuggestion"];
/** Turn Seq */ /** Turn Seq */
turn_seq: number; turn_seq: number;
@ -2906,7 +2906,7 @@ export interface components {
*/ */
privacy: string; privacy: string;
/** Reachedphase */ /** Reachedphase */
reachedPhase: string; reachedPhase?: ("라포" | "탐색" | "개입" | "정리") | null;
/** /**
* Reviewready * Reviewready
* @default false * @default false
@ -2995,10 +2995,16 @@ export interface components {
}; };
/** ReviewPhaseSegment */ /** ReviewPhaseSegment */
ReviewPhaseSegment: { ReviewPhaseSegment: {
/** Key */ /**
key: string; * Key
/** Label */ * @enum {string}
label: string; */
key: "rapport" | "explore" | "intervene" | "closing";
/**
* Label
* @enum {string}
*/
label: "라포" | "탐색" | "개입" | "정리";
/** Weight */ /** Weight */
weight: number; weight: number;
}; };
@ -3196,13 +3202,8 @@ export interface components {
/** Session No */ /** Session No */
session_no: number; session_no: number;
}; };
/** /** SessionEvaluationResponse */
* SessionEvaluation SessionEvaluationResponse: {
* @description deep-loop / .
*
* + + (3) + rationale/critique + .
*/
SessionEvaluation: {
/** Alternative Utterances */ /** Alternative Utterances */
alternative_utterances?: string[]; alternative_utterances?: string[];
distribution?: components["schemas"]["TechniqueDistribution"]; distribution?: components["schemas"]["TechniqueDistribution"];
@ -3224,8 +3225,11 @@ export interface components {
scope: string; scope: string;
/** Session Id */ /** Session Id */
session_id: string; session_id: string;
/** Stage */ /**
stage: string; * Stage
* @enum {string}
*/
stage: "라포" | "탐색" | "개입" | "정리";
/** Strengths */ /** Strengths */
strengths?: string[]; strengths?: string[];
/** Supervisor Critique */ /** Supervisor Critique */
@ -3275,8 +3279,11 @@ export interface components {
phaseAxis?: string[]; phaseAxis?: string[];
/** Phases */ /** Phases */
phases?: components["schemas"]["ReviewPhaseSegment"][]; phases?: components["schemas"]["ReviewPhaseSegment"][];
/** Reachedphase */ /**
reachedPhase: string; * Reachedphase
* @enum {string}
*/
reachedPhase: "라포" | "탐색" | "개입" | "정리";
/** /**
* Reviewready * Reviewready
* @default false * @default false
@ -3374,6 +3381,19 @@ export interface components {
status: "pending" | "viewed" | "closed"; status: "pending" | "viewed" | "closed";
/** Updatedat */ /** Updatedat */
updatedAt?: string | null; updatedAt?: string | null;
/**
* Worksheetnote
* @default
*/
worksheetNote: string;
/** Worksheetreviewedat */
worksheetReviewedAt?: string | null;
/**
* Worksheetstatus
* @default pending
* @enum {string}
*/
worksheetStatus: "pending" | "approved" | "changes_requested" | "rejected";
}; };
/** TeacherDashboardResponse */ /** TeacherDashboardResponse */
TeacherDashboardResponse: { TeacherDashboardResponse: {
@ -3418,8 +3438,11 @@ export interface components {
session_id: string; session_id: string;
/** Session No */ /** Session No */
session_no: number; session_no: number;
/** Stage */ /**
stage: string; * Stage
* @enum {string}
*/
stage: "라포" | "탐색" | "개입" | "정리";
/** Started At */ /** Started At */
started_at: string; started_at: string;
/** /**
@ -3511,6 +3534,10 @@ export interface components {
* @enum {string} * @enum {string}
*/ */
status: "viewed" | "closed"; status: "viewed" | "closed";
/** Worksheet Note */
worksheet_note?: string | null;
/** Worksheet Status */
worksheet_status?: ("approved" | "changes_requested" | "rejected" | "pending") | null;
}; };
/** TeacherSessionReviewStatusResponse */ /** TeacherSessionReviewStatusResponse */
TeacherSessionReviewStatusResponse: { TeacherSessionReviewStatusResponse: {
@ -3533,6 +3560,19 @@ export interface components {
status: "pending" | "viewed" | "closed"; status: "pending" | "viewed" | "closed";
/** Updated At */ /** Updated At */
updated_at?: string | null; updated_at?: string | null;
/**
* Worksheet Note
* @default
*/
worksheet_note: string;
/** Worksheet Reviewed At */
worksheet_reviewed_at?: string | null;
/**
* Worksheet Status
* @default pending
* @enum {string}
*/
worksheet_status: "pending" | "approved" | "changes_requested" | "rejected";
}; };
/** TeacherSessionSummary */ /** TeacherSessionSummary */
TeacherSessionSummary: { TeacherSessionSummary: {
@ -3564,8 +3604,11 @@ export interface components {
session_id: string; session_id: string;
/** Session No */ /** Session No */
session_no: number; session_no: number;
/** Stage */ /**
stage: string; * Stage
* @enum {string}
*/
stage: "라포" | "탐색" | "개입" | "정리";
/** Started At */ /** Started At */
started_at: string; started_at: string;
/** Status */ /** Status */
@ -3610,17 +3653,8 @@ export interface components {
/** Rationale */ /** Rationale */
rationale?: string | null; rationale?: string | null;
}; };
/** /** TurnEvaluationResponse */
* TurnEvaluation TurnEvaluationResponse: {
* @description fast-loop ( 4).
*
* 4:
* technique[] : () ()
* client_state_read[] : ()
* appropriateness : 적절성 pos|warn|neutral ()
* intent_deviation : '의도와 다른 부분' ( None)
*/
TurnEvaluation: {
/** /**
* Appropriateness * Appropriateness
* @default neutral * @default neutral
@ -3640,8 +3674,11 @@ export interface components {
loop: string; loop: string;
/** Rapport Signal */ /** Rapport Signal */
rapport_signal?: number | null; rapport_signal?: number | null;
/** Stage */ /**
stage: string; * Stage
* @enum {string}
*/
stage: "라포" | "탐색" | "개입" | "정리";
/** Techniques */ /** Techniques */
techniques?: components["schemas"]["TechniqueTag"][]; techniques?: components["schemas"]["TechniqueTag"][];
/** Theory Mode */ /** Theory Mode */
@ -4791,7 +4828,7 @@ export interface operations {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["SessionEvaluation"]; "application/json": components["schemas"]["SessionEvaluationResponse"];
}; };
}; };
/** @description Validation Error */ /** @description Validation Error */
@ -4829,7 +4866,7 @@ export interface operations {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["TurnEvaluation"]; "application/json": components["schemas"]["TurnEvaluationResponse"];
}; };
}; };
/** @description Validation Error */ /** @description Validation Error */

View file

@ -1611,6 +1611,33 @@ export default function Session() {
sending; sending;
const textTurnBlocked = sending || voiceStatus === "thinking"; const textTurnBlocked = sending || voiceStatus === "thinking";
const elapsedLabel = formatElapsed(elapsed); const elapsedLabel = formatElapsed(elapsed);
const recommendedSessionSeconds = 30 * 60;
const remainingLabel = formatElapsed(Math.max(0, recommendedSessionSeconds - elapsed));
const turnCount = utterances.filter((utterance) => !utterance.partial).length;
const latestClientUtterance = [...utterances]
.reverse()
.find((utterance) => utterance.speaker === "client" && utterance.text.trim());
const stageClientLine =
latestClientUtterance?.text ??
primaryContext?.v ??
`${clientName}님이 당신의 첫 질문을 기다리고 있습니다.`;
const voiceInputStatus =
voiceAvailable === false
? "음성 미설정"
: micOn
? "수신 중"
: voiceStatus === "requesting"
? "권한 요청"
: "대기";
const responseStatus =
sending || clientReplyPending
? "응답 대기"
: voiceStatus === "thinking"
? "전사 중"
: voiceStatus === "speaking"
? "재생 중"
: "안정";
const safetyStatusText = safety ? "확인 필요" : "안전";
const personaIsUsable = personaSummary ? isUsablePersona(personaSummary) : false; const personaIsUsable = personaSummary ? isUsablePersona(personaSummary) : false;
const consentRequired = user?.role === "learner" && user.consentAt == null; const consentRequired = user?.role === "learner" && user.consentAt == null;
const canStartSession = personaLoadState === "ready" && personaIsUsable && !consentRequired; const canStartSession = personaLoadState === "ready" && personaIsUsable && !consentRequired;
@ -2370,23 +2397,20 @@ export default function Session() {
</div> </div>
</section> </section>
{/* 안전 점검(있을 때만, warn 톤 — 빨강 아님) */} <section className={"sx-safety" + (safety ? "" : " sx-safety--ok")}>
{safety ? ( <span className="sx-safety__ico">
<section className="sx-safety"> <Icon name={safety ? "alert" : "shield"} size={17} />
<span className="sx-safety__ico"> </span>
<Icon name="alert" size={17} /> <span className="sx-safety__text">
</span> <b> </b> · {safety ?? "현재 감지된 위기 신호 없음"}
<span className="sx-safety__text"> {crisisResource ? (
<b> </b> · {safety} <span className="sx-crisis-resource">
{crisisResource ? ( <strong>{crisisResource.title}</strong>
<span className="sx-crisis-resource"> <a href={`tel:${crisisResource.number}`}>{crisisResource.number}</a>
<strong>{crisisResource.title}</strong> </span>
<a href={`tel:${crisisResource.number}`}>{crisisResource.number}</a> ) : null}
</span> </span>
) : null} </section>
</span>
</section>
) : null}
</div> </div>
</div> </div>
@ -2530,7 +2554,7 @@ export default function Session() {
} }
> >
<div className="sx-coach-history__item-top"> <div className="sx-coach-history__item-top">
<span>{event.turn_seq} · {event.stage}</span> <span>{event.turn_seq} · {event.stage ?? "단계 미상"}</span>
<time dateTime={event.created_at}> <time dateTime={event.created_at}>
{formatCoachTimestamp(event.created_at)} {formatCoachTimestamp(event.created_at)}
</time> </time>

View file

@ -31,6 +31,8 @@ import "./session-review/session-review.css";
type LoadState = "loading" | "ready" | "error"; type LoadState = "loading" | "ready" | "error";
type PrepostMeasureName = UserPrepostMeasureItem["measure_name"]; type PrepostMeasureName = UserPrepostMeasureItem["measure_name"];
type PrepostTimepoint = UserPrepostMeasureItem["timepoint"]; type PrepostTimepoint = UserPrepostMeasureItem["timepoint"];
type WorksheetReviewStatus = "pending" | "approved" | "changes_requested" | "rejected";
type WorksheetDecisionStatus = Exclude<WorksheetReviewStatus, "pending">;
const PREPOST_PILOT_ID = "phase3-pilot-draft"; const PREPOST_PILOT_ID = "phase3-pilot-draft";
const PREPOST_INSTRUMENT_VERSION = "pilot-prepost-scaffold-2026-06-28"; const PREPOST_INSTRUMENT_VERSION = "pilot-prepost-scaffold-2026-06-28";
@ -52,6 +54,13 @@ function prepostDraftKey(measure: PrepostMeasureName, timepoint: PrepostTimepoin
return `${measure}:${timepoint}`; return `${measure}:${timepoint}`;
} }
function worksheetReviewStatusLabel(status: WorksheetReviewStatus) {
if (status === "approved") return "승인";
if (status === "changes_requested") return "수정요청";
if (status === "rejected") return "반려";
return "검수 대기";
}
function splitPrepostDraftKey(key: string): [PrepostMeasureName, PrepostTimepoint] | null { function splitPrepostDraftKey(key: string): [PrepostMeasureName, PrepostTimepoint] | null {
const [measure, timepoint] = key.split(":"); const [measure, timepoint] = key.split(":");
if ( if (
@ -667,7 +676,10 @@ export default function SessionReview() {
const [shareUrl, setShareUrl] = useState<string | null>(null); const [shareUrl, setShareUrl] = useState<string | null>(null);
const [shareError, setShareError] = useState<string | null>(null); const [shareError, setShareError] = useState<string | null>(null);
const [teacherNote, setTeacherNote] = useState(""); const [teacherNote, setTeacherNote] = useState("");
const [teacherWorksheetNote, setTeacherWorksheetNote] = useState("");
const [teacherReviewSaving, setTeacherReviewSaving] = useState<"viewed" | "closed" | null>(null); const [teacherReviewSaving, setTeacherReviewSaving] = useState<"viewed" | "closed" | null>(null);
const [teacherWorksheetSaving, setTeacherWorksheetSaving] =
useState<WorksheetDecisionStatus | null>(null);
const [teacherReviewError, setTeacherReviewError] = useState<string | null>(null); const [teacherReviewError, setTeacherReviewError] = useState<string | null>(null);
const turnRefs = useRef<Record<string, HTMLDivElement | null>>({}); const turnRefs = useRef<Record<string, HTMLDivElement | null>>({});
@ -696,6 +708,7 @@ export default function SessionReview() {
if (!alive) return; if (!alive) return;
setData(next); setData(next);
setTeacherNote(next.teacherReview?.note ?? ""); setTeacherNote(next.teacherReview?.note ?? "");
setTeacherWorksheetNote(next.teacherReview?.worksheetNote ?? "");
setTeacherReviewError(null); setTeacherReviewError(null);
setLoadState("ready"); setLoadState("ready");
} catch (err) { } catch (err) {
@ -762,6 +775,7 @@ export default function SessionReview() {
const saved = await teacherApi.updateSessionReviewStatus(sessionId, { const saved = await teacherApi.updateSessionReviewStatus(sessionId, {
status, status,
note: teacherNote, note: teacherNote,
worksheet_note: teacherWorksheetNote,
}); });
setData((current) => setData((current) =>
current current
@ -773,11 +787,15 @@ export default function SessionReview() {
reviewerId: saved.reviewer_id ?? null, reviewerId: saved.reviewer_id ?? null,
reviewedAt: saved.reviewed_at ?? null, reviewedAt: saved.reviewed_at ?? null,
updatedAt: saved.updated_at ?? null, updatedAt: saved.updated_at ?? null,
worksheetStatus: saved.worksheet_status ?? "pending",
worksheetNote: saved.worksheet_note ?? "",
worksheetReviewedAt: saved.worksheet_reviewed_at ?? null,
}, },
} }
: current, : current,
); );
setTeacherNote(saved.note); setTeacherNote(saved.note);
setTeacherWorksheetNote(saved.worksheet_note ?? "");
} catch (err) { } catch (err) {
setTeacherReviewError( setTeacherReviewError(
err instanceof Error ? err.message : "교수자 검토 상태를 저장하지 못했습니다.", err instanceof Error ? err.message : "교수자 검토 상태를 저장하지 못했습니다.",
@ -787,6 +805,45 @@ export default function SessionReview() {
} }
} }
async function saveWorksheetReviewStatus(status: WorksheetDecisionStatus) {
if (!sessionId || !data) return;
setTeacherWorksheetSaving(status);
setTeacherReviewError(null);
try {
const saved = await teacherApi.updateSessionReviewStatus(sessionId, {
status: teacherReviewStatus === "closed" ? "closed" : "viewed",
note: teacherNote,
worksheet_status: status,
worksheet_note: teacherWorksheetNote,
});
setData((current) =>
current
? {
...current,
teacherReview: {
status: saved.status,
note: saved.note,
reviewerId: saved.reviewer_id ?? null,
reviewedAt: saved.reviewed_at ?? null,
updatedAt: saved.updated_at ?? null,
worksheetStatus: saved.worksheet_status ?? "pending",
worksheetNote: saved.worksheet_note ?? "",
worksheetReviewedAt: saved.worksheet_reviewed_at ?? null,
},
}
: current,
);
setTeacherNote(saved.note);
setTeacherWorksheetNote(saved.worksheet_note ?? "");
} catch (err) {
setTeacherReviewError(
err instanceof Error ? err.message : "워크시트 검수 상태를 저장하지 못했습니다.",
);
} finally {
setTeacherWorksheetSaving(null);
}
}
if (loadState === "loading") { if (loadState === "loading") {
return ( return (
<AppShell contextLabel={contextLabel} navRole={reviewNavRole}> <AppShell contextLabel={contextLabel} navRole={reviewNavRole}>
@ -838,7 +895,11 @@ export default function SessionReview() {
: teacherReviewStatus === "viewed" : teacherReviewStatus === "viewed"
? "검토 중" ? "검토 중"
: "검토 대기"; : "검토 대기";
const worksheetReviewStatus = (teacherReview?.worksheetStatus ?? "pending") as WorksheetReviewStatus;
const worksheetReviewLabel = worksheetReviewStatusLabel(worksheetReviewStatus);
const hasLearnerWorksheet = data.caseWorksheet?.status === "saved_by_learner";
const canCloseTeacherReview = isSupervisorView && data.sessionSignal === "종료됨"; const canCloseTeacherReview = isSupervisorView && data.sessionSignal === "종료됨";
const canDecideWorksheet = canCloseTeacherReview && hasLearnerWorksheet;
const reviewReadiness = hasTranscript const reviewReadiness = hasTranscript
? `${turns.length}개 발화 기반` ? `${turns.length}개 발화 기반`
: "축어록 저장 후 생성"; : "축어록 저장 후 생성";
@ -1147,7 +1208,7 @@ export default function SessionReview() {
<Button <Button
variant="secondary" variant="secondary"
size="sm" size="sm"
disabled={teacherReviewSaving !== null} disabled={teacherReviewSaving !== null || teacherWorksheetSaving !== null}
onClick={() => void saveTeacherReviewStatus("viewed")} onClick={() => void saveTeacherReviewStatus("viewed")}
> >
{teacherReviewSaving === "viewed" ? "저장 중" : "메모 저장"} {teacherReviewSaving === "viewed" ? "저장 중" : "메모 저장"}
@ -1157,6 +1218,7 @@ export default function SessionReview() {
disabled={ disabled={
!canCloseTeacherReview || !canCloseTeacherReview ||
teacherReviewSaving !== null || teacherReviewSaving !== null ||
teacherWorksheetSaving !== null ||
teacherReviewStatus === "closed" teacherReviewStatus === "closed"
} }
onClick={() => void saveTeacherReviewStatus("closed")} onClick={() => void saveTeacherReviewStatus("closed")}
@ -1164,6 +1226,65 @@ export default function SessionReview() {
{teacherReviewSaving === "closed" ? "완료 중" : "검토 완료"} {teacherReviewSaving === "closed" ? "완료 중" : "검토 완료"}
</Button> </Button>
</div> </div>
<div className="sr-teacher-review__worksheet">
<div className="sr-teacher-review__head sr-teacher-review__head--sub">
<div>
<Kicker> </Kicker>
<b>{worksheetReviewLabel}</b>
</div>
<Badge tone={worksheetReviewStatus === "approved" ? "accent" : "neutral"}>
{worksheetReviewLabel}
</Badge>
</div>
<label className="sr-teacher-review__note">
<span> </span>
<textarea
value={teacherWorksheetNote}
rows={3}
placeholder="수정요청이나 반려 사유를 간단히 남깁니다."
onChange={(event) => setTeacherWorksheetNote(event.target.value)}
maxLength={2000}
/>
</label>
{teacherReview?.worksheetReviewedAt ? (
<p className="sr-teacher-review__meta">
{teacherReview.worksheetReviewedAt}
</p>
) : hasLearnerWorksheet ? (
<p className="sr-teacher-review__meta">
.
</p>
) : (
<p className="sr-teacher-review__meta">
·· .
</p>
)}
<div className="sr-teacher-review__actions sr-teacher-review__actions--three">
<Button
size="sm"
disabled={!canDecideWorksheet || teacherWorksheetSaving !== null}
onClick={() => void saveWorksheetReviewStatus("approved")}
>
{teacherWorksheetSaving === "approved" ? "저장 중" : "승인"}
</Button>
<Button
variant="secondary"
size="sm"
disabled={!canDecideWorksheet || teacherWorksheetSaving !== null}
onClick={() => void saveWorksheetReviewStatus("changes_requested")}
>
{teacherWorksheetSaving === "changes_requested" ? "저장 중" : "수정요청"}
</Button>
<Button
variant="secondary"
size="sm"
disabled={!canDecideWorksheet || teacherWorksheetSaving !== null}
onClick={() => void saveWorksheetReviewStatus("rejected")}
>
{teacherWorksheetSaving === "rejected" ? "저장 중" : "반려"}
</Button>
</div>
</div>
</Card> </Card>
) : null} ) : null}

View file

@ -263,6 +263,14 @@
font-size: var(--fs-body); font-size: var(--fs-body);
line-height: 1.35; line-height: 1.35;
} }
.sr-teacher-review__head--sub {
margin-bottom: var(--sp-2);
}
.sr-teacher-review__worksheet {
margin-top: var(--sp-4);
padding-top: var(--sp-4);
border-top: 1px solid var(--border-subtle);
}
.sr-teacher-review__note { .sr-teacher-review__note {
display: grid; display: grid;
gap: 7px; gap: 7px;
@ -311,6 +319,9 @@
.sr-teacher-review__actions .vg-btn { .sr-teacher-review__actions .vg-btn {
width: 100%; width: 100%;
} }
.sr-teacher-review__actions--three {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.sr-summary { .sr-summary {
max-width: 68ch; max-width: 68ch;

View file

@ -40,16 +40,36 @@
padding: 0; padding: 0;
} }
.sx-page--active { .sx-page--active {
--paper-2: #1b2b27;
--bg-surface: #14231f;
--bg-surface-2: #1d2d29;
--bg-stage: #0c1714;
--text-strong: #edf7f4;
--text-body: #c9d9d5;
--text-muted: #8fa29c;
--text-on-accent: #071411;
--border-subtle: rgba(169, 215, 204, 0.15);
--border-strong: rgba(169, 215, 204, 0.24);
--border-focus: #7cc8b8;
--focus-ring: rgba(124, 200, 184, 0.32);
--accent: #79bfae;
--accent-deep: #9ad3c7;
--accent-tint: rgba(121, 191, 174, 0.16);
--accent-bright: #79c8b7;
--clay: #d08b70;
--clay-deep: #ebb196;
--clay-tint: rgba(208, 139, 112, 0.15);
height: 100vh; height: 100vh;
height: 100dvh; height: 100dvh;
grid-template-rows: min-content minmax(0, 1fr) min-content; grid-template-rows: min-content minmax(0, 1fr) min-content;
padding: 10px 14px 14px; padding: 10px 14px 14px;
gap: 10px; gap: 10px;
background: background:
radial-gradient(circle at 16% 14%, rgba(95, 150, 139, 0.2), transparent 27%), radial-gradient(circle at 18% 8%, rgba(121, 191, 174, 0.12), transparent 28%),
radial-gradient(circle at 82% 8%, rgba(176, 115, 92, 0.18), transparent 24%), radial-gradient(circle at 86% 12%, rgba(208, 139, 112, 0.1), transparent 24%),
linear-gradient(135deg, #111c1a, #1d2926 52%, #15211f); linear-gradient(135deg, #0a1210 0%, #0e1714 48%, #101d19 100%);
color: #eaf0f1; color: var(--text-strong);
color-scheme: dark;
} }
.sx-page--active .sx-grid { .sx-page--active .sx-grid {
height: auto; height: auto;
@ -69,11 +89,11 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 10px;
border: 1px solid rgba(255, 255, 255, 0.11); border: 1px solid rgba(169, 215, 204, 0.16);
border-radius: var(--radius); border-radius: var(--radius);
background: rgba(16, 28, 26, 0.9); background: rgba(13, 24, 21, 0.92);
color: rgba(238, 244, 242, 0.88); color: rgba(238, 244, 242, 0.88);
box-shadow: 0 10px 30px rgba(7, 16, 14, 0.18); box-shadow: 0 12px 34px rgba(4, 12, 10, 0.26);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
min-width: 0; min-width: 0;
} }
@ -312,13 +332,12 @@
.sx-page--active .sx-panel, .sx-page--active .sx-panel,
.sx-page--active .sx-mobile-context { .sx-page--active .sx-mobile-context {
background: background:
linear-gradient(180deg, rgba(20, 35, 31, 0.92), rgba(13, 25, 22, 0.94)), linear-gradient(180deg, rgba(23, 39, 35, 0.94), rgba(13, 25, 22, 0.96));
var(--asset-warm-elements) center / cover no-repeat; border-color: var(--border-subtle);
border-color: rgba(203, 227, 220, 0.13);
box-shadow: box-shadow:
0 18px 42px rgba(4, 10, 9, 0.24), 0 18px 42px rgba(2, 8, 7, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.04); inset 0 1px 0 rgba(237, 247, 244, 0.045);
color: rgba(234, 240, 241, 0.78); color: var(--text-body);
} }
/* 컬럼 공통 */ /* 컬럼 공통 */
@ -337,7 +356,7 @@
min-height: 0; min-height: 0;
} }
.sx-page--active .sx-col-center { .sx-page--active .sx-col-center {
grid-template-rows: minmax(230px, 0.42fr) minmax(0, 1fr); grid-template-rows: minmax(250px, 0.44fr) minmax(0, 1fr);
} }
/* ── LEFT: 세로 단계 트랙 ── */ /* ── LEFT: 세로 단계 트랙 ── */
@ -544,11 +563,13 @@
} }
.sx-page--active .sx-stage { .sx-page--active .sx-stage {
background: background:
radial-gradient(circle at 28% 50%, rgba(197, 222, 214, 0.26), transparent 42%), radial-gradient(circle at 50% 47%, rgba(121, 191, 174, 0.22), transparent 36%),
linear-gradient(135deg, rgba(24, 42, 38, 0.94), rgba(48, 65, 59, 0.9)), radial-gradient(circle at 50% 82%, rgba(208, 139, 112, 0.12), transparent 24%),
var(--asset-warm-elements) center / cover no-repeat; linear-gradient(145deg, rgba(11, 22, 19, 0.98), rgba(20, 34, 30, 0.96) 56%, rgba(10, 19, 17, 0.98));
border-color: rgba(255, 255, 255, 0.11); border-color: rgba(169, 215, 204, 0.18);
box-shadow: 0 18px 44px rgba(7, 16, 14, 0.28); box-shadow:
0 22px 52px rgba(2, 8, 7, 0.42),
inset 0 1px 0 rgba(237, 247, 244, 0.05);
grid-template-columns: minmax(184px, 256px) minmax(0, 1fr); grid-template-columns: minmax(184px, 256px) minmax(0, 1fr);
padding: 20px 24px; padding: 20px 24px;
} }
@ -556,7 +577,7 @@
content: ""; content: "";
position: absolute; position: absolute;
inset: 12px; inset: 12px;
border: 1px solid rgba(255, 255, 255, 0.07); border: 1px solid rgba(169, 215, 204, 0.1);
border-radius: calc(var(--radius-lg) - 2px); border-radius: calc(var(--radius-lg) - 2px);
pointer-events: none; pointer-events: none;
} }
@ -571,8 +592,8 @@
gap: 8px; gap: 8px;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: var(--text-on-accent); color: var(--text-muted);
opacity: 0.72; opacity: 1;
} }
.sx-stage__status { .sx-stage__status {
min-width: 0; min-width: 0;
@ -594,12 +615,15 @@
border-radius: 50%; border-radius: 50%;
flex: none; flex: none;
background: var(--clay); background: var(--clay);
box-shadow: 0 0 0 4px rgba(208, 139, 112, 0.12);
} }
.sx-stage__state-dot.is-learner { .sx-stage__state-dot.is-learner {
background: var(--accent-bright); background: var(--accent-bright);
box-shadow: 0 0 0 4px rgba(121, 200, 183, 0.12);
} }
.sx-stage__state-dot.is-think { .sx-stage__state-dot.is-think {
background: var(--neutral-sig); background: var(--neutral-sig);
box-shadow: none;
} }
/* 음성 오브 + 아바타 (오브는 아바타 둘레 링) */ /* 음성 오브 + 아바타 (오브는 아바타 둘레 링) */
@ -620,8 +644,8 @@
pointer-events: none; pointer-events: none;
--ring: var(--accent-bright); --ring: var(--accent-bright);
box-shadow: box-shadow:
0 0 0 2px color-mix(in srgb, var(--ring) 55%, transparent), 0 0 0 1px color-mix(in srgb, var(--ring) 58%, transparent),
0 0 26px 3px color-mix(in srgb, var(--ring) 28%, transparent); 0 0 30px 2px color-mix(in srgb, var(--ring) 22%, transparent);
transition: box-shadow var(--dur-slow) var(--ease-out); transition: box-shadow var(--dur-slow) var(--ease-out);
} }
.sx-orb[data-orb="learner"] { .sx-orb[data-orb="learner"] {
@ -632,9 +656,9 @@
--ring: var(--clay); --ring: var(--clay);
/* 음량(--amp)을 JS 가 주입 → blur 미세 모듈레이션 */ /* 음량(--amp)을 JS 가 주입 → blur 미세 모듈레이션 */
box-shadow: box-shadow:
0 0 0 2px color-mix(in srgb, var(--ring) 60%, transparent), 0 0 0 1px color-mix(in srgb, var(--ring) 62%, transparent),
0 0 calc(22px + var(--amp, 0) * 18px) 3px 0 0 calc(24px + var(--amp, 0) * 18px) 2px
color-mix(in srgb, var(--ring) 32%, transparent); color-mix(in srgb, var(--ring) 28%, transparent);
} }
.sx-orb[data-orb="thinking"] { .sx-orb[data-orb="thinking"] {
--ring: var(--neutral-sig); --ring: var(--neutral-sig);
@ -659,8 +683,8 @@
grid-row: 2; grid-row: 2;
font-size: 15px; font-size: 15px;
font-weight: 650; font-weight: 650;
color: var(--text-on-accent); color: var(--text-strong);
opacity: 0.88; opacity: 0.9;
margin-top: 0; margin-top: 0;
text-align: left; text-align: left;
line-height: 1.45; line-height: 1.45;
@ -725,10 +749,10 @@
width: min(100%, 520px); width: min(100%, 520px);
max-width: calc(100% - 48px); max-width: calc(100% - 48px);
padding: 7px 10px; padding: 7px 10px;
border: 1px solid rgba(203, 227, 220, 0.12); border: 1px solid rgba(169, 215, 204, 0.15);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: rgba(8, 19, 17, 0.42); background: rgba(7, 18, 15, 0.62);
color: rgba(234, 240, 241, 0.82); color: var(--text-strong);
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@ -764,12 +788,11 @@
} }
.sx-page--active .sx-transcript { .sx-page--active .sx-transcript {
background: background:
linear-gradient(180deg, rgba(20, 35, 31, 0.94), rgba(12, 23, 20, 0.96)), linear-gradient(180deg, rgba(19, 34, 30, 0.97), rgba(10, 22, 19, 0.98));
var(--asset-warm-elements) center / cover no-repeat; border-color: rgba(169, 215, 204, 0.16);
border-color: rgba(203, 227, 220, 0.13);
box-shadow: box-shadow:
0 18px 42px rgba(4, 10, 9, 0.28), 0 18px 44px rgba(2, 8, 7, 0.34),
inset 0 1px 0 rgba(255, 255, 255, 0.04); inset 0 1px 0 rgba(237, 247, 244, 0.04);
color: var(--text-body); color: var(--text-body);
} }
.sx-transcript__head { .sx-transcript__head {
@ -1047,6 +1070,11 @@
outline: none; outline: none;
box-shadow: 0 0 0 3px var(--focus-ring); box-shadow: 0 0 0 3px var(--focus-ring);
} }
.sx-page--active button:focus-visible,
.sx-page--active textarea:focus-visible {
outline: 2px solid rgba(124, 200, 184, 0.88);
outline-offset: 2px;
}
.sx-turn-error { .sx-turn-error {
margin-top: 10px; margin-top: 10px;
} }
@ -1085,6 +1113,7 @@
gap: 10px; gap: 10px;
padding: 10px 12px; padding: 10px 12px;
background: var(--paper-2); background: var(--paper-2);
border: 1px solid transparent;
border-radius: var(--radius); border-radius: var(--radius);
transition: opacity var(--dur-slow) var(--ease-out); transition: opacity var(--dur-slow) var(--ease-out);
} }
@ -1097,6 +1126,7 @@
padding: 12px; padding: 12px;
border-radius: var(--radius); border-radius: var(--radius);
background: var(--paper-2); background: var(--paper-2);
border: 1px solid transparent;
color: var(--text-muted); color: var(--text-muted);
font-size: 12.5px; font-size: 12.5px;
line-height: 1.5; line-height: 1.5;
@ -1516,6 +1546,7 @@
gap: 10px; gap: 10px;
padding: 12px; padding: 12px;
background: var(--warn-tint); background: var(--warn-tint);
border: 1px solid color-mix(in srgb, var(--warn-solid) 24%, transparent);
border-radius: var(--radius); border-radius: var(--radius);
} }
.sx-safety + .sx-safety { .sx-safety + .sx-safety {
@ -1528,6 +1559,14 @@
.sx-safety--engine .sx-safety__text { .sx-safety--engine .sx-safety__text {
color: var(--info-text); color: var(--info-text);
} }
.sx-safety--ok {
background: var(--accent-tint);
border-color: rgba(121, 200, 183, 0.2);
}
.sx-safety--ok .sx-safety__ico,
.sx-safety--ok .sx-safety__text {
color: #9ed6ca;
}
.sx-safety__ico { .sx-safety__ico {
flex: none; flex: none;
width: 17px; width: 17px;
@ -1581,9 +1620,11 @@
.sx-page--active .sx-controlbar { .sx-page--active .sx-controlbar {
width: min(100%, 1460px); width: min(100%, 1460px);
margin: 0 auto; margin: 0 auto;
background: rgba(16, 28, 26, 0.94); background: rgba(11, 22, 19, 0.96);
border-color: rgba(255, 255, 255, 0.12); border-color: rgba(169, 215, 204, 0.16);
box-shadow: 0 14px 36px rgba(7, 16, 14, 0.24); box-shadow:
0 16px 40px rgba(2, 8, 7, 0.34),
inset 0 1px 0 rgba(237, 247, 244, 0.04);
color: rgba(238, 244, 242, 0.86); color: rgba(238, 244, 242, 0.86);
} }
.sx-page--active .sx-mic-block__l, .sx-page--active .sx-mic-block__l,
@ -1594,30 +1635,30 @@
color: rgba(238, 244, 242, 0.58); color: rgba(238, 244, 242, 0.58);
} }
.sx-page--active .sx-segmented { .sx-page--active .sx-segmented {
background: rgba(255, 255, 255, 0.08); background: rgba(237, 247, 244, 0.07);
border-color: rgba(255, 255, 255, 0.1); border: 1px solid rgba(169, 215, 204, 0.1);
} }
.sx-page--active .sx-segmented button { .sx-page--active .sx-segmented button {
color: rgba(238, 244, 242, 0.68); color: rgba(238, 244, 242, 0.68);
} }
.sx-page--active .sx-segmented button:hover { .sx-page--active .sx-segmented button:hover {
color: #ffffff; color: var(--text-strong);
background: rgba(255, 255, 255, 0.08); background: rgba(237, 247, 244, 0.08);
} }
.sx-page--active .sx-segmented button.is-on { .sx-page--active .sx-segmented button.is-on {
background: rgba(145, 200, 189, 0.2); background: rgba(121, 200, 183, 0.24);
color: #ffffff; color: var(--text-strong);
} }
.sx-page--active .sx-cb-sep { .sx-page--active .sx-cb-sep {
background: rgba(255, 255, 255, 0.12); background: rgba(255, 255, 255, 0.12);
} }
.sx-page--active .sx-pause { .sx-page--active .sx-pause {
background: rgba(255, 255, 255, 0.08); background: rgba(237, 247, 244, 0.07);
border-color: rgba(255, 255, 255, 0.12); border-color: rgba(169, 215, 204, 0.14);
color: #eef4f2; color: #eef4f2;
} }
.sx-page--active .sx-pause:hover { .sx-page--active .sx-pause:hover {
background: rgba(255, 255, 255, 0.12); background: rgba(237, 247, 244, 0.11);
} }
.sx-page--active .sx-review-button { .sx-page--active .sx-review-button {
background: rgba(145, 200, 189, 0.14); background: rgba(145, 200, 189, 0.14);
@ -1640,7 +1681,8 @@
.sx-page--active .sx-signal__rest, .sx-page--active .sx-signal__rest,
.sx-page--active .sx-mobile-context__row span, .sx-page--active .sx-mobile-context__row span,
.sx-page--active .sx-mobile-context__brief span { .sx-page--active .sx-mobile-context__brief span {
background: rgba(255, 255, 255, 0.055); background: rgba(237, 247, 244, 0.055);
border-color: rgba(169, 215, 204, 0.09);
color: rgba(234, 240, 241, 0.76); color: rgba(234, 240, 241, 0.76);
} }
.sx-page--active .sx-chip.is-clay, .sx-page--active .sx-chip.is-clay,
@ -1688,17 +1730,26 @@
color: #9ed1c6; color: #9ed1c6;
} }
.sx-page--active .sx-compose textarea { .sx-page--active .sx-compose textarea {
background: rgba(255, 255, 255, 0.055); background: rgba(7, 18, 15, 0.58);
color: #eef6f3; color: #eef6f3;
border-color: rgba(203, 227, 220, 0.18); border-color: rgba(169, 215, 204, 0.22);
} }
.sx-page--active .sx-compose textarea::placeholder { .sx-page--active .sx-compose textarea::placeholder {
color: rgba(234, 240, 241, 0.36); color: rgba(234, 240, 241, 0.36);
} }
.sx-page--active .sx-compose .vg-btn--primary:disabled { .sx-page--active .sx-compose .vg-btn--primary:disabled {
background: rgba(255, 255, 255, 0.055); background: rgba(237, 247, 244, 0.055);
color: rgba(234, 240, 241, 0.38); color: rgba(234, 240, 241, 0.38);
border-color: rgba(203, 227, 220, 0.14); border-color: rgba(169, 215, 204, 0.14);
}
.sx-page--active .sx-compose .vg-btn--primary:not(:disabled) {
background: var(--accent);
color: var(--text-on-accent);
border-color: transparent;
box-shadow: 0 0 0 1px rgba(237, 247, 244, 0.08), 0 10px 22px rgba(3, 12, 10, 0.24);
}
.sx-page--active .sx-compose .vg-btn--primary:hover:not(:disabled) {
background: #8ad1c2;
} }
/* 마이크 (주 컨트롤, 음성 호흡 펄스) — 원형 예외 허용 */ /* 마이크 (주 컨트롤, 음성 호흡 펄스) — 원형 예외 허용 */
.sx-mic-block { .sx-mic-block {
@ -1819,7 +1870,10 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
transition: all var(--dur-fast) var(--ease-out); transition:
background var(--dur-fast) var(--ease-out),
border-color var(--dur-fast) var(--ease-out),
color var(--dur-fast) var(--ease-out);
} }
.sx-pause:hover { .sx-pause:hover {
background: var(--bg-surface-2); background: var(--bg-surface-2);
@ -1846,7 +1900,10 @@
justify-content: center; justify-content: center;
gap: 8px; gap: 8px;
white-space: nowrap; white-space: nowrap;
transition: all var(--dur-fast) var(--ease-out); transition:
background var(--dur-fast) var(--ease-out),
border-color var(--dur-fast) var(--ease-out),
color var(--dur-fast) var(--ease-out);
} }
.sx-review-button:hover { .sx-review-button:hover {
background: color-mix(in srgb, var(--accent-tint) 80%, var(--bg-surface)); background: color-mix(in srgb, var(--accent-tint) 80%, var(--bg-surface));
@ -1886,7 +1943,9 @@
padding: 7px 12px; padding: 7px 12px;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
transition: all var(--dur-fast) var(--ease-out); transition:
background var(--dur-fast) var(--ease-out),
color var(--dur-fast) var(--ease-out);
white-space: nowrap; white-space: nowrap;
} }
.sx-segmented button:hover { .sx-segmented button:hover {
@ -2629,7 +2688,7 @@
.sx-page--active .sx-col-center { .sx-page--active .sx-col-center {
grid-column: 2; grid-column: 2;
grid-row: 2; grid-row: 2;
grid-template-rows: minmax(142px, 0.26fr) minmax(0, 1fr); grid-template-rows: minmax(154px, 0.28fr) minmax(0, 1fr);
gap: 10px; gap: 10px;
} }
.sx-page--active .sx-col-right { .sx-page--active .sx-col-right {
@ -2703,7 +2762,7 @@
} }
.sx-page--active .sx-stage { .sx-page--active .sx-stage {
grid-template-columns: minmax(110px, 132px) minmax(0, 1fr); grid-template-columns: minmax(110px, 132px) minmax(0, 1fr);
padding: 10px 12px; padding: 12px 14px;
column-gap: 12px; column-gap: 12px;
} }
.sx-page--active .sx-stage .vg-avatar { .sx-page--active .sx-stage .vg-avatar {
@ -2821,7 +2880,7 @@
.sx-page--active .sx-col-center { .sx-page--active .sx-col-center {
grid-column: 1; grid-column: 1;
grid-row: 2; grid-row: 2;
grid-template-rows: minmax(118px, 0.22fr) minmax(0, 1fr); grid-template-rows: minmax(132px, 0.24fr) minmax(0, 1fr);
gap: 9px; gap: 9px;
width: 100%; width: 100%;
min-width: 0; min-width: 0;
@ -2834,7 +2893,7 @@
max-width: 100%; max-width: 100%;
} }
.sx-page--active .sx-stage { .sx-page--active .sx-stage {
padding: 6px 8px; padding: 8px 10px;
grid-template-columns: minmax(88px, 104px) minmax(0, 1fr); grid-template-columns: minmax(88px, 104px) minmax(0, 1fr);
column-gap: 10px; column-gap: 10px;
} }