@tipp/ui 2.9.0 → 2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/index.js +17 -17
- package/dist/app/platform/edit-coaching-time.js +17 -17
- package/dist/app/platform/edit-service-type.js +17 -17
- package/dist/app/platform/goal-manage-card-edit.js +17 -17
- package/dist/app/platform/on-offline-radio-card.js +17 -17
- package/dist/app/platform/reservation-card.js +17 -17
- package/dist/app/platform/session-card.js +17 -17
- package/dist/app/roleplay/chart/session-chart.cjs +351 -0
- package/dist/app/roleplay/chart/session-chart.cjs.map +1 -0
- package/dist/app/roleplay/chart/session-chart.d.cts +16 -0
- package/dist/app/roleplay/chart/session-chart.d.ts +16 -0
- package/dist/app/roleplay/chart/session-chart.js +14 -0
- package/dist/app/roleplay/chart/session-chart.js.map +1 -0
- package/dist/app/roleplay/index.cjs +5715 -0
- package/dist/app/roleplay/index.cjs.map +1 -0
- package/dist/app/roleplay/index.d.cts +4 -0
- package/dist/app/roleplay/index.d.ts +4 -0
- package/dist/app/roleplay/index.js +182 -0
- package/dist/app/roleplay/index.js.map +1 -0
- package/dist/app/roleplay/parts/analysis-section.cjs +2178 -0
- package/dist/app/roleplay/parts/analysis-section.cjs.map +1 -0
- package/dist/app/roleplay/parts/analysis-section.d.cts +17 -0
- package/dist/app/roleplay/parts/analysis-section.d.ts +17 -0
- package/dist/app/roleplay/parts/analysis-section.js +97 -0
- package/dist/app/roleplay/parts/analysis-section.js.map +1 -0
- package/dist/app/roleplay/parts/coaching-panel.cjs +2191 -0
- package/dist/app/roleplay/parts/coaching-panel.cjs.map +1 -0
- package/dist/app/roleplay/parts/coaching-panel.d.cts +8 -0
- package/dist/app/roleplay/parts/coaching-panel.d.ts +8 -0
- package/dist/app/roleplay/parts/coaching-panel.js +95 -0
- package/dist/app/roleplay/parts/coaching-panel.js.map +1 -0
- package/dist/app/roleplay/parts/content.cjs +4687 -0
- package/dist/app/roleplay/parts/content.cjs.map +1 -0
- package/dist/app/roleplay/parts/content.d.cts +15 -0
- package/dist/app/roleplay/parts/content.d.ts +15 -0
- package/dist/app/roleplay/parts/content.js +174 -0
- package/dist/app/roleplay/parts/content.js.map +1 -0
- package/dist/app/roleplay/parts/header.cjs +2300 -0
- package/dist/app/roleplay/parts/header.cjs.map +1 -0
- package/dist/app/roleplay/parts/header.d.cts +13 -0
- package/dist/app/roleplay/parts/header.d.ts +13 -0
- package/dist/app/roleplay/parts/header.js +96 -0
- package/dist/app/roleplay/parts/header.js.map +1 -0
- package/dist/app/roleplay/parts/score-summary.cjs +2276 -0
- package/dist/app/roleplay/parts/score-summary.cjs.map +1 -0
- package/dist/app/roleplay/parts/score-summary.d.cts +12 -0
- package/dist/app/roleplay/parts/score-summary.d.ts +12 -0
- package/dist/app/roleplay/parts/score-summary.js +96 -0
- package/dist/app/roleplay/parts/score-summary.js.map +1 -0
- package/dist/app/roleplay/parts/score-trend-section.cjs +3687 -0
- package/dist/app/roleplay/parts/score-trend-section.cjs.map +1 -0
- package/dist/app/roleplay/parts/score-trend-section.d.cts +14 -0
- package/dist/app/roleplay/parts/score-trend-section.d.ts +14 -0
- package/dist/app/roleplay/parts/score-trend-section.js +167 -0
- package/dist/app/roleplay/parts/score-trend-section.js.map +1 -0
- package/dist/app/roleplay/parts/script-transcript.cjs +2288 -0
- package/dist/app/roleplay/parts/script-transcript.cjs.map +1 -0
- package/dist/app/roleplay/parts/script-transcript.d.cts +15 -0
- package/dist/app/roleplay/parts/script-transcript.d.ts +15 -0
- package/dist/app/roleplay/parts/script-transcript.js +96 -0
- package/dist/app/roleplay/parts/script-transcript.js.map +1 -0
- package/dist/app/roleplay/parts/section-nav.cjs +2433 -0
- package/dist/app/roleplay/parts/section-nav.cjs.map +1 -0
- package/dist/app/roleplay/parts/section-nav.d.cts +15 -0
- package/dist/app/roleplay/parts/section-nav.d.ts +15 -0
- package/dist/app/roleplay/parts/section-nav.js +96 -0
- package/dist/app/roleplay/parts/section-nav.js.map +1 -0
- package/dist/app/roleplay/parts/session-replay-dialog.cjs +2598 -0
- package/dist/app/roleplay/parts/session-replay-dialog.cjs.map +1 -0
- package/dist/app/roleplay/parts/session-replay-dialog.d.cts +14 -0
- package/dist/app/roleplay/parts/session-replay-dialog.d.ts +14 -0
- package/dist/app/roleplay/parts/session-replay-dialog.js +97 -0
- package/dist/app/roleplay/parts/session-replay-dialog.js.map +1 -0
- package/dist/app/roleplay/parts/step-detail.cjs +2311 -0
- package/dist/app/roleplay/parts/step-detail.cjs.map +1 -0
- package/dist/app/roleplay/parts/step-detail.d.cts +9 -0
- package/dist/app/roleplay/parts/step-detail.d.ts +9 -0
- package/dist/app/roleplay/parts/step-detail.js +96 -0
- package/dist/app/roleplay/parts/step-detail.js.map +1 -0
- package/dist/app/roleplay/parts/step-radar.cjs +463 -0
- package/dist/app/roleplay/parts/step-radar.cjs.map +1 -0
- package/dist/app/roleplay/parts/step-radar.d.cts +8 -0
- package/dist/app/roleplay/parts/step-radar.d.ts +8 -0
- package/dist/app/roleplay/parts/step-radar.js +14 -0
- package/dist/app/roleplay/parts/step-radar.js.map +1 -0
- package/dist/app/roleplay/parts/step-summary.cjs +2651 -0
- package/dist/app/roleplay/parts/step-summary.cjs.map +1 -0
- package/dist/app/roleplay/parts/step-summary.d.cts +8 -0
- package/dist/app/roleplay/parts/step-summary.d.ts +8 -0
- package/dist/app/roleplay/parts/step-summary.js +101 -0
- package/dist/app/roleplay/parts/step-summary.js.map +1 -0
- package/dist/app/roleplay/rp-session-detail.cjs +5712 -0
- package/dist/app/roleplay/rp-session-detail.cjs.map +1 -0
- package/dist/app/roleplay/rp-session-detail.d.cts +17 -0
- package/dist/app/roleplay/rp-session-detail.d.ts +17 -0
- package/dist/app/roleplay/rp-session-detail.js +179 -0
- package/dist/app/roleplay/rp-session-detail.js.map +1 -0
- package/dist/app/roleplay/types.cjs +19 -0
- package/dist/app/roleplay/types.cjs.map +1 -0
- package/dist/app/roleplay/types.d.cts +42 -0
- package/dist/app/roleplay/types.d.ts +42 -0
- package/dist/app/roleplay/types.js +2 -0
- package/dist/app/roleplay/types.js.map +1 -0
- package/dist/app/roleplay/utils/report-parse.cjs +285 -0
- package/dist/app/roleplay/utils/report-parse.cjs.map +1 -0
- package/dist/app/roleplay/utils/report-parse.d.cts +57 -0
- package/dist/app/roleplay/utils/report-parse.d.ts +57 -0
- package/dist/app/roleplay/utils/report-parse.js +8 -0
- package/dist/app/roleplay/utils/report-parse.js.map +1 -0
- package/dist/app/roleplay/utils/report-parse.test.cjs +491 -0
- package/dist/app/roleplay/utils/report-parse.test.cjs.map +1 -0
- package/dist/app/roleplay/utils/report-parse.test.d.cts +2 -0
- package/dist/app/roleplay/utils/report-parse.test.d.ts +2 -0
- package/dist/app/roleplay/utils/report-parse.test.js +237 -0
- package/dist/app/roleplay/utils/report-parse.test.js.map +1 -0
- package/dist/app/roleplay/utils/session-utils.cjs +204 -0
- package/dist/app/roleplay/utils/session-utils.cjs.map +1 -0
- package/dist/app/roleplay/utils/session-utils.d.cts +38 -0
- package/dist/app/roleplay/utils/session-utils.d.ts +38 -0
- package/dist/app/roleplay/utils/session-utils.js +46 -0
- package/dist/app/roleplay/utils/session-utils.js.map +1 -0
- package/dist/chunk-32WIS4L7.js +131 -0
- package/dist/chunk-32WIS4L7.js.map +1 -0
- package/dist/chunk-3UGBMKSQ.js +166 -0
- package/dist/chunk-3UGBMKSQ.js.map +1 -0
- package/dist/chunk-4YAA25AV.js +161 -0
- package/dist/chunk-4YAA25AV.js.map +1 -0
- package/dist/chunk-5QIT4URV.js +87 -0
- package/dist/chunk-5QIT4URV.js.map +1 -0
- package/dist/chunk-6YIKOYBQ.js +185 -0
- package/dist/chunk-6YIKOYBQ.js.map +1 -0
- package/dist/chunk-7MXY3D3Z.js +84 -0
- package/dist/chunk-7MXY3D3Z.js.map +1 -0
- package/dist/chunk-EBFQFOUQ.js +138 -0
- package/dist/chunk-EBFQFOUQ.js.map +1 -0
- package/dist/chunk-EHDUKUML.js +183 -0
- package/dist/chunk-EHDUKUML.js.map +1 -0
- package/dist/{chunk-NEJCZABQ.js → chunk-FJEDSUGU.js} +7 -7
- package/dist/chunk-GA6W62DO.js +101 -0
- package/dist/chunk-GA6W62DO.js.map +1 -0
- package/dist/chunk-KJPBBBVK.js +142 -0
- package/dist/chunk-KJPBBBVK.js.map +1 -0
- package/dist/{chunk-TQTCT7DC.js → chunk-LRNO4W2P.js} +5 -5
- package/dist/{chunk-6DWUGXSY.js → chunk-N242VJOP.js} +2 -2
- package/dist/chunk-NKW6BO5R.js +264 -0
- package/dist/chunk-NKW6BO5R.js.map +1 -0
- package/dist/{chunk-R5UDNKV2.js → chunk-O332K3SW.js} +2 -2
- package/dist/chunk-OUP3VFXG.js +1 -0
- package/dist/chunk-OUP3VFXG.js.map +1 -0
- package/dist/chunk-QMAINJ4G.js +190 -0
- package/dist/chunk-QMAINJ4G.js.map +1 -0
- package/dist/{chunk-KLJVIEFW.js → chunk-SOKGYD5B.js} +7 -7
- package/dist/chunk-TFJJKM3W.js +261 -0
- package/dist/chunk-TFJJKM3W.js.map +1 -0
- package/dist/{chunk-KQ3ME6XY.js → chunk-U2EAXN3Y.js} +4 -4
- package/dist/chunk-UXAEOCOH.js +150 -0
- package/dist/chunk-UXAEOCOH.js.map +1 -0
- package/dist/chunk-YHP75Y5K.js +242 -0
- package/dist/chunk-YHP75Y5K.js.map +1 -0
- package/dist/chunk-ZE36DFOC.js +142 -0
- package/dist/chunk-ZE36DFOC.js.map +1 -0
- package/dist/index.css +76 -0
- package/dist/index.js +32 -32
- package/dist/molecules/curriculumV2/CurriculumContents/CurriculumReviewSteps/ReviewQuestion/index.js +5 -5
- package/dist/molecules/curriculumV2/CurriculumContents/CurriculumReviewSteps/ReviewQuestion/review-question-body.js +2 -2
- package/dist/molecules/curriculumV2/CurriculumContents/CurriculumReviewSteps/index.js +7 -7
- package/dist/molecules/curriculumV2/CurriculumContents/curriculum-review.js +8 -8
- package/dist/molecules/curriculumV2/CurriculumContents/index.js +9 -9
- package/dist/molecules/curriculumV2/index.js +11 -11
- package/dist/molecules/dynamic-form-editor.js +2 -2
- package/dist/molecules/index.js +29 -29
- package/package.json +28 -2
- package/src/app/roleplay/chart/session-chart.tsx +185 -0
- package/src/app/roleplay/index.ts +8 -0
- package/src/app/roleplay/parts/analysis-section.tsx +98 -0
- package/src/app/roleplay/parts/coaching-panel.tsx +101 -0
- package/src/app/roleplay/parts/content.tsx +146 -0
- package/src/app/roleplay/parts/header.tsx +168 -0
- package/src/app/roleplay/parts/score-summary.tsx +158 -0
- package/src/app/roleplay/parts/score-trend-section.tsx +265 -0
- package/src/app/roleplay/parts/script-transcript.tsx +190 -0
- package/src/app/roleplay/parts/section-nav.tsx +299 -0
- package/src/app/roleplay/parts/session-replay-dialog.tsx +168 -0
- package/src/app/roleplay/parts/step-detail.tsx +173 -0
- package/src/app/roleplay/parts/step-radar.tsx +80 -0
- package/src/app/roleplay/parts/step-summary.tsx +149 -0
- package/src/app/roleplay/rp-session-detail.tsx +176 -0
- package/src/app/roleplay/types.ts +61 -0
- package/src/app/roleplay/utils/report-parse.test.ts +268 -0
- package/src/app/roleplay/utils/report-parse.ts +443 -0
- package/src/app/roleplay/utils/session-utils.ts +215 -0
- package/src/constrant/trainingLog.dummy.json +1 -0
- /package/dist/{chunk-NEJCZABQ.js.map → chunk-FJEDSUGU.js.map} +0 -0
- /package/dist/{chunk-TQTCT7DC.js.map → chunk-LRNO4W2P.js.map} +0 -0
- /package/dist/{chunk-6DWUGXSY.js.map → chunk-N242VJOP.js.map} +0 -0
- /package/dist/{chunk-R5UDNKV2.js.map → chunk-O332K3SW.js.map} +0 -0
- /package/dist/{chunk-KLJVIEFW.js.map → chunk-SOKGYD5B.js.map} +0 -0
- /package/dist/{chunk-KQ3ME6XY.js.map → chunk-U2EAXN3Y.js.map} +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { RpSessionScriptLine } from '../types.js';
|
|
2
|
+
import { RpQuality, RpReportStep } from './report-parse.js';
|
|
3
|
+
|
|
4
|
+
interface SpeechShare {
|
|
5
|
+
userPct: number;
|
|
6
|
+
partnerPct: number;
|
|
7
|
+
}
|
|
8
|
+
type QColor = 'green' | 'amber' | 'red' | 'gray';
|
|
9
|
+
declare const COLOR_VARS: Record<QColor, {
|
|
10
|
+
fg: string;
|
|
11
|
+
bg: string;
|
|
12
|
+
border: string;
|
|
13
|
+
}>;
|
|
14
|
+
declare function qualityColor(quality: RpQuality | null): QColor;
|
|
15
|
+
declare const QUALITY_RATIO: Record<RpQuality, number>;
|
|
16
|
+
declare function qualityRatio(quality: RpQuality | null): number | null;
|
|
17
|
+
declare function gradeColorFromLabel(label: string | null): QColor;
|
|
18
|
+
declare function stepColor(step: RpReportStep): QColor;
|
|
19
|
+
declare function stepVars(step: RpReportStep): {
|
|
20
|
+
fg: string;
|
|
21
|
+
bg: string;
|
|
22
|
+
border: string;
|
|
23
|
+
};
|
|
24
|
+
declare function stepRatio(step: RpReportStep): number | null;
|
|
25
|
+
declare function stepScoreText(step: RpReportStep): string | null;
|
|
26
|
+
declare function stepGradeLabel(step: RpReportStep): string | null;
|
|
27
|
+
declare function hasStepSignal(step: RpReportStep): boolean;
|
|
28
|
+
declare function stepCode(step: RpReportStep): string;
|
|
29
|
+
declare function formatDuration(seconds: number | null): string;
|
|
30
|
+
declare function formatDate(iso: string): string;
|
|
31
|
+
declare function formatTimestamp(ms: number): string;
|
|
32
|
+
declare const SYNC_TOLERANCE = 0.05;
|
|
33
|
+
declare function isLengthClose(mediaSec: number | null, sttSec: number): boolean;
|
|
34
|
+
declare const SPEECH_OVERTALK_THRESHOLD = 40;
|
|
35
|
+
declare function computeSpeechShare(lines: RpSessionScriptLine[]): SpeechShare | null;
|
|
36
|
+
declare function statusLabelOf(status: string | null): string;
|
|
37
|
+
|
|
38
|
+
export { COLOR_VARS, type QColor, QUALITY_RATIO, SPEECH_OVERTALK_THRESHOLD, SYNC_TOLERANCE, type SpeechShare, computeSpeechShare, formatDate, formatDuration, formatTimestamp, gradeColorFromLabel, hasStepSignal, isLengthClose, qualityColor, qualityRatio, statusLabelOf, stepCode, stepColor, stepGradeLabel, stepRatio, stepScoreText, stepVars };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COLOR_VARS,
|
|
3
|
+
QUALITY_RATIO,
|
|
4
|
+
SPEECH_OVERTALK_THRESHOLD,
|
|
5
|
+
SYNC_TOLERANCE,
|
|
6
|
+
computeSpeechShare,
|
|
7
|
+
formatDate,
|
|
8
|
+
formatDuration,
|
|
9
|
+
formatTimestamp,
|
|
10
|
+
gradeColorFromLabel,
|
|
11
|
+
hasStepSignal,
|
|
12
|
+
isLengthClose,
|
|
13
|
+
qualityColor,
|
|
14
|
+
qualityRatio,
|
|
15
|
+
statusLabelOf,
|
|
16
|
+
stepCode,
|
|
17
|
+
stepColor,
|
|
18
|
+
stepGradeLabel,
|
|
19
|
+
stepRatio,
|
|
20
|
+
stepScoreText,
|
|
21
|
+
stepVars
|
|
22
|
+
} from "../../../chunk-4YAA25AV.js";
|
|
23
|
+
import "../../../chunk-CRTRMMJ7.js";
|
|
24
|
+
export {
|
|
25
|
+
COLOR_VARS,
|
|
26
|
+
QUALITY_RATIO,
|
|
27
|
+
SPEECH_OVERTALK_THRESHOLD,
|
|
28
|
+
SYNC_TOLERANCE,
|
|
29
|
+
computeSpeechShare,
|
|
30
|
+
formatDate,
|
|
31
|
+
formatDuration,
|
|
32
|
+
formatTimestamp,
|
|
33
|
+
gradeColorFromLabel,
|
|
34
|
+
hasStepSignal,
|
|
35
|
+
isLengthClose,
|
|
36
|
+
qualityColor,
|
|
37
|
+
qualityRatio,
|
|
38
|
+
statusLabelOf,
|
|
39
|
+
stepCode,
|
|
40
|
+
stepColor,
|
|
41
|
+
stepGradeLabel,
|
|
42
|
+
stepRatio,
|
|
43
|
+
stepScoreText,
|
|
44
|
+
stepVars
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=session-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StepSummary
|
|
3
|
+
} from "./chunk-EBFQFOUQ.js";
|
|
4
|
+
import {
|
|
5
|
+
ScoreSummary
|
|
6
|
+
} from "./chunk-6YIKOYBQ.js";
|
|
7
|
+
import {
|
|
8
|
+
ScoreTrendSection
|
|
9
|
+
} from "./chunk-YHP75Y5K.js";
|
|
10
|
+
import {
|
|
11
|
+
SectionNav
|
|
12
|
+
} from "./chunk-NKW6BO5R.js";
|
|
13
|
+
import {
|
|
14
|
+
StepDetail
|
|
15
|
+
} from "./chunk-KJPBBBVK.js";
|
|
16
|
+
import {
|
|
17
|
+
hasStepSignal
|
|
18
|
+
} from "./chunk-4YAA25AV.js";
|
|
19
|
+
import {
|
|
20
|
+
AnalysisSection,
|
|
21
|
+
Section
|
|
22
|
+
} from "./chunk-7MXY3D3Z.js";
|
|
23
|
+
import {
|
|
24
|
+
CoachingPanel
|
|
25
|
+
} from "./chunk-GA6W62DO.js";
|
|
26
|
+
import {
|
|
27
|
+
Separator
|
|
28
|
+
} from "./chunk-HYITAA4J.js";
|
|
29
|
+
import {
|
|
30
|
+
Grid
|
|
31
|
+
} from "./chunk-EGEQY3KT.js";
|
|
32
|
+
import {
|
|
33
|
+
Heading
|
|
34
|
+
} from "./chunk-USGOLLYO.js";
|
|
35
|
+
import {
|
|
36
|
+
Box
|
|
37
|
+
} from "./chunk-4Y5BEXVN.js";
|
|
38
|
+
import {
|
|
39
|
+
Typo
|
|
40
|
+
} from "./chunk-L4QQGIYQ.js";
|
|
41
|
+
import {
|
|
42
|
+
Flex
|
|
43
|
+
} from "./chunk-25HMMI7R.js";
|
|
44
|
+
|
|
45
|
+
// src/app/roleplay/parts/content.tsx
|
|
46
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
47
|
+
function Content({
|
|
48
|
+
detail,
|
|
49
|
+
report,
|
|
50
|
+
history,
|
|
51
|
+
speechShare,
|
|
52
|
+
scrollMarginTop
|
|
53
|
+
}) {
|
|
54
|
+
var _a;
|
|
55
|
+
const { steps, skills, strengths, improvements } = report;
|
|
56
|
+
const scoreValue = (_a = detail.totalScore) != null ? _a : report.totalScore;
|
|
57
|
+
const showTrend = history.length > 1;
|
|
58
|
+
const showStepSummary = steps.length > 0 && steps.some(hasStepSignal);
|
|
59
|
+
const showStepDetail = steps.length > 0;
|
|
60
|
+
const navItems = [
|
|
61
|
+
{ id: "score-summary", label: "\uC885\uD569 \uC810\uC218", score: scoreValue, max: 100 },
|
|
62
|
+
...strengths.length > 0 ? [{ id: "strengths", label: "\uAC15\uC810" }] : [],
|
|
63
|
+
...improvements.length > 0 ? [{ id: "improvements", label: "\uAC1C\uC120\uC810" }] : [],
|
|
64
|
+
...showTrend ? [{ id: "score-trend", label: "\uCD1D\uC810 \uCD94\uC774" }] : [],
|
|
65
|
+
...showStepSummary ? [{ id: "step-summary", label: "\uD3C9\uAC00 \uB2E8\uACC4 \uC694\uC57D" }] : [],
|
|
66
|
+
...showStepDetail ? [{ id: "step-detail", label: "\uB2E8\uACC4\uBCC4 \uC0C1\uC138 \uBD84\uC11D" }] : [],
|
|
67
|
+
...skills.length > 0 ? [{ id: "coaching", label: "\uC989\uC2DC \uC801\uC6A9 \uC2A4\uD0AC" }] : []
|
|
68
|
+
];
|
|
69
|
+
return /* @__PURE__ */ jsxs(
|
|
70
|
+
Grid,
|
|
71
|
+
{
|
|
72
|
+
align: "start",
|
|
73
|
+
columns: { initial: "1", md: "minmax(0, 1fr) 224px" },
|
|
74
|
+
gap: "7",
|
|
75
|
+
children: [
|
|
76
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "7", children: [
|
|
77
|
+
report.frameworkTitle !== null || report.frameworkDescription !== null ? /* @__PURE__ */ jsxs(Box, { children: [
|
|
78
|
+
report.frameworkTitle !== null ? /* @__PURE__ */ jsx(Heading, { size: "5", weight: "bold", children: report.frameworkTitle }) : null,
|
|
79
|
+
report.frameworkDescription !== null ? /* @__PURE__ */ jsx(Box, { mt: report.frameworkTitle !== null ? "2" : "0", children: /* @__PURE__ */ jsx(Typo, { as: "p", variant: "body", children: report.frameworkDescription }) }) : null
|
|
80
|
+
] }) : null,
|
|
81
|
+
/* @__PURE__ */ jsx(Box, { id: "score-summary", style: { scrollMarginTop }, children: /* @__PURE__ */ jsx(ScoreSummary, { detail, report, speechShare }) }),
|
|
82
|
+
strengths.length > 0 ? /* @__PURE__ */ jsx(Box, { id: "strengths", style: { scrollMarginTop }, children: /* @__PURE__ */ jsx(
|
|
83
|
+
AnalysisSection,
|
|
84
|
+
{
|
|
85
|
+
desc: "\uC774\uBC88 \uC138\uC158\uC5D0\uC11C \uC798\uD55C \uC810",
|
|
86
|
+
items: strengths,
|
|
87
|
+
title: "\uAC15\uC810",
|
|
88
|
+
tone: "good"
|
|
89
|
+
}
|
|
90
|
+
) }) : null,
|
|
91
|
+
improvements.length > 0 ? /* @__PURE__ */ jsx(Box, { id: "improvements", style: { scrollMarginTop }, children: /* @__PURE__ */ jsx(
|
|
92
|
+
AnalysisSection,
|
|
93
|
+
{
|
|
94
|
+
desc: "\uB2E4\uC74C \uC2DC\uB3C4\uC5D0\uC11C \uBCF4\uC644\uD560 \uC810",
|
|
95
|
+
items: improvements,
|
|
96
|
+
title: "\uAC1C\uC120\uC810",
|
|
97
|
+
tone: "bad"
|
|
98
|
+
}
|
|
99
|
+
) }) : null,
|
|
100
|
+
showTrend ? /* @__PURE__ */ jsx(Box, { id: "score-trend", style: { scrollMarginTop }, children: /* @__PURE__ */ jsx(ScoreTrendSection, { history }) }) : null,
|
|
101
|
+
showStepSummary ? /* @__PURE__ */ jsx(Box, { id: "step-summary", style: { scrollMarginTop }, children: /* @__PURE__ */ jsx(StepSummary, { steps }) }) : null,
|
|
102
|
+
showStepDetail ? /* @__PURE__ */ jsx(Box, { id: "step-detail", style: { scrollMarginTop }, children: /* @__PURE__ */ jsx(
|
|
103
|
+
Section,
|
|
104
|
+
{
|
|
105
|
+
desc: "\uB300\uD654 \uD504\uB808\uC784\uC6CC\uD06C \uB2E8\uACC4\uBCC4 \uD3C9\uAC00 \uACB0\uACFC\uC785\uB2C8\uB2E4.",
|
|
106
|
+
title: "\uB2E8\uACC4\uBCC4 \uC0C1\uC138 \uBD84\uC11D",
|
|
107
|
+
children: /* @__PURE__ */ jsx(Flex, { direction: "column", children: steps.map((step, i) => /* @__PURE__ */ jsxs(Box, { children: [
|
|
108
|
+
i > 0 ? /* @__PURE__ */ jsx(
|
|
109
|
+
Separator,
|
|
110
|
+
{
|
|
111
|
+
mb: "6",
|
|
112
|
+
size: "4",
|
|
113
|
+
style: { marginTop: "calc(var(--space-9) * 1.6)" }
|
|
114
|
+
}
|
|
115
|
+
) : null,
|
|
116
|
+
/* @__PURE__ */ jsx(StepDetail, { scrollMarginTop, step })
|
|
117
|
+
] }, step.key)) })
|
|
118
|
+
}
|
|
119
|
+
) }) : null,
|
|
120
|
+
skills.length > 0 ? /* @__PURE__ */ jsx(Box, { id: "coaching", style: { scrollMarginTop }, children: /* @__PURE__ */ jsx(CoachingPanel, { skills }) }) : null
|
|
121
|
+
] }),
|
|
122
|
+
/* @__PURE__ */ jsx(SectionNav, { items: navItems, steps: showStepDetail ? steps : [] })
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export {
|
|
129
|
+
Content
|
|
130
|
+
};
|
|
131
|
+
//# sourceMappingURL=chunk-32WIS4L7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/app/roleplay/parts/content.tsx"],"sourcesContent":["'use client';\n\nimport React from 'react';\nimport { Box, Flex, Grid, Heading, Separator, Typo } from '../../../atoms';\nimport type { RpSessionDetailData, RpSessionHistoryItem } from '../types';\nimport type { RpReport } from '../utils/report-parse';\nimport { hasStepSignal } from '../utils/session-utils';\nimport type { SpeechShare } from '../utils/session-utils';\nimport { AnalysisSection, Section } from './analysis-section';\nimport { CoachingPanel } from './coaching-panel';\nimport { ScoreSummary } from './score-summary';\nimport { ScoreTrendSection } from './score-trend-section';\nimport { SectionNav } from './section-nav';\nimport type { SectionNavItem } from './section-nav';\nimport { StepDetail } from './step-detail';\nimport { StepSummary } from './step-summary';\n\nexport interface ContentProps {\n detail: RpSessionDetailData;\n report: RpReport;\n history: RpSessionHistoryItem[];\n speechShare: SpeechShare | null;\n scrollMarginTop: string;\n}\n\nexport function Content({\n detail,\n report,\n history,\n speechShare,\n scrollMarginTop,\n}: ContentProps): React.ReactElement {\n const { steps, skills, strengths, improvements } = report;\n const scoreValue = detail.totalScore ?? report.totalScore;\n\n const showTrend = history.length > 1;\n const showStepSummary = steps.length > 0 && steps.some(hasStepSignal);\n const showStepDetail = steps.length > 0;\n\n const navItems: SectionNavItem[] = [\n { id: 'score-summary', label: '종합 점수', score: scoreValue, max: 100 },\n ...(strengths.length > 0 ? [{ id: 'strengths', label: '강점' }] : []),\n ...(improvements.length > 0 ? [{ id: 'improvements', label: '개선점' }] : []),\n ...(showTrend ? [{ id: 'score-trend', label: '총점 추이' }] : []),\n ...(showStepSummary ? [{ id: 'step-summary', label: '평가 단계 요약' }] : []),\n ...(showStepDetail ? [{ id: 'step-detail', label: '단계별 상세 분석' }] : []),\n ...(skills.length > 0 ? [{ id: 'coaching', label: '즉시 적용 스킬' }] : []),\n ];\n\n return (\n <Grid\n align=\"start\"\n columns={{ initial: '1', md: 'minmax(0, 1fr) 224px' }}\n gap=\"7\"\n >\n <Flex direction=\"column\" gap=\"7\">\n {report.frameworkTitle !== null || report.frameworkDescription !== null ? (\n <Box>\n {report.frameworkTitle !== null ? (\n <Heading size=\"5\" weight=\"bold\">\n {report.frameworkTitle}\n </Heading>\n ) : null}\n {report.frameworkDescription !== null ? (\n <Box mt={report.frameworkTitle !== null ? '2' : '0'}>\n <Typo as=\"p\" variant=\"body\">\n {report.frameworkDescription}\n </Typo>\n </Box>\n ) : null}\n </Box>\n ) : null}\n\n <Box id=\"score-summary\" style={{ scrollMarginTop }}>\n <ScoreSummary detail={detail} report={report} speechShare={speechShare} />\n </Box>\n\n {strengths.length > 0 ? (\n <Box id=\"strengths\" style={{ scrollMarginTop }}>\n <AnalysisSection\n desc=\"이번 세션에서 잘한 점\"\n items={strengths}\n title=\"강점\"\n tone=\"good\"\n />\n </Box>\n ) : null}\n\n {improvements.length > 0 ? (\n <Box id=\"improvements\" style={{ scrollMarginTop }}>\n <AnalysisSection\n desc=\"다음 시도에서 보완할 점\"\n items={improvements}\n title=\"개선점\"\n tone=\"bad\"\n />\n </Box>\n ) : null}\n\n {showTrend ? (\n <Box id=\"score-trend\" style={{ scrollMarginTop }}>\n <ScoreTrendSection history={history} />\n </Box>\n ) : null}\n\n {showStepSummary ? (\n <Box id=\"step-summary\" style={{ scrollMarginTop }}>\n <StepSummary steps={steps} />\n </Box>\n ) : null}\n\n {showStepDetail ? (\n <Box id=\"step-detail\" style={{ scrollMarginTop }}>\n <Section\n desc=\"대화 프레임워크 단계별 평가 결과입니다.\"\n title=\"단계별 상세 분석\"\n >\n <Flex direction=\"column\">\n {steps.map((step, i) => (\n <Box key={step.key}>\n {i > 0 ? (\n <Separator\n mb=\"6\"\n size=\"4\"\n style={{ marginTop: 'calc(var(--space-9) * 1.6)' }}\n />\n ) : null}\n <StepDetail scrollMarginTop={scrollMarginTop} step={step} />\n </Box>\n ))}\n </Flex>\n </Section>\n </Box>\n ) : null}\n\n {skills.length > 0 ? (\n <Box id=\"coaching\" style={{ scrollMarginTop }}>\n <CoachingPanel skills={skills} />\n </Box>\n ) : null}\n </Flex>\n\n <SectionNav items={navItems} steps={showStepDetail ? steps : []} />\n </Grid>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ScriptTranscript
|
|
3
|
+
} from "./chunk-EHDUKUML.js";
|
|
4
|
+
import {
|
|
5
|
+
computeSpeechShare,
|
|
6
|
+
isLengthClose
|
|
7
|
+
} from "./chunk-4YAA25AV.js";
|
|
8
|
+
import {
|
|
9
|
+
Tabs
|
|
10
|
+
} from "./chunk-CGDBUDY3.js";
|
|
11
|
+
import {
|
|
12
|
+
Grid
|
|
13
|
+
} from "./chunk-EGEQY3KT.js";
|
|
14
|
+
import {
|
|
15
|
+
Dialog
|
|
16
|
+
} from "./chunk-KDS7OQOY.js";
|
|
17
|
+
import {
|
|
18
|
+
Box
|
|
19
|
+
} from "./chunk-4Y5BEXVN.js";
|
|
20
|
+
import {
|
|
21
|
+
__async
|
|
22
|
+
} from "./chunk-CRTRMMJ7.js";
|
|
23
|
+
|
|
24
|
+
// src/app/roleplay/parts/session-replay-dialog.tsx
|
|
25
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
26
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
27
|
+
function resolveUrl(raw, disp, resolver) {
|
|
28
|
+
return __async(this, null, function* () {
|
|
29
|
+
if (!raw) return void 0;
|
|
30
|
+
if (!resolver) return raw;
|
|
31
|
+
try {
|
|
32
|
+
return yield resolver(raw, { disposition: disp });
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return raw;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function SessionReplayDialog({
|
|
39
|
+
open,
|
|
40
|
+
onOpenChange,
|
|
41
|
+
detail,
|
|
42
|
+
script,
|
|
43
|
+
onResolveMediaUrl
|
|
44
|
+
}) {
|
|
45
|
+
var _a, _b, _c;
|
|
46
|
+
const videoRef = useRef(null);
|
|
47
|
+
const audioRef = useRef(null);
|
|
48
|
+
const hasVideo = Boolean(detail.trainingVideoURL);
|
|
49
|
+
const hasAudio = Boolean(detail.trainingAudioURL);
|
|
50
|
+
const [tab, setTab] = useState(hasVideo ? "video" : "audio");
|
|
51
|
+
const [currentMs, setCurrentMs] = useState(0);
|
|
52
|
+
const [videoSec, setVideoSec] = useState((_a = detail.videoDurationSec) != null ? _a : null);
|
|
53
|
+
const [audioSec, setAudioSec] = useState((_b = detail.audioDurationSec) != null ? _b : null);
|
|
54
|
+
const [resolvedVideoUrl, setResolvedVideoUrl] = useState(void 0);
|
|
55
|
+
const [resolvedAudioUrl, setResolvedAudioUrl] = useState(void 0);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!open) return;
|
|
58
|
+
void resolveUrl(detail.trainingVideoURL, "inline", onResolveMediaUrl).then(
|
|
59
|
+
setResolvedVideoUrl
|
|
60
|
+
);
|
|
61
|
+
void resolveUrl(detail.trainingAudioURL, "inline", onResolveMediaUrl).then(
|
|
62
|
+
setResolvedAudioUrl
|
|
63
|
+
);
|
|
64
|
+
}, [open, detail.trainingVideoURL, detail.trainingAudioURL, onResolveMediaUrl]);
|
|
65
|
+
const lines = useMemo(() => {
|
|
66
|
+
var _a2;
|
|
67
|
+
return (_a2 = script == null ? void 0 : script.items) != null ? _a2 : [];
|
|
68
|
+
}, [script == null ? void 0 : script.items]);
|
|
69
|
+
const speechShare = useMemo(() => computeSpeechShare(lines), [lines]);
|
|
70
|
+
const sttSec = ((_c = script == null ? void 0 : script.durationMs) != null ? _c : 0) / 1e3;
|
|
71
|
+
const canSync = tab === "video" ? hasVideo && isLengthClose(videoSec, sttSec) : hasAudio && isLengthClose(audioSec, sttSec);
|
|
72
|
+
const activeMediaRef = tab === "video" ? videoRef : audioRef;
|
|
73
|
+
const handleSeek = (startMs) => {
|
|
74
|
+
if (!canSync) return;
|
|
75
|
+
const el = activeMediaRef.current;
|
|
76
|
+
if (!el) return;
|
|
77
|
+
el.currentTime = startMs / 1e3;
|
|
78
|
+
void el.play();
|
|
79
|
+
};
|
|
80
|
+
const handleTimeUpdate = () => {
|
|
81
|
+
const el = activeMediaRef.current;
|
|
82
|
+
if (el) {
|
|
83
|
+
setCurrentMs(el.currentTime * 1e3);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
return /* @__PURE__ */ jsx(Dialog.Root, { onOpenChange, open, children: /* @__PURE__ */ jsx(Dialog.Content, { maxWidth: "1040px", title: "\uC138\uC158 \uB2E4\uC2DC\uBCF4\uAE30", width: "100%", children: /* @__PURE__ */ jsxs(
|
|
87
|
+
Grid,
|
|
88
|
+
{
|
|
89
|
+
columns: { initial: "1", md: "minmax(0, 1fr) minmax(0, 1.05fr)" },
|
|
90
|
+
gap: "5",
|
|
91
|
+
mt: "2",
|
|
92
|
+
children: [
|
|
93
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(
|
|
94
|
+
Tabs.Root,
|
|
95
|
+
{
|
|
96
|
+
onValueChange: (v) => {
|
|
97
|
+
setTab(v);
|
|
98
|
+
},
|
|
99
|
+
value: tab,
|
|
100
|
+
children: [
|
|
101
|
+
/* @__PURE__ */ jsxs(Tabs.List, { children: [
|
|
102
|
+
hasVideo ? /* @__PURE__ */ jsx(Tabs.Trigger, { value: "video", children: "\uC601\uC0C1" }) : null,
|
|
103
|
+
hasAudio ? /* @__PURE__ */ jsx(Tabs.Trigger, { value: "audio", children: "\uC624\uB514\uC624" }) : null
|
|
104
|
+
] }),
|
|
105
|
+
hasVideo ? /* @__PURE__ */ jsx(Tabs.Content, { value: "video", children: /* @__PURE__ */ jsx(
|
|
106
|
+
Box,
|
|
107
|
+
{
|
|
108
|
+
mt: "3",
|
|
109
|
+
style: {
|
|
110
|
+
borderRadius: "var(--radius-3)",
|
|
111
|
+
overflow: "hidden",
|
|
112
|
+
background: "var(--gray-3)"
|
|
113
|
+
},
|
|
114
|
+
children: /* @__PURE__ */ jsx(
|
|
115
|
+
"video",
|
|
116
|
+
{
|
|
117
|
+
controls: true,
|
|
118
|
+
onLoadedMetadata: () => {
|
|
119
|
+
var _a2, _b2;
|
|
120
|
+
setVideoSec((_b2 = (_a2 = videoRef.current) == null ? void 0 : _a2.duration) != null ? _b2 : null);
|
|
121
|
+
},
|
|
122
|
+
onTimeUpdate: handleTimeUpdate,
|
|
123
|
+
ref: videoRef,
|
|
124
|
+
src: resolvedVideoUrl,
|
|
125
|
+
style: { width: "100%", display: "block", maxHeight: 440 }
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
) }) : null,
|
|
130
|
+
hasAudio ? /* @__PURE__ */ jsx(Tabs.Content, { value: "audio", children: /* @__PURE__ */ jsx(Box, { mt: "3", children: /* @__PURE__ */ jsx(
|
|
131
|
+
"audio",
|
|
132
|
+
{
|
|
133
|
+
controls: true,
|
|
134
|
+
onLoadedMetadata: () => {
|
|
135
|
+
var _a2, _b2;
|
|
136
|
+
setAudioSec((_b2 = (_a2 = audioRef.current) == null ? void 0 : _a2.duration) != null ? _b2 : null);
|
|
137
|
+
},
|
|
138
|
+
onTimeUpdate: handleTimeUpdate,
|
|
139
|
+
ref: audioRef,
|
|
140
|
+
src: resolvedAudioUrl,
|
|
141
|
+
style: { width: "100%" }
|
|
142
|
+
}
|
|
143
|
+
) }) }) : null
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
) }),
|
|
147
|
+
/* @__PURE__ */ jsx(
|
|
148
|
+
ScriptTranscript,
|
|
149
|
+
{
|
|
150
|
+
canSync,
|
|
151
|
+
currentMs,
|
|
152
|
+
lines,
|
|
153
|
+
onSeek: handleSeek,
|
|
154
|
+
partnerName: detail.characterTitle,
|
|
155
|
+
speechShare
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
) }) });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
SessionReplayDialog
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=chunk-3UGBMKSQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/app/roleplay/parts/session-replay-dialog.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { Box, Dialog, Grid, Tabs } from '../../../atoms';\nimport { computeSpeechShare, isLengthClose } from '../utils/session-utils';\nimport type { RpSessionDetailData, RpSessionScript } from '../types';\nimport { ScriptTranscript } from './script-transcript';\n\nexport interface SessionReplayDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n detail: RpSessionDetailData;\n script?: RpSessionScript | null;\n onResolveMediaUrl?: (\n rawUrl: string,\n options: { disposition: 'inline' | 'attachment' }\n ) => string | Promise<string>;\n}\n\ntype ReplayTab = 'video' | 'audio';\n\nasync function resolveUrl(\n raw: string | null | undefined,\n disp: 'inline' | 'attachment',\n resolver?: (rawUrl: string, options: { disposition: 'inline' | 'attachment' }) => string | Promise<string>\n): Promise<string | undefined> {\n if (!raw) return undefined;\n if (!resolver) return raw;\n try {\n return await resolver(raw, { disposition: disp });\n } catch {\n return raw;\n }\n}\n\nexport function SessionReplayDialog({\n open,\n onOpenChange,\n detail,\n script,\n onResolveMediaUrl,\n}: SessionReplayDialogProps): React.ReactElement {\n const videoRef = useRef<HTMLVideoElement>(null);\n const audioRef = useRef<HTMLAudioElement>(null);\n\n const hasVideo = Boolean(detail.trainingVideoURL);\n const hasAudio = Boolean(detail.trainingAudioURL);\n\n const [tab, setTab] = useState<ReplayTab>(hasVideo ? 'video' : 'audio');\n const [currentMs, setCurrentMs] = useState(0);\n const [videoSec, setVideoSec] = useState<number | null>(detail.videoDurationSec ?? null);\n const [audioSec, setAudioSec] = useState<number | null>(detail.audioDurationSec ?? null);\n\n const [resolvedVideoUrl, setResolvedVideoUrl] = useState<string | undefined>(undefined);\n const [resolvedAudioUrl, setResolvedAudioUrl] = useState<string | undefined>(undefined);\n\n useEffect(() => {\n if (!open) return;\n void resolveUrl(detail.trainingVideoURL, 'inline', onResolveMediaUrl).then(\n setResolvedVideoUrl\n );\n void resolveUrl(detail.trainingAudioURL, 'inline', onResolveMediaUrl).then(\n setResolvedAudioUrl\n );\n }, [open, detail.trainingVideoURL, detail.trainingAudioURL, onResolveMediaUrl]);\n\n const lines = useMemo(() => script?.items ?? [], [script?.items]);\n const speechShare = useMemo(() => computeSpeechShare(lines), [lines]);\n const sttSec = (script?.durationMs ?? 0) / 1000;\n\n const canSync =\n tab === 'video'\n ? hasVideo && isLengthClose(videoSec, sttSec)\n : hasAudio && isLengthClose(audioSec, sttSec);\n\n const activeMediaRef = tab === 'video' ? videoRef : audioRef;\n\n const handleSeek = (startMs: number): void => {\n if (!canSync) return;\n const el = activeMediaRef.current;\n if (!el) return;\n el.currentTime = startMs / 1000;\n void el.play();\n };\n\n const handleTimeUpdate = (): void => {\n const el = activeMediaRef.current;\n if (el) {\n setCurrentMs(el.currentTime * 1000);\n }\n };\n\n return (\n <Dialog.Root onOpenChange={onOpenChange} open={open}>\n <Dialog.Content maxWidth=\"1040px\" title=\"세션 다시보기\" width=\"100%\">\n <Grid\n columns={{ initial: '1', md: 'minmax(0, 1fr) minmax(0, 1.05fr)' }}\n gap=\"5\"\n mt=\"2\"\n >\n <Box>\n <Tabs.Root\n onValueChange={(v) => {\n setTab(v as ReplayTab);\n }}\n value={tab}\n >\n <Tabs.List>\n {hasVideo ? <Tabs.Trigger value=\"video\">영상</Tabs.Trigger> : null}\n {hasAudio ? <Tabs.Trigger value=\"audio\">오디오</Tabs.Trigger> : null}\n </Tabs.List>\n {hasVideo ? (\n <Tabs.Content value=\"video\">\n <Box\n mt=\"3\"\n style={{\n borderRadius: 'var(--radius-3)',\n overflow: 'hidden',\n background: 'var(--gray-3)',\n }}\n >\n {/* eslint-disable-next-line jsx-a11y/media-has-caption -- 학습 다시보기 영상은 자막 트랙이 없는 사용자 녹화본 */}\n <video\n controls\n onLoadedMetadata={() => {\n setVideoSec(videoRef.current?.duration ?? null);\n }}\n onTimeUpdate={handleTimeUpdate}\n ref={videoRef}\n src={resolvedVideoUrl}\n style={{ width: '100%', display: 'block', maxHeight: 440 }}\n />\n </Box>\n </Tabs.Content>\n ) : null}\n {hasAudio ? (\n <Tabs.Content value=\"audio\">\n <Box mt=\"3\">\n {/* eslint-disable-next-line jsx-a11y/media-has-caption -- 학습 다시보기 오디오는 자막 트랙이 없는 사용자 녹음본 */}\n <audio\n controls\n onLoadedMetadata={() => {\n setAudioSec(audioRef.current?.duration ?? null);\n }}\n onTimeUpdate={handleTimeUpdate}\n ref={audioRef}\n src={resolvedAudioUrl}\n style={{ width: '100%' }}\n />\n </Box>\n </Tabs.Content>\n ) : null}\n </Tabs.Root>\n </Box>\n\n <ScriptTranscript\n canSync={canSync}\n currentMs={currentMs}\n lines={lines}\n onSeek={handleSeek}\n partnerName={detail.characterTitle}\n speechShare={speechShare}\n />\n </Grid>\n </Dialog.Content>\n </Dialog.Root>\n );\n}\n"],"mappings":"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","names":["_a","_b"]}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// src/app/roleplay/utils/session-utils.ts
|
|
2
|
+
import { format } from "date-fns";
|
|
3
|
+
var COLOR_VARS = {
|
|
4
|
+
green: {
|
|
5
|
+
fg: "var(--green-11)",
|
|
6
|
+
bg: "var(--green-a3)",
|
|
7
|
+
border: "var(--green-a6)"
|
|
8
|
+
},
|
|
9
|
+
amber: {
|
|
10
|
+
fg: "var(--amber-11)",
|
|
11
|
+
bg: "var(--amber-a3)",
|
|
12
|
+
border: "var(--amber-a6)"
|
|
13
|
+
},
|
|
14
|
+
red: { fg: "var(--red-11)", bg: "var(--red-a3)", border: "var(--red-a6)" },
|
|
15
|
+
gray: { fg: "var(--gray-9)", bg: "var(--gray-a3)", border: "var(--gray-5)" }
|
|
16
|
+
};
|
|
17
|
+
function qualityColor(quality) {
|
|
18
|
+
switch (quality) {
|
|
19
|
+
case "\uCDA9\uC871":
|
|
20
|
+
return "green";
|
|
21
|
+
case "\uBD80\uBD84\uCDA9\uC871":
|
|
22
|
+
return "amber";
|
|
23
|
+
case "\uBBF8\uD761":
|
|
24
|
+
return "red";
|
|
25
|
+
default:
|
|
26
|
+
return "gray";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
var QUALITY_RATIO = {
|
|
30
|
+
\uCDA9\uC871: 1,
|
|
31
|
+
\uBD80\uBD84\uCDA9\uC871: 0.6,
|
|
32
|
+
\uBBF8\uD761: 0.3
|
|
33
|
+
};
|
|
34
|
+
function qualityRatio(quality) {
|
|
35
|
+
return quality === null ? null : QUALITY_RATIO[quality];
|
|
36
|
+
}
|
|
37
|
+
function gradeColorFromLabel(label) {
|
|
38
|
+
if (!label) return "gray";
|
|
39
|
+
if (label.includes("\uBBF8\uD761") || label.includes("\uBD80\uC871") || label.includes("\uBBF8\uB2EC"))
|
|
40
|
+
return "red";
|
|
41
|
+
if (label.includes("\uBD80\uBD84") || label.includes("\uBCF4\uD1B5")) return "amber";
|
|
42
|
+
if (label.includes("\uCDA9\uC871") || label.includes("\uC6B0\uC218") || label.includes("\uCDA9\uBD84"))
|
|
43
|
+
return "green";
|
|
44
|
+
return "gray";
|
|
45
|
+
}
|
|
46
|
+
function stepColor(step) {
|
|
47
|
+
return step.quality !== null ? qualityColor(step.quality) : gradeColorFromLabel(step.qualityLabel);
|
|
48
|
+
}
|
|
49
|
+
function stepVars(step) {
|
|
50
|
+
return COLOR_VARS[stepColor(step)];
|
|
51
|
+
}
|
|
52
|
+
function stepRatio(step) {
|
|
53
|
+
if (step.maxScore !== null && step.maxScore > 0 && step.score !== null) {
|
|
54
|
+
return Math.max(0, Math.min(1, step.score / step.maxScore));
|
|
55
|
+
}
|
|
56
|
+
return qualityRatio(step.quality);
|
|
57
|
+
}
|
|
58
|
+
function stepScoreText(step) {
|
|
59
|
+
if (step.score === null) return null;
|
|
60
|
+
return step.maxScore !== null ? `${step.score}/${step.maxScore}` : `${step.score}`;
|
|
61
|
+
}
|
|
62
|
+
function stepGradeLabel(step) {
|
|
63
|
+
return step.qualityLabel;
|
|
64
|
+
}
|
|
65
|
+
function hasStepSignal(step) {
|
|
66
|
+
return step.score !== null || step.qualityLabel !== null;
|
|
67
|
+
}
|
|
68
|
+
var LETTER_RE = /[A-Za-z]/;
|
|
69
|
+
function stepCode(step) {
|
|
70
|
+
if (step.initial !== null && step.initial.trim().length > 0) {
|
|
71
|
+
return step.initial.trim();
|
|
72
|
+
}
|
|
73
|
+
const label = step.label.trim();
|
|
74
|
+
const first = label.charAt(0);
|
|
75
|
+
const second = label.charAt(1);
|
|
76
|
+
if (LETTER_RE.test(first) && (second === " " || second === "\u2014" || second === "-")) {
|
|
77
|
+
return first.toUpperCase();
|
|
78
|
+
}
|
|
79
|
+
if (first === "[" && LETTER_RE.test(second) && label.charAt(2) === "]") {
|
|
80
|
+
return second.toUpperCase();
|
|
81
|
+
}
|
|
82
|
+
return step.key.split("_")[0].slice(0, 4).toUpperCase();
|
|
83
|
+
}
|
|
84
|
+
function formatDuration(seconds) {
|
|
85
|
+
if (seconds === null) return "\u2014";
|
|
86
|
+
const m = Math.floor(seconds / 60);
|
|
87
|
+
const s = seconds % 60;
|
|
88
|
+
return `${m}\uBD84 ${String(s).padStart(2, "0")}\uCD08`;
|
|
89
|
+
}
|
|
90
|
+
function formatDate(iso) {
|
|
91
|
+
const d = new Date(iso);
|
|
92
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
93
|
+
return format(d, "M/d HH:mm");
|
|
94
|
+
}
|
|
95
|
+
function formatTimestamp(ms) {
|
|
96
|
+
const total = Math.floor(ms / 1e3);
|
|
97
|
+
const m = Math.floor(total / 60);
|
|
98
|
+
const s = total % 60;
|
|
99
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
100
|
+
}
|
|
101
|
+
var SYNC_TOLERANCE = 0.05;
|
|
102
|
+
function isLengthClose(mediaSec, sttSec) {
|
|
103
|
+
if (!mediaSec || mediaSec <= 0 || sttSec <= 0) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return Math.abs(mediaSec - sttSec) / Math.max(mediaSec, sttSec) <= SYNC_TOLERANCE;
|
|
107
|
+
}
|
|
108
|
+
var SPEECH_OVERTALK_THRESHOLD = 40;
|
|
109
|
+
function computeSpeechShare(lines) {
|
|
110
|
+
let userChars = 0;
|
|
111
|
+
let partnerChars = 0;
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
const count = line.text.replace(/\s/g, "").length;
|
|
114
|
+
if (line.speaker === "user") {
|
|
115
|
+
userChars += count;
|
|
116
|
+
} else {
|
|
117
|
+
partnerChars += count;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const total = userChars + partnerChars;
|
|
121
|
+
if (total === 0) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const userPct = Math.round(userChars / total * 100);
|
|
125
|
+
return { userPct, partnerPct: 100 - userPct };
|
|
126
|
+
}
|
|
127
|
+
var STATUS_LABEL = {
|
|
128
|
+
complete: "\uBD84\uC11D \uC644\uB8CC",
|
|
129
|
+
completed: "\uBD84\uC11D \uC644\uB8CC",
|
|
130
|
+
analyzing: "AI \uBD84\uC11D \uC911",
|
|
131
|
+
partial: "\uC77C\uBD80 \uC0B0\uCD9C\uBB3C \uC5C6\uC74C"
|
|
132
|
+
};
|
|
133
|
+
function statusLabelOf(status) {
|
|
134
|
+
var _a;
|
|
135
|
+
if (!status) return "\uC138\uC158";
|
|
136
|
+
return (_a = STATUS_LABEL[status]) != null ? _a : status;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export {
|
|
140
|
+
COLOR_VARS,
|
|
141
|
+
qualityColor,
|
|
142
|
+
QUALITY_RATIO,
|
|
143
|
+
qualityRatio,
|
|
144
|
+
gradeColorFromLabel,
|
|
145
|
+
stepColor,
|
|
146
|
+
stepVars,
|
|
147
|
+
stepRatio,
|
|
148
|
+
stepScoreText,
|
|
149
|
+
stepGradeLabel,
|
|
150
|
+
hasStepSignal,
|
|
151
|
+
stepCode,
|
|
152
|
+
formatDuration,
|
|
153
|
+
formatDate,
|
|
154
|
+
formatTimestamp,
|
|
155
|
+
SYNC_TOLERANCE,
|
|
156
|
+
isLengthClose,
|
|
157
|
+
SPEECH_OVERTALK_THRESHOLD,
|
|
158
|
+
computeSpeechShare,
|
|
159
|
+
statusLabelOf
|
|
160
|
+
};
|
|
161
|
+
//# sourceMappingURL=chunk-4YAA25AV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/app/roleplay/utils/session-utils.ts"],"sourcesContent":["// 롤플레잉 세션 상세 순수 유틸 함수 모음.\n// 원본: website/src/modules/customer/roleplay/pages/RpSessionDetail/RpSessionDetail.tsx\n// 'use client' 없음 — 순수 함수, 클라이언트 전용 아님.\n\nimport { format } from 'date-fns';\nimport type { RpSessionScriptLine } from '../types';\nimport type { RpQuality, RpReportStep } from './report-parse';\n\n/* ───────────────────────── 타입 ───────────────────────── */\n\nexport interface SpeechShare { userPct: number; partnerPct: number }\n\nexport type QColor = 'green' | 'amber' | 'red' | 'gray';\n\n/* ───────────────────────── 색 변수 ───────────────────────── */\n\nexport const COLOR_VARS: Record<QColor, { fg: string; bg: string; border: string }> = {\n green: {\n fg: 'var(--green-11)',\n bg: 'var(--green-a3)',\n border: 'var(--green-a6)',\n },\n amber: {\n fg: 'var(--amber-11)',\n bg: 'var(--amber-a3)',\n border: 'var(--amber-a6)',\n },\n red: { fg: 'var(--red-11)', bg: 'var(--red-a3)', border: 'var(--red-a6)' },\n gray: { fg: 'var(--gray-9)', bg: 'var(--gray-a3)', border: 'var(--gray-5)' },\n};\n\n/* ───────────────────────── 등급 색 ───────────────────────── */\n\nexport function qualityColor(quality: RpQuality | null): QColor {\n switch (quality) {\n case '충족':\n return 'green';\n case '부분충족':\n return 'amber';\n case '미흡':\n return 'red';\n default:\n return 'gray';\n }\n}\n\nexport const QUALITY_RATIO: Record<RpQuality, number> = {\n 충족: 1,\n 부분충족: 0.6,\n 미흡: 0.3,\n} as const;\n\nexport function qualityRatio(quality: RpQuality | null): number | null {\n return quality === null ? null : QUALITY_RATIO[quality];\n}\n\n// 표준 등급이 아닌 자유 등급 문자열에서 색만 키워드로 추정(비율은 산출하지 않음).\nexport function gradeColorFromLabel(label: string | null): QColor {\n if (!label) return 'gray';\n if (\n label.includes('미흡') ||\n label.includes('부족') ||\n label.includes('미달')\n )\n return 'red';\n if (label.includes('부분') || label.includes('보통')) return 'amber';\n if (\n label.includes('충족') ||\n label.includes('우수') ||\n label.includes('충분')\n )\n return 'green';\n return 'gray';\n}\n\n/* ───────────────────────── 단계(Step) 헬퍼 ───────────────────────── */\n\n// 단계 색: 표준 등급 우선, 없으면 라벨 키워드 추정.\nexport function stepColor(step: RpReportStep): QColor {\n return step.quality !== null\n ? qualityColor(step.quality)\n : gradeColorFromLabel(step.qualityLabel);\n}\n\nexport function stepVars(step: RpReportStep): { fg: string; bg: string; border: string } {\n return COLOR_VARS[stepColor(step)];\n}\n\n// progress/radar 정규화 비율(0~1). score/maxScore 우선, 없으면 표준 등급 비율, 둘 다 없으면 null.\nexport function stepRatio(step: RpReportStep): number | null {\n if (step.maxScore !== null && step.maxScore > 0 && step.score !== null) {\n return Math.max(0, Math.min(1, step.score / step.maxScore));\n }\n return qualityRatio(step.quality);\n}\n\n// 점수 텍스트: max 있으면 'score/max', 없으면 'score', score 없으면 null.\nexport function stepScoreText(step: RpReportStep): string | null {\n if (step.score === null) return null;\n return step.maxScore !== null\n ? `${step.score}/${step.maxScore}`\n : `${step.score}`;\n}\n\n// 표시할 등급 라벨(자유 문자열 포함). 없으면 null.\nexport function stepGradeLabel(step: RpReportStep): string | null {\n return step.qualityLabel;\n}\n\n// 단계에 표시할 평가 정보가 하나라도 있는지(점수 또는 등급).\nexport function hasStepSignal(step: RpReportStep): boolean {\n return step.score !== null || step.qualityLabel !== null;\n}\n\n// radar 축/요약 라벨용 짧은 코드.\n// displayMeta.steps.initial 이 지정돼 있으면 최우선(이니셜 충돌 해소).\n// 없으면 기존 휴리스틱: acronym 프레임워크는 선두 글자, 아니면 키 약식.\nconst LETTER_RE = /[A-Za-z]/;\n\nexport function stepCode(step: RpReportStep): string {\n if (step.initial !== null && step.initial.trim().length > 0) {\n return step.initial.trim();\n }\n const label = step.label.trim();\n const first = label.charAt(0);\n const second = label.charAt(1);\n // \"O — Opening\" / \"O - Opening\" / \"O Opening\" 형태\n if (\n LETTER_RE.test(first) &&\n (second === ' ' || second === '—' || second === '-')\n ) {\n return first.toUpperCase();\n }\n // \"[I] Introduction\" 형태\n if (first === '[' && LETTER_RE.test(second) && label.charAt(2) === ']') {\n return second.toUpperCase();\n }\n return step.key.split('_')[0].slice(0, 4).toUpperCase();\n}\n\n/* ───────────────────────── 포맷 유틸 ───────────────────────── */\n\nexport function formatDuration(seconds: number | null): string {\n if (seconds === null) return '—';\n const m = Math.floor(seconds / 60);\n const s = seconds % 60;\n return `${m}분 ${String(s).padStart(2, '0')}초`;\n}\n\nexport function formatDate(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return iso;\n return format(d, 'M/d HH:mm');\n}\n\nexport function formatTimestamp(ms: number): string {\n const total = Math.floor(ms / 1000);\n const m = Math.floor(total / 60);\n const s = total % 60;\n return `${m}:${String(s).padStart(2, '0')}`;\n}\n\n/* ───────────────────────── 미디어 싱크 ───────────────────────── */\n\n// 미디어 길이와 STT 길이가 이 비율 이내로 가까우면 싱크(클릭 seek/재생 하이라이트) 활성.\nexport const SYNC_TOLERANCE = 0.05;\n\nexport function isLengthClose(mediaSec: number | null, sttSec: number): boolean {\n if (!mediaSec || mediaSec <= 0 || sttSec <= 0) {\n return false;\n }\n return (\n Math.abs(mediaSec - sttSec) / Math.max(mediaSec, sttSec) <= SYNC_TOLERANCE\n );\n}\n\n/* ───────────────────────── 발화량 ───────────────────────── */\n\n// 내 발화량이 이 비율을 넘으면 과다 발화로 간주(빨간 막대 + 주의 문구).\nexport const SPEECH_OVERTALK_THRESHOLD = 40;\n\n// 화자별 발화량 비율. 기준은 공백·개행 제거 글자 수(실제 말한 양에 근접, 공백 패턴 왜곡 방지).\n// 발화 글자가 전혀 없으면 null → 미표시.\nexport function computeSpeechShare(lines: RpSessionScriptLine[]): SpeechShare | null {\n let userChars = 0;\n let partnerChars = 0;\n for (const line of lines) {\n const count = line.text.replace(/\\s/g, '').length;\n if (line.speaker === 'user') {\n userChars += count;\n } else {\n partnerChars += count;\n }\n }\n const total = userChars + partnerChars;\n if (total === 0) {\n return null;\n }\n const userPct = Math.round((userChars / total) * 100);\n return { userPct, partnerPct: 100 - userPct };\n}\n\n/* ───────────────────────── 세션 상태 ───────────────────────── */\n\nconst STATUS_LABEL: Record<string, string> = {\n complete: '분석 완료',\n completed: '분석 완료',\n analyzing: 'AI 분석 중',\n partial: '일부 산출물 없음',\n};\n\nexport function statusLabelOf(status: string | null): string {\n if (!status) return '세션';\n return STATUS_LABEL[status] ?? status;\n}\n"],"mappings":";AAIA,SAAS,cAAc;AAYhB,IAAM,aAAyE;AAAA,EACpF,OAAO;AAAA,IACL,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,QAAQ;AAAA,EACV;AAAA,EACA,OAAO;AAAA,IACL,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,QAAQ;AAAA,EACV;AAAA,EACA,KAAK,EAAE,IAAI,iBAAiB,IAAI,iBAAiB,QAAQ,gBAAgB;AAAA,EACzE,MAAM,EAAE,IAAI,iBAAiB,IAAI,kBAAkB,QAAQ,gBAAgB;AAC7E;AAIO,SAAS,aAAa,SAAmC;AAC9D,UAAQ,SAAS;AAAA,IACf,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAEO,IAAM,gBAA2C;AAAA,EACtD,cAAI;AAAA,EACJ,0BAAM;AAAA,EACN,cAAI;AACN;AAEO,SAAS,aAAa,SAA0C;AACrE,SAAO,YAAY,OAAO,OAAO,cAAc,OAAO;AACxD;AAGO,SAAS,oBAAoB,OAA8B;AAChE,MAAI,CAAC,MAAO,QAAO;AACnB,MACE,MAAM,SAAS,cAAI,KACnB,MAAM,SAAS,cAAI,KACnB,MAAM,SAAS,cAAI;AAEnB,WAAO;AACT,MAAI,MAAM,SAAS,cAAI,KAAK,MAAM,SAAS,cAAI,EAAG,QAAO;AACzD,MACE,MAAM,SAAS,cAAI,KACnB,MAAM,SAAS,cAAI,KACnB,MAAM,SAAS,cAAI;AAEnB,WAAO;AACT,SAAO;AACT;AAKO,SAAS,UAAU,MAA4B;AACpD,SAAO,KAAK,YAAY,OACpB,aAAa,KAAK,OAAO,IACzB,oBAAoB,KAAK,YAAY;AAC3C;AAEO,SAAS,SAAS,MAAgE;AACvF,SAAO,WAAW,UAAU,IAAI,CAAC;AACnC;AAGO,SAAS,UAAU,MAAmC;AAC3D,MAAI,KAAK,aAAa,QAAQ,KAAK,WAAW,KAAK,KAAK,UAAU,MAAM;AACtE,WAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,QAAQ,KAAK,QAAQ,CAAC;AAAA,EAC5D;AACA,SAAO,aAAa,KAAK,OAAO;AAClC;AAGO,SAAS,cAAc,MAAmC;AAC/D,MAAI,KAAK,UAAU,KAAM,QAAO;AAChC,SAAO,KAAK,aAAa,OACrB,GAAG,KAAK,KAAK,IAAI,KAAK,QAAQ,KAC9B,GAAG,KAAK,KAAK;AACnB;AAGO,SAAS,eAAe,MAAmC;AAChE,SAAO,KAAK;AACd;AAGO,SAAS,cAAc,MAA6B;AACzD,SAAO,KAAK,UAAU,QAAQ,KAAK,iBAAiB;AACtD;AAKA,IAAM,YAAY;AAEX,SAAS,SAAS,MAA4B;AACnD,MAAI,KAAK,YAAY,QAAQ,KAAK,QAAQ,KAAK,EAAE,SAAS,GAAG;AAC3D,WAAO,KAAK,QAAQ,KAAK;AAAA,EAC3B;AACA,QAAM,QAAQ,KAAK,MAAM,KAAK;AAC9B,QAAM,QAAQ,MAAM,OAAO,CAAC;AAC5B,QAAM,SAAS,MAAM,OAAO,CAAC;AAE7B,MACE,UAAU,KAAK,KAAK,MACnB,WAAW,OAAO,WAAW,YAAO,WAAW,MAChD;AACA,WAAO,MAAM,YAAY;AAAA,EAC3B;AAEA,MAAI,UAAU,OAAO,UAAU,KAAK,MAAM,KAAK,MAAM,OAAO,CAAC,MAAM,KAAK;AACtE,WAAO,OAAO,YAAY;AAAA,EAC5B;AACA,SAAO,KAAK,IAAI,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE,YAAY;AACxD;AAIO,SAAS,eAAe,SAAgC;AAC7D,MAAI,YAAY,KAAM,QAAO;AAC7B,QAAM,IAAI,KAAK,MAAM,UAAU,EAAE;AACjC,QAAM,IAAI,UAAU;AACpB,SAAO,GAAG,CAAC,UAAK,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC;AAC5C;AAEO,SAAS,WAAW,KAAqB;AAC9C,QAAM,IAAI,IAAI,KAAK,GAAG;AACtB,MAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AACtC,SAAO,OAAO,GAAG,WAAW;AAC9B;AAEO,SAAS,gBAAgB,IAAoB;AAClD,QAAM,QAAQ,KAAK,MAAM,KAAK,GAAI;AAClC,QAAM,IAAI,KAAK,MAAM,QAAQ,EAAE;AAC/B,QAAM,IAAI,QAAQ;AAClB,SAAO,GAAG,CAAC,IAAI,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC;AAC3C;AAKO,IAAM,iBAAiB;AAEvB,SAAS,cAAc,UAAyB,QAAyB;AAC9E,MAAI,CAAC,YAAY,YAAY,KAAK,UAAU,GAAG;AAC7C,WAAO;AAAA,EACT;AACA,SACE,KAAK,IAAI,WAAW,MAAM,IAAI,KAAK,IAAI,UAAU,MAAM,KAAK;AAEhE;AAKO,IAAM,4BAA4B;AAIlC,SAAS,mBAAmB,OAAkD;AACnF,MAAI,YAAY;AAChB,MAAI,eAAe;AACnB,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,KAAK,KAAK,QAAQ,OAAO,EAAE,EAAE;AAC3C,QAAI,KAAK,YAAY,QAAQ;AAC3B,mBAAa;AAAA,IACf,OAAO;AACL,sBAAgB;AAAA,IAClB;AAAA,EACF;AACA,QAAM,QAAQ,YAAY;AAC1B,MAAI,UAAU,GAAG;AACf,WAAO;AAAA,EACT;AACA,QAAM,UAAU,KAAK,MAAO,YAAY,QAAS,GAAG;AACpD,SAAO,EAAE,SAAS,YAAY,MAAM,QAAQ;AAC9C;AAIA,IAAM,eAAuC;AAAA,EAC3C,UAAU;AAAA,EACV,WAAW;AAAA,EACX,WAAW;AAAA,EACX,SAAS;AACX;AAEO,SAAS,cAAc,QAA+B;AAnN7D;AAoNE,MAAI,CAAC,OAAQ,QAAO;AACpB,UAAO,kBAAa,MAAM,MAAnB,YAAwB;AACjC;","names":[]}
|