import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { AppShell } from "../components/shell/AppShell";
import { Badge, Button, Card, Dot, Icon, Kicker } from "../components/ui";
import {
ApiError,
personaReviewApi,
teacherApi,
type PersonaReviewAction,
type PersonaReviewStatus,
type PersonaReviewSummary,
type TeacherLearnerGrowth,
type TeacherGrowthPoint,
type TeacherSafetyAlert,
type TeacherDashboardResponse,
} from "../lib/api";
type LoadState = "loading" | "ready" | "error";
function formatDateTime(value: string | null): string {
if (!value) return "-";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString("ko-KR", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
function formatScore(value: number | null | undefined): string {
if (typeof value !== "number" || Number.isNaN(value)) return "평가 부족";
return `${Math.round(value * 100)}%`;
}
function formatDelta(value: number | null | undefined): string {
if (typeof value !== "number" || Number.isNaN(value)) return "변화 부족";
const sign = value > 0 ? "+" : "";
return `${sign}${Math.round(value * 100)}%p`;
}
function trendLabel(value: string): string {
if (value === "up") return "상승";
if (value === "down") return "하락";
if (value === "flat") return "유지";
return "평가 부족";
}
function sessionOpenLabel(status: string): string {
return status === "ended" ? "상세 리뷰" : "진행 기록";
}
function sessionReviewStatusLabel(status: string | null | undefined): string {
if (status === "closed") return "검토 완료";
if (status === "viewed") return "메모 저장";
return "검토 대기";
}
function sessionReviewTone(status: string | null | undefined): "accent" | "neutral" | "warn" {
if (status === "closed") return "accent";
if (status === "viewed") return "warn";
return "neutral";
}
function personaReviewStatusLabel(status: PersonaReviewStatus): string {
if (status === "review") return "검수 대기";
if (status === "draft") return "수정 대기";
if (status === "approved") return "승인됨";
return "보관됨";
}
function personaReviewTone(status: PersonaReviewStatus): "accent" | "neutral" | "warn" {
if (status === "review") return "accent";
if (status === "draft") return "warn";
return "neutral";
}
function EmptyState({ title, desc }: { title: string; desc: string }) {
return (
{title}
{desc}
);
}
export default function Professor() {
const navigate = useNavigate();
const [dashboard, setDashboard] = useState(null);
const [personaReviews, setPersonaReviews] = useState([]);
const [loadState, setLoadState] = useState("loading");
const [error, setError] = useState(null);
const [personaReviewLoading, setPersonaReviewLoading] = useState(true);
const [personaReviewError, setPersonaReviewError] = useState(null);
const [personaDecisionBusy, setPersonaDecisionBusy] = useState(null);
const [updatedAt, setUpdatedAt] = useState(null);
const loadDashboard = useCallback(async () => {
setLoadState("loading");
setError(null);
try {
const next = await teacherApi.dashboard();
setDashboard(next);
setUpdatedAt(new Date());
setLoadState("ready");
} catch (err) {
setError(err instanceof Error ? err.message : "교수자 대시보드를 불러오지 못했습니다.");
setLoadState("error");
}
}, []);
const loadPersonaReviews = useCallback(async () => {
setPersonaReviewLoading(true);
setPersonaReviewError(null);
try {
const next = await personaReviewApi.list();
setPersonaReviews(next);
} catch (err) {
// 404(검수 엔드포인트 부재/검수 데이터 없음)는 장애가 아니므로
// 빨간 에러 배너 대신 정상 빈 상태로만 표시한다.
if (err instanceof ApiError && err.status === 404) {
setPersonaReviews([]);
} else {
// 진짜 장애(5xx/네트워크)일 때만 정돈된 에러 카드를 노출한다.
setPersonaReviewError("검수 데이터를 불러오지 못했습니다.");
}
} finally {
setPersonaReviewLoading(false);
}
}, []);
const decidePersonaReview = useCallback(
async (personaId: string, action: PersonaReviewAction) => {
setPersonaDecisionBusy(`${personaId}:${action}`);
setPersonaReviewError(null);
try {
const next = await personaReviewApi.decide(personaId, action);
setPersonaReviews((current) => {
if (next.status === "draft" || next.status === "review") {
return current.map((item) => (item.persona_id === next.persona_id ? next : item));
}
return current.filter((item) => item.persona_id !== next.persona_id);
});
} catch (err) {
console.warn("[professor] failed to decide persona review", err);
setPersonaReviewError("페르소나 검수 결정을 저장하지 못했습니다.");
} finally {
setPersonaDecisionBusy(null);
}
},
[],
);
useEffect(() => {
void loadDashboard();
void loadPersonaReviews();
}, [loadDashboard, loadPersonaReviews]);
const kpis = useMemo(
() => [
{
label: "검토 대기",
value: dashboard?.pending_reviews?.length ?? 0,
hint: "종료 회기",
icon: "review" as const,
emphasis: "primary" as const,
},
{
label: "위기 알림",
value: dashboard?.safety_alerts?.length ?? 0,
hint: "109 확인",
icon: "alert" as const,
emphasis: "warn" as const,
},
{
label: "페르소나 검수",
value: personaReviews.length,
hint: "승인 대기",
icon: "check" as const,
},
{
label: "최근 세션",
value: dashboard?.recent_sessions?.length ?? 0,
hint: "종료 · 진행",
icon: "play" as const,
},
{
label: "전체 학생",
value: dashboard?.total_learners ?? 0,
hint: "담당 코호트",
icon: "users" as const,
},
{
label: "활성 세션",
value: dashboard?.active_sessions ?? 0,
hint: "진행 중",
icon: "play" as const,
},
],
[dashboard, personaReviews.length],
);
const pendingCount = dashboard?.pending_reviews?.length ?? 0;
const hasPending = pendingCount > 0;
const totalSessions = (dashboard?.active_sessions ?? 0) + (dashboard?.ended_sessions ?? 0);
const personaReviewCount = personaReviews.length;
const safetyAlerts = dashboard?.safety_alerts ?? [];
const learnerGrowth = dashboard?.learner_growth ?? [];
const pendingReviews = dashboard?.pending_reviews ?? [];
const recentSessions = dashboard?.recent_sessions ?? [];
const firstPendingReview = pendingReviews[0];
return (
교수 콘솔
{hasPending ? `${pendingCount}건의 리뷰가 대기 중입니다.` : "검토할 실제 회기가 없습니다."}
현재 기록된 학습 세션을 기준으로 표시합니다. 기록이 없는 항목은 비워 두며,
임의 코호트나 추정 위험도는 보여주지 않습니다.
{firstPendingReview ? (
}
onClick={() => navigate(`/teach/session/${firstPendingReview.session_id}/review`)}
>
첫 검토 열기
) : null}
}
onClick={() => {
void loadDashboard();
void loadPersonaReviews();
}}
disabled={loadState === "loading" || personaReviewLoading}
>
{loadState === "loading" || personaReviewLoading ? "확인 중" : "새로고침"}
검토 큐
{dashboard?.message ?? "학습 세션을 확인하는 중입니다."}
{dashboard ? `${dashboard.cohort_label} · 실제 기록` : "확인 중"}
{updatedAt ? ` · ${updatedAt.toLocaleTimeString("ko-KR")} 갱신` : ""}
{pendingCount}
리뷰 대기
{totalSessions}
누적 회기
{kpis.map((kpi) => {
const emphasis = "emphasis" in kpi ? kpi.emphasis : null;
return (
{kpi.label}
{kpi.value}
{kpi.hint}
);
})}
{error ? (
) : null}
학습자 성장 추적
이력·항목별 추이
0 ? "accent" : "neutral"}>
{learnerGrowth.length}명
{learnerGrowth.length > 0 ? (
{learnerGrowth.map((learner) => (
))}
) : (
)}
검토 대기
아직 닫지 않은 종료 회기
{pendingReviews.length}건
{pendingReviews.length > 0 ? (
{pendingReviews.map((session) => (
))}
) : (
)}
위기 알림
109 안전 확인 큐
0 ? "warn" : "neutral"}>
{safetyAlerts.length}건
{safetyAlerts.length > 0 ? (
{safetyAlerts.map((alert) => (
))}
) : (
)}
페르소나 검수
공개 전 승인 큐
0 ? "accent" : "neutral"}>
{personaReviewCount}건
{personaReviewError ? (
{personaReviewError}
네트워크 또는 서버 오류가 발생했습니다.
}
onClick={() => void loadPersonaReviews()}
disabled={personaReviewLoading}
>
{personaReviewLoading ? "확인 중" : "다시 시도"}
) : personaReviews.length > 0 ? (
{personaReviews.map((persona) => {
return (
{persona.code}
{persona.display_name}
v{persona.version} · {persona.difficulty} ·{" "}
{persona.theory_target.join(" · ") || "공통"}
{persona.source_provenance || "출처 정보 없음"}
{personaReviewStatusLabel(persona.status)}
{formatDateTime(persona.created_at ?? null)}
{persona.status === "review" ? (
}
onClick={() => void decidePersonaReview(persona.persona_id, "approve")}
disabled={personaDecisionBusy === `${persona.persona_id}:approve`}
>
승인
) : null}
}
onClick={() =>
navigate(`/teach/personas?draft=${encodeURIComponent(persona.persona_id)}`)
}
>
스튜디오에서 검수
);
})}
) : (
)}
페르소나 스튜디오
배경 서사, 임상 개념화, 역린·금기, 회기별 시나리오, 프롬프트 미리보기와
검수 요청을 한 작업면에서 관리합니다.
초안 {personaReviewCount}건
공개 전 검수 큐 연동
}
onClick={() => navigate("/teach/personas")}
>
스튜디오 열기
최근 세션
최근 회기 기록
{recentSessions.length}건
{recentSessions.length > 0 ? (
학습자
페르소나
상태
단계
턴
시작
종료
열기
{recentSessions.map((session) => {
const openLabel = sessionOpenLabel(session.status);
return (
);
})}
) : (
)}
);
}
function GrowthCard({ learner }: { learner: TeacherLearnerGrowth }) {
const points = learner.points ?? [];
const topTechniques = learner.top_techniques ?? [];
const recentPoints = points.slice(-3).reverse();
return (
{learner.learner_label}
{learner.ended_sessions}/{learner.sessions}회기 완료 · {formatDateTime(learner.latest_at)}
{trendLabel(learner.trend)}
최근 적절성
{formatScore(learner.latest_score)}
변화
{formatDelta(learner.score_delta)}
평균 라포
{formatScore(learner.avg_rapport == null ? null : (learner.avg_rapport + 1) / 2)}
{points.map((point) => (
))}
{topTechniques.length > 0 ? (
topTechniques.map((tag) => {tag})
) : (
기법 태그 부족
)}
{recentPoints.length > 0 ? (
recentPoints.map((point) => (
{point.persona_code} · {point.session_no}회기
{formatScore(point.score)} · 기법 {point.technique_count} · 점검 {point.watch_count}
))
) : (
회기 평가 부족
종료 회기와 턴별 평가가 필요합니다.
)}
);
}
function GrowthBar({ point }: { point: TeacherGrowthPoint }) {
const hasScore = typeof point.score === "number" && !Number.isNaN(point.score);
const height = hasScore ? Math.max(10, Math.round((point.score ?? 0) * 100)) : 10;
return (
{point.session_no}
);
}
function SafetyAlertRow({ alert }: { alert: TeacherSafetyAlert }) {
return (
{alert.learner_label}
{alert.persona_code || "세션"} · 위험도 {alert.ko_risk_level} ·{" "}
{formatDateTime(alert.created_at)}
{alert.session_id}
{alert.resource_title}
{alert.resource_number}
);
}
const PF_CSS = `
.pf-root{
max-width:1280px;
margin:0 auto;
display:flex;
flex-direction:column;
gap:16px;
}
.pf-head{
display:flex;
align-items:flex-end;
justify-content:space-between;
gap:var(--sp-4);
flex-wrap:wrap;
}
.pf-head__actions{
display:flex;
align-items:center;
justify-content:flex-end;
gap:8px;
flex-wrap:wrap;
}
.pf-head h1{
margin:6px 0 0;
color:var(--text-strong);
font-size:var(--fs-h2);
line-height:1.28;
letter-spacing:0;
}
.pf-head p{
margin:6px 0 0;
max-width:760px;
color:var(--text-body);
font-size:var(--fs-xs);
line-height:1.55;
}
.pf-signal-strip{
order:1;
display:grid;
grid-template-columns:1fr;
gap:0;
min-width:0;
}
.pf-triage{
display:grid;
grid-template-columns:auto minmax(0,1fr) auto;
align-items:center;
gap:12px;
min-width:0;
padding:14px 16px;
border:1px solid var(--hair);
border-radius:var(--radius);
background:var(--bg-surface);
box-shadow:var(--shadow-sm);
}
.pf-triage.is-active{
background:color-mix(in srgb,var(--accent-tint) 42%,var(--bg-surface));
}
.pf-triage__copy{
min-width:0;
}
.pf-triage__copy .vg-kicker{
margin-bottom:4px;
}
.pf-triage__copy b{
display:block;
color:var(--text-strong);
font-size:var(--fs-body);
line-height:1.35;
}
.pf-triage__copy span,
.pf-triage__meta small{
color:var(--text-muted);
font-size:var(--fs-xs);
}
.pf-triage__meta{
display:grid;
grid-template-columns:repeat(2,minmax(0,1fr));
gap:0;
min-width:180px;
border:1px solid var(--border-subtle);
border-radius:var(--radius);
background:color-mix(in srgb,var(--bg-surface) 86%,transparent);
overflow:hidden;
}
.pf-triage__meta span{
display:grid;
align-content:center;
gap:2px;
padding:9px 12px;
}
.pf-triage__meta span + span{
border-left:1px solid var(--hair);
}
.pf-triage__meta b{
color:var(--text-strong);
font-family:var(--font-num);
font-size:17px;
line-height:1;
}
.pf-error{
display:flex;
align-items:center;
gap:10px;
padding:12px 14px;
border-radius:var(--radius);
background:var(--crit-tint);
color:var(--crit-text);
font-size:var(--fs-sm);
}
.pf-kpis{
display:grid;
grid-template-columns:repeat(6,minmax(0,1fr));
border:1px solid var(--hair);
border-radius:var(--radius);
overflow:hidden;
background:var(--bg-surface);
box-shadow:var(--shadow-sm);
}
.pf-kpi{
position:relative;
min-width:0;
padding:14px 16px;
display:grid;
grid-template-columns:minmax(0,1fr) auto;
gap:4px 10px;
}
.pf-kpi:nth-child(n+2){border-left:1px solid var(--hair);}
.pf-kpi:nth-child(n+4){border-top:0;}
.pf-kpi--primary,
.pf-kpi--warn{
background:color-mix(in srgb,var(--accent-tint) 32%,var(--bg-surface));
}
.pf-kpi--warn{
background:color-mix(in srgb,var(--warn-tint) 36%,var(--bg-surface));
}
.pf-kpi--primary .pf-kpi__ic{
color:var(--text-on-accent);
background:var(--accent);
}
.pf-kpi--warn .pf-kpi__ic{
color:var(--warn-text);
background:var(--warn-tint);
}
.pf-kpi__ic{
grid-column:2;
grid-row:1 / span 3;
width:30px;
height:30px;
display:grid;
place-items:center;
border-radius:var(--radius);
color:var(--accent);
background:var(--accent-tint);
}
.pf-kpi__lab{
display:block;
color:var(--text-muted);
font-size:var(--fs-xs);
font-weight:700;
}
.pf-kpi b{
display:block;
color:var(--text-strong);
font-family:var(--font-num);
font-size:24px;
line-height:1;
}
.pf-kpi small{
color:var(--text-muted);
font-size:12px;
line-height:1.35;
}
.pf-workspace{
order:2;
display:grid;
grid-template-columns:minmax(320px,390px) minmax(0,1fr);
align-items:start;
gap:14px;
min-width:0;
}
.pf-queue-stack{
display:flex;
flex-direction:column;
gap:14px;
min-width:0;
}
.pf-section{
display:flex;
flex-direction:column;
gap:10px;
min-width:0;
}
.pf-section__head{
display:flex;
align-items:flex-end;
justify-content:space-between;
gap:12px;
}
.pf-section__head h2{
margin:4px 0 0;
color:var(--text-strong);
font-size:var(--fs-body);
font-weight:700;
line-height:1.3;
letter-spacing:0;
}
.pf-panel{
padding:0;
overflow:hidden;
}
.pf-studio-card{
display:grid;
grid-template-columns:minmax(0,1fr) auto;
gap:14px;
align-items:center;
padding:14px;
background:linear-gradient(180deg,var(--bg-surface),color-mix(in srgb,var(--accent-tint) 18%,var(--bg-surface)));
}
.pf-studio-card__copy{
min-width:0;
display:grid;
gap:7px;
}
.pf-studio-card__copy b{
color:var(--text-strong);
font-size:var(--fs-body);
}
.pf-studio-card__copy p{
margin:0;
color:var(--text-muted);
font-size:var(--fs-xs);
line-height:1.55;
}
.pf-studio-card__meta{
display:flex;
flex-wrap:wrap;
gap:6px;
}
.pf-studio-card__meta span{
padding:4px 7px;
border:1px solid var(--hair);
border-radius:var(--radius-sm);
color:var(--text-body);
background:var(--bg-surface-2);
font-size:11px;
line-height:1.2;
}
.pf-studio-card__actions{
display:flex;
justify-content:flex-end;
}
.pf-section--growth{
order:3;
min-width:0;
}
.pf-growth-panel{
background:linear-gradient(180deg,var(--bg-surface),color-mix(in srgb,var(--accent-tint) 18%,var(--bg-surface)));
}
.pf-growth-list{
max-height:min(420px,44vh);
overflow:auto;
scrollbar-gutter:stable;
display:grid;
grid-template-columns:repeat(3,minmax(0,1fr));
gap:12px;
padding:12px;
}
.pf-growth-card{
min-width:0;
display:flex;
flex-direction:column;
gap:12px;
padding:13px;
border:1px solid var(--hair);
border-radius:var(--radius-sm);
background:color-mix(in srgb,var(--bg-surface) 82%,var(--bg-surface-2));
box-shadow:none;
}
.pf-growth-card__top{
display:flex;
align-items:flex-start;
justify-content:space-between;
gap:10px;
min-width:0;
}
.pf-growth-card__id{
min-width:0;
display:grid;
gap:3px;
}
.pf-growth-card__id b{
color:var(--text-strong);
font-size:var(--fs-sm);
line-height:1.35;
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
}
.pf-growth-card__id span,
.pf-growth-card__metrics small,
.pf-growth-point span,
.pf-recent__review-state{
color:var(--text-muted);
font-size:12px;
line-height:1.4;
}
.pf-recent__review-state{
display:block;
margin-top:3px;
line-height:1.2;
}
.pf-growth-card__metrics{
display:grid;
grid-template-columns:repeat(3,minmax(0,1fr));
gap:8px;
}
.pf-growth-card__metrics span{
min-width:0;
display:grid;
gap:3px;
padding:9px 10px;
border:1px solid var(--paper-2);
border-radius:var(--radius-sm);
background:var(--bg-surface-2);
}
.pf-growth-card__metrics b{
color:var(--text-strong);
font-family:var(--font-num);
font-size:15px;
line-height:1.15;
white-space:nowrap;
}
.pf-growth-bars{
height:82px;
display:flex;
align-items:flex-end;
gap:6px;
padding:8px 8px 6px;
border:1px solid var(--paper-2);
border-radius:var(--radius-sm);
background:color-mix(in srgb,var(--bg-surface-2) 82%,transparent);
}
.pf-growth-bar{
flex:1 1 0;
min-width:14px;
height:100%;
display:grid;
grid-template-rows:minmax(0,1fr) 14px;
gap:4px;
align-items:end;
}
.pf-growth-bar i{
display:block;
width:100%;
min-height:6px;
border-radius:6px 6px 3px 3px;
background:linear-gradient(180deg,var(--accent),var(--accent-deep));
}
.pf-growth-bar.is-empty i{
background:repeating-linear-gradient(135deg,var(--paper-2),var(--paper-2) 3px,var(--hair) 3px,var(--hair) 6px);
}
.pf-growth-bar small{
color:var(--text-muted);
font-family:var(--font-num);
font-size:10px;
text-align:center;
line-height:1;
}
.pf-growth-card__tags{
min-height:26px;
display:flex;
flex-wrap:wrap;
gap:6px;
align-content:flex-start;
}
.pf-growth-card__tags span{
max-width:100%;
padding:4px 7px;
border:1px solid var(--paper-2);
border-radius:999px;
color:var(--text-body);
background:var(--bg-surface-2);
font-size:11px;
line-height:1.2;
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
}
.pf-growth-card__points{
display:grid;
gap:7px;
}
.pf-growth-point{
min-width:0;
display:grid;
grid-template-columns:minmax(86px,.5fr) minmax(0,1fr);
gap:8px;
align-items:center;
}
.pf-growth-point b{
color:var(--text-strong);
font-size:12px;
line-height:1.35;
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
}
.pf-growth-point span{
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
}
.pf-empty{
min-height:118px;
display:grid;
place-items:center;
gap:6px;
padding:var(--sp-5);
text-align:center;
}
.pf-empty b{
color:var(--text-strong);
font-size:var(--fs-body);
}
.pf-empty span{
color:var(--text-muted);
font-size:var(--fs-xs);
}
.pf-list{
max-height:min(320px,42vh);
overflow:auto;
scrollbar-gutter:stable;
display:flex;
flex-direction:column;
}
.pf-personas{
max-height:min(320px,42vh);
overflow:auto;
scrollbar-gutter:stable;
display:flex;
flex-direction:column;
}
.pf-persona{
display:grid;
grid-template-columns:minmax(0,1fr);
gap:8px;
align-items:start;
padding:12px;
border-top:1px solid var(--paper-2);
}
.pf-persona:first-child{border-top:0;}
.pf-persona__main{
min-width:0;
display:grid;
grid-template-columns:40px minmax(0,1fr);
gap:10px;
align-items:center;
}
.pf-persona__code{
width:40px;
height:32px;
display:grid;
place-items:center;
border-radius:var(--radius);
color:var(--accent-deep);
background:var(--accent-tint);
font-family:var(--font-num);
font-weight:800;
font-size:12px;
}
.pf-persona__main b{
display:block;
color:var(--text-strong);
font-size:var(--fs-sm);
line-height:1.35;
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
}
.pf-persona__main span,
.pf-persona p,
.pf-persona__meta span{
color:var(--text-muted);
font-size:var(--fs-xs);
line-height:1.45;
}
.pf-persona p{
margin:0;
min-width:0;
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
}
.pf-persona__meta{
display:flex;
align-items:center;
gap:10px;
justify-content:space-between;
white-space:nowrap;
}
.pf-persona__actions{
display:flex;
justify-content:flex-end;
gap:8px;
min-width:0;
}
.pf-persona__actions .vg-btn{
min-width:72px;
padding-inline:10px;
}
.pf-alerts{
max-height:min(300px,38vh);
overflow:auto;
scrollbar-gutter:stable;
display:flex;
flex-direction:column;
}
.pf-alert{
display:grid;
grid-template-columns:auto minmax(0,1fr) auto;
gap:10px;
align-items:center;
padding:12px;
border-top:1px solid var(--paper-2);
background:color-mix(in srgb,var(--warn-tint) 34%,transparent);
}
.pf-alert:first-child{border-top:0;}
.pf-alert__ic{
width:30px;
height:30px;
display:grid;
place-items:center;
border-radius:var(--radius);
color:var(--warn-text);
background:var(--warn-tint);
}
.pf-alert__main{
min-width:0;
display:grid;
gap:3px;
}
.pf-alert__main b{
color:var(--text-strong);
font-size:var(--fs-sm);
}
.pf-alert__main span,
.pf-alert__main code,
.pf-alert__resource span{
color:var(--text-muted);
font-size:12px;
}
.pf-alert__main code{
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
}
.pf-alert__resource{
display:grid;
gap:2px;
justify-items:end;
min-width:86px;
}
.pf-alert__resource b{
color:var(--warn-text);
font-family:var(--font-num);
font-size:18px;
}
.pf-session{
width:100%;
font:inherit;
text-align:left;
background:transparent;
color:inherit;
display:grid;
grid-template-columns:8px minmax(0,1fr) auto;
gap:8px 10px;
align-items:start;
padding:12px;
border:0;
border-top:1px solid var(--paper-2);
cursor:default;
}
.pf-session:first-child{border-top:0;}
.pf-session--action{
cursor:pointer;
}
.pf-session--action:hover{
background:var(--bg-surface-2);
}
.pf-session--action:focus-visible{
outline:2px solid var(--accent);
outline-offset:-2px;
}
.pf-session__dot{
width:8px;
height:8px;
border-radius:50%;
background:var(--accent);
}
.pf-session__main{
min-width:0;
display:flex;
flex-direction:column;
gap:3px;
}
.pf-session__main b{
color:var(--text-strong);
font-size:var(--fs-sm);
}
.pf-session__main span,.pf-session__main code{
color:var(--text-muted);
font-size:var(--fs-xs);
overflow-wrap:anywhere;
}
.pf-session__main code,.pf-recent__learner code{
font-family:var(--font-num);
overflow:hidden;
text-overflow:ellipsis;
}
.pf-session__meta{
grid-column:2 / 4;
display:flex;
align-items:center;
justify-content:space-between;
gap:8px;
color:var(--text-muted);
font-family:var(--font-num);
font-size:var(--fs-xs);
white-space:normal;
}
.pf-session__open{
grid-column:3;
grid-row:1;
display:inline-flex;
align-items:center;
gap:5px;
align-self:center;
justify-self:end;
min-height:30px;
padding:0 9px;
border:1px solid var(--hair);
border-radius:var(--radius-sm);
color:var(--accent-deep);
background:var(--accent-tint);
font-size:var(--fs-xs);
font-weight:760;
white-space:nowrap;
}
.pf-session--action:hover .pf-session__open{
border-color:var(--accent);
}
.pf-recent-list{
max-height:min(620px,calc(100vh - 220px));
overflow:auto;
scrollbar-gutter:stable;
min-width:0;
}
.pf-recent-head,
.pf-recent-row{
display:grid;
grid-template-columns:
minmax(128px,1.25fr)
minmax(58px,.55fr)
minmax(66px,.55fr)
minmax(82px,.85fr)
minmax(32px,.35fr)
minmax(70px,.6fr)
minmax(66px,.55fr)
minmax(86px,.65fr);
align-items:center;
gap:8px;
min-width:0;
}
.pf-recent-head{
position:sticky;
top:0;
z-index:1;
color:var(--text-muted);
font-size:var(--fs-xs);
font-weight:700;
padding:9px 12px;
border-bottom:1px solid var(--hair);
background:var(--bg-surface);
white-space:nowrap;
}
.pf-recent-row{
width:100%;
font:inherit;
text-align:left;
color:inherit;
background:transparent;
padding:10px 12px;
border:0;
border-top:1px solid var(--paper-2);
}
.pf-recent-row--action{
cursor:pointer;
}
.pf-recent-row--action:hover{
background:var(--bg-surface-2);
}
.pf-recent-row--action:focus-visible{
outline:2px solid var(--accent);
outline-offset:-2px;
}
.pf-recent-head + .pf-recent-row{
border-top:0;
}
.pf-recent__learner,
.pf-recent__cell{
min-width:0;
color:var(--text-body);
font-size:var(--fs-sm);
overflow-wrap:anywhere;
}
.pf-recent__cell::before{
display:none;
}
.pf-recent__learner{
min-width:0;
display:flex;
flex-direction:column;
gap:3px;
}
.pf-recent__learner b{
color:var(--text-strong);
font-size:var(--fs-sm);
line-height:1.35;
overflow-wrap:anywhere;
}
.pf-recent__learner code{
max-width:100%;
color:var(--text-muted);
font-size:11px;
white-space:nowrap;
overflow:hidden;
text-overflow:ellipsis;
}
.pf-recent__cell .vg-badge{
justify-self:start;
}
.pf-recent__cell--open{
display:flex;
justify-content:flex-end;
}
.pf-recent__open{
display:inline-flex;
align-items:center;
justify-content:center;
gap:4px;
min-height:30px;
padding:0 7px;
border:1px solid var(--hair);
border-radius:var(--radius-sm);
color:var(--accent-deep);
background:var(--accent-tint);
font-size:var(--fs-xs);
font-weight:760;
white-space:nowrap;
}
.pf-recent-row--action:hover .pf-recent__open{
border-color:var(--accent);
}
@media (max-width:1100px){
.pf-signal-strip,
.pf-workspace{
grid-template-columns:1fr;
}
.pf-triage{
grid-template-columns:auto minmax(0,max-content) auto;
justify-content:start;
}
.pf-kpis{
grid-template-columns:repeat(4,minmax(0,1fr));
}
.pf-kpi:nth-child(n+2){border-left:1px solid var(--hair);}
.pf-kpi:nth-child(n+4){border-top:0;}
.pf-growth-list{
grid-template-columns:repeat(2,minmax(0,1fr));
}
.pf-list,
.pf-personas{
max-height:360px;
}
.pf-recent-list{
max-height:460px;
}
}
@media (max-width:860px){
.pf-head__actions{
width:100%;
justify-content:flex-start;
}
.pf-triage{
grid-template-columns:auto minmax(0,1fr);
}
.pf-triage__meta{
grid-column:1 / -1;
width:100%;
}
.pf-kpis{grid-template-columns:repeat(2,minmax(0,1fr));}
.pf-kpi:nth-child(n+2){border-left:0;}
.pf-kpi:nth-child(even){border-left:1px solid var(--hair);}
.pf-kpi:nth-child(n+3){border-top:1px solid var(--hair);}
.pf-growth-list{
grid-template-columns:1fr;
}
.pf-recent-list{
display:flex;
flex-direction:column;
gap:10px;
padding:10px;
background:var(--bg-surface-2);
overflow-x:hidden;
}
.pf-recent-head{
display:none;
}
.pf-recent-row{
display:grid;
grid-template-columns:repeat(2,minmax(0,1fr));
gap:10px 12px;
padding:12px;
border:1px solid var(--paper-2);
border-radius:var(--radius);
background:var(--bg-surface);
}
.pf-recent__learner{
grid-column:1 / -1;
padding-bottom:10px;
border-bottom:1px solid var(--paper-2);
}
.pf-recent__learner code{
white-space:normal;
overflow-wrap:anywhere;
}
.pf-recent__cell{
display:grid;
grid-template-columns:minmax(74px,.4fr) minmax(0,1fr);
gap:8px;
align-items:center;
}
.pf-recent__cell--open{
justify-content:stretch;
}
.pf-recent__cell::before{
display:block;
content:attr(data-label);
color:var(--text-muted);
font-size:var(--fs-xs);
font-weight:700;
line-height:1.35;
}
.pf-session{
grid-template-columns:8px minmax(0,1fr);
}
.pf-session__meta{
grid-column:2;
justify-content:flex-start;
}
.pf-session__open{
grid-column:2;
grid-row:auto;
justify-self:start;
}
}
@media (max-width:520px){
.pf-kpis{grid-template-columns:repeat(2,minmax(0,1fr));}
.pf-kpi,
.pf-kpi + .pf-kpi{border-left:0;}
.pf-kpi:nth-child(even){border-left:1px solid var(--hair);}
.pf-kpi:nth-child(n+3){border-top:1px solid var(--hair);}
.pf-persona__actions{
display:grid;
grid-template-columns:repeat(2,minmax(0,1fr));
}
.pf-persona__actions .vg-btn{
width:100%;
}
.pf-growth-list{
padding:10px;
}
.pf-growth-card__metrics{
grid-template-columns:1fr;
}
.pf-growth-point{
grid-template-columns:1fr;
gap:2px;
}
.pf-growth-point b,
.pf-growth-point span{
white-space:normal;
}
.pf-alert{
grid-template-columns:auto minmax(0,1fr);
}
.pf-alert__resource{
grid-column:2;
justify-items:start;
}
.pf-recent-list{
padding:8px;
}
.pf-recent-row{
grid-template-columns:1fr;
gap:10px;
}
.pf-recent__cell{
grid-template-columns:minmax(64px,.32fr) minmax(0,1fr);
}
.pf-recent__cell--open .pf-recent__open{
justify-self:start;
}
}
`;