한신대 피드백 개선팩 반영

This commit is contained in:
Yun Chan 2026-07-03 19:53:14 +09:00
parent 5a9c110c11
commit 6b6241f468
25 changed files with 1247 additions and 94 deletions

View file

@ -4006,6 +4006,8 @@ export interface components {
crisis_resource?: components["schemas"]["CrisisResourceResponse"] | null;
/** Effective Openness */
effective_openness: number;
/** Output Error */
output_error?: string | null;
/**
* Safety Flagged
* @default false

View file

@ -278,6 +278,7 @@ export interface SessionStreamDone {
crisis_kind?: string;
crisis_resource?: CrisisResource | null;
conversation_stopped?: boolean;
output_error?: string | null;
}
function safeParse(data: string): unknown {
@ -346,6 +347,7 @@ export async function openSessionStream(
crisis_kind: parsed.crisis_kind,
crisis_resource: parsed.crisis_resource,
conversation_stopped: parsed.conversation_stopped,
output_error: parsed.output_error,
};
handlers.onDone?.(donePayload);
return;

View file

@ -1503,6 +1503,11 @@ export default function Session() {
if (done.conversation_stopped || done.crisis_resource) {
conversationStopped = true;
}
const qualityRetryable = done.output_error === "client_reply_quality_retryable";
if (qualityRetryable) {
setTurnError("내담자 응답 품질을 확인하지 못했습니다. 발화를 조금 다듬어 다시 시도해 주세요.");
pushSignal("warn", "내담자 응답 재시도 필요");
}
if (clientId != null) {
const id = clientId;
@ -1528,12 +1533,12 @@ export default function Session() {
setUtterances((prev) =>
prev.map((u) => (u.id === learnerId ? { ...u, turnSeq: done.turn_seq } : u)),
);
if (!conversationStopped) {
if (!conversationStopped && !qualityRetryable) {
pushSignal("warn", "내담자 응답 없음");
}
}
setAvatarState("listening");
if (!conversationStopped) {
if (!conversationStopped && !qualityRetryable) {
void requestLiveCoach({
learnerText: text,
clientReply,

View file

@ -115,6 +115,35 @@ function renderSummary(summary: string) {
});
}
function displayReviewSummary(summary: string) {
if (/(?:사유:\s*)?(?:engine_error|session evaluation timeout|timeout|RuntimeError)/i.test(summary)) {
return "저장된 축어록은 확인했지만 deep-loop 평가 AI 산출물을 표시하지 못했습니다. AI 평가 재시도가 필요합니다.";
}
return summary;
}
function displayEvaluationRetryError(message: string) {
if (!message.trim()) return "AI 평가를 다시 실행하지 못했습니다.";
return "AI 평가 재시도를 완료하지 못했습니다. 잠시 뒤 다시 실행해 주세요.";
}
const REVIEW_DISPLAY_PLACEHOLDERS: Record<string, string> = {
"[NAME]": "익명 내담자",
"[ORG]": "소속 기관",
"[PHONE]": "연락처",
"[EMAIL]": "이메일",
"[RRN]": "주민등록번호",
"[ADDRESS]": "주소",
};
function displayTranscriptText(text: string) {
let next = text;
for (const [placeholder, label] of Object.entries(REVIEW_DISPLAY_PLACEHOLDERS)) {
next = next.split(placeholder).join(label);
}
return next;
}
function shouldPollReviewReady(data: SessionReviewResponse) {
return (
!data.reviewReady &&
@ -298,6 +327,27 @@ function SupervisorCallout({ note }: { note: ReviewNote }) {
);
}
function EvaluationScopeNotice() {
return (
<div className="sr-eval-scope" aria-label="AI 평가 범위 안내">
<div className="sr-eval-scope__title">
<Icon name="info" size={14} strokeWidth={2.2} />
<b>AI </b>
</div>
<dl>
<div>
<dt> </dt>
<dd>fast-loop는 , deep-loop는 .</dd>
</div>
<div>
<dt> </dt>
<dd> , , .</dd>
</div>
</dl>
</div>
);
}
function JumpablePoint({
point,
turns,
@ -838,13 +888,13 @@ export default function SessionReview() {
}
setReviewReloadSeq((seq) => seq + 1);
} catch (err) {
setEvaluationRetryError(
const message =
err instanceof ApiError
? err.detail
: err instanceof Error
? err.message
: "AI 평가를 다시 실행하지 못했습니다.",
);
: "";
setEvaluationRetryError(displayEvaluationRetryError(message));
} finally {
setEvaluationRetrying(false);
}
@ -1042,7 +1092,7 @@ export default function SessionReview() {
<Card className="sr-overview">
<Kicker> </Kicker>
<p className="sr-summary">
{renderSummary(data.summary)}
{renderSummary(displayReviewSummary(data.summary))}
</p>
<div className="sr-readiness" aria-label="리뷰 생성 상태">
<span>
@ -1250,7 +1300,7 @@ export default function SessionReview() {
(turn.speaker === "client" ? " sr-turn__said--client" : "")
}
>
{turn.text}
{displayTranscriptText(turn.text)}
</p>
{turn.note ? <SupervisorCallout note={turn.note} /> : null}
</div>
@ -1279,6 +1329,7 @@ export default function SessionReview() {
{teacherReviewStatusLabel}
</Badge>
</div>
<EvaluationScopeNotice />
<label className="sr-teacher-review__note">
<span> </span>
<textarea
@ -1491,7 +1542,7 @@ export default function SessionReview() {
<div className={`sr-feedback${data.clientFeedback ? " sr-feedback--filled" : ""}`}>
<div className="sr-feedback__kicker">
<span className="sr-technique__dot" aria-hidden="true" />
</div>
{data.clientFeedback ? (
<>

View file

@ -311,6 +311,45 @@
.sr-teacher-review__head--sub {
margin-bottom: var(--sp-2);
}
.sr-eval-scope {
display: grid;
gap: var(--sp-2);
margin-bottom: var(--sp-3);
padding: 10px 11px;
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
background: var(--bg-surface-2);
}
.sr-eval-scope__title {
display: inline-flex;
align-items: center;
gap: 7px;
color: var(--text-strong);
font-size: var(--fs-xs);
line-height: 1.35;
}
.sr-eval-scope dl {
display: grid;
gap: 7px;
margin: 0;
}
.sr-eval-scope dl div {
display: grid;
gap: 2px;
}
.sr-eval-scope dt {
color: var(--text-strong);
font-size: 11px;
font-weight: 740;
line-height: 1.35;
}
.sr-eval-scope dd {
margin: 0;
color: var(--text-muted);
font-size: 11px;
line-height: 1.5;
overflow-wrap: anywhere;
}
.sr-teacher-review__worksheet {
margin-top: var(--sp-4);
padding-top: var(--sp-4);
@ -1468,6 +1507,7 @@
[data-theme="dark"] .sr-readiness span,
[data-theme="dark"] .sr-empty,
[data-theme="dark"] .sr-chip-toggle,
[data-theme="dark"] .sr-eval-scope,
[data-theme="dark"] .sr-ws-item,
[data-theme="dark"] .sr-prepost__field input,
[data-theme="dark"] .sr-feedback,

View file

@ -906,6 +906,8 @@
}
.sx-transcript__scroll {
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
max-height: none;
flex: 1;
min-height: 160px;
@ -913,6 +915,10 @@
display: flex;
flex-direction: column;
}
.sx-transcript__scroll::-webkit-scrollbar {
width: 0;
height: 0;
}
.sx-transcript__empty {
min-height: 100%;
display: grid;
@ -3686,6 +3692,9 @@
.sx-mobile-context__row span {
padding: 7px 6px;
}
.sx-page--active .sx-transcript__scroll {
min-height: 152px;
}
.sx-page--active .sx-controlbar {
padding: 8px;
gap: 8px;
@ -3833,6 +3842,25 @@
.sx-page--active .sx-stage__now {
display: none;
}
.sx-page--active .sx-transcript__scroll {
min-height: 110px;
}
.sx-page--active .sx-transcript__empty {
grid-template-columns: minmax(0, 1fr);
gap: 2px;
padding: 2px 0;
}
.sx-page--active .sx-transcript__empty-dot {
display: none;
}
.sx-page--active .sx-transcript__empty b {
font-size: 12px;
line-height: 1.25;
}
.sx-page--active .sx-transcript__empty span {
font-size: 11px;
line-height: 1.25;
}
.sx-page--active .sx-mic-block__ms {
display: none;
}
@ -3900,14 +3928,18 @@
height: 58px !important;
}
.sx-page--active .sx-transcript {
padding: 6px 8px;
padding: 3px 6px;
}
.sx-page--active .sx-transcript__head {
margin-bottom: 4px;
margin-bottom: 2px;
}
.sx-page--active .sx-transcript__empty {
gap: 8px;
padding: 8px 6px;
}
.sx-page--active .sx-compose {
margin-top: 5px;
padding-top: 5px;
margin-top: 3px;
padding-top: 3px;
}
.sx-page--active .sx-compose textarea {
min-height: 36px;