현재 작업 전체 반영

This commit is contained in:
Yun Chan 2026-06-27 16:08:41 +09:00
parent 5560638e54
commit c0dddab594
85 changed files with 11322 additions and 539 deletions

View file

@ -30,6 +30,7 @@ import {
apiWsUrl,
personaApi,
sessionApi,
type CrisisResource,
type PersonaSummary,
type SessionDetailResponse,
type SessionStage,
@ -65,6 +66,8 @@ interface VoiceEvent {
stage?: SessionStage;
effective_openness?: number;
safety_flagged?: boolean;
crisis_resource?: CrisisResource | null;
conversation_stopped?: boolean;
}
interface Utterance {
@ -403,6 +406,7 @@ export default function Session() {
const [utterances, setUtterances] = useState<Utterance[]>([]);
const [openness, setOpenness] = useState(0);
const [safety, setSafety] = useState<string | null>(null);
const [crisisResource, setCrisisResource] = useState<CrisisResource | null>(null);
const [turnError, setTurnError] = useState<string | null>(null);
// ── 음성/턴 UI 상태 ──
@ -452,6 +456,8 @@ export default function Session() {
setStarted(false);
setUtterances([]);
setElapsed(0);
setSafety(null);
setCrisisResource(null);
setResumedSessionLoaded(false);
setStartError(null);
}, [navigate, routeIsSessionId, routeParam]);
@ -537,6 +543,22 @@ export default function Session() {
setSignalSeq((seq) => [...seq.slice(-4), tone]);
}, []);
const applyCrisisGate = useCallback((resource?: CrisisResource | null) => {
const fallback: CrisisResource = {
title: "자살예방상담전화 109",
number: "109",
message: "지금은 연습을 멈추고 실제 안전 확인이 먼저입니다.",
};
const next = resource ?? fallback;
setCrisisResource(next);
setSafety(next.message);
setPaused(true);
setMicOn(false);
setVoiceStatus("idle");
setVoiceDetail("위기 신호가 감지되어 연습을 중단했습니다.");
pushSignal("warn", "위기 안전게이트 작동");
}, [pushSignal]);
useEffect(() => {
if (!routeIsSessionId) return;
let alive = true;
@ -567,6 +589,8 @@ export default function Session() {
setElapsed(elapsedFromSession(detail));
setStarted(true);
setPaused(false);
setSafety(null);
setCrisisResource(null);
setAvatarState("idle");
setMicOn(false);
setVoiceStatus("idle");
@ -610,6 +634,8 @@ export default function Session() {
setStage(res.stage);
setOpenness(res.effective_openness);
setUtterances([]);
setSafety(null);
setCrisisResource(null);
setElapsed(0);
if (res.degraded) {
pushSignal("neutral", "서버 기록 제한");
@ -700,9 +726,17 @@ export default function Session() {
if (data.safety_flagged) {
setSafety("방금 발화에서 위기 신호가 감지됐어요. 안전을 우선해 머물러 주세요.");
}
if (data.conversation_stopped || data.crisis_resource) {
applyCrisisGate(data.crisis_resource);
}
},
onSafety: () => {
onSafety: (payload) => {
setSafety("방금 발화에서 위기 신호가 감지됐어요. 안전을 우선해 머물러 주세요.");
if (payload && typeof payload === "object") {
const resource = (payload as { crisis_resource?: CrisisResource }).crisis_resource;
const stopped = (payload as { conversation_stopped?: boolean }).conversation_stopped;
if (resource || stopped) applyCrisisGate(resource);
}
},
});
@ -994,6 +1028,9 @@ export default function Session() {
if (payload.safety_flagged) {
setSafety("방금 발화에서 위기 신호가 감지됐어요. 안전을 우선해 머물러 주세요.");
}
if (payload.conversation_stopped || payload.crisis_resource) {
applyCrisisGate(payload.crisis_resource);
}
const pendingId = pendingVoiceLearnerIdRef.current;
if (pendingId != null) {
setUtterances((prev) =>
@ -1748,6 +1785,12 @@ export default function Session() {
</span>
<span className="sx-safety__text">
<b> </b> · {safety}
{crisisResource ? (
<span className="sx-crisis-resource">
<strong>{crisisResource.title}</strong>
<a href={`tel:${crisisResource.number}`}>{crisisResource.number}</a>
</span>
) : null}
</span>
</section>
) : null}