feat: 운영 안정성과 세션 음성 경험 개선
This commit is contained in:
parent
facc4ad2d9
commit
c788343467
95 changed files with 8431 additions and 1785 deletions
|
|
@ -93,14 +93,14 @@ const STUDIO_TABS: { key: StudioTab; label: string }[] = [
|
|||
];
|
||||
|
||||
const AUTHORING_STEPS = [
|
||||
{ key: "source", label: "자료", desc: "기록·교재·가이드 정리" },
|
||||
{ key: "source", label: "자료", desc: "기록, 교재, 가이드 정리" },
|
||||
{ key: "generate", label: "초안", desc: "근거를 확인하고 초안 생성" },
|
||||
{ key: "edit", label: "설정", desc: "임상·회기·말투 조정" },
|
||||
{ key: "review", label: "검토", desc: "근거·위험·버전 확인" },
|
||||
{ key: "edit", label: "설정", desc: "임상, 회기, 말투 조정" },
|
||||
{ key: "review", label: "검토", desc: "근거, 위험, 버전 확인" },
|
||||
] as const;
|
||||
|
||||
const WORKSPACE_TABS: { key: WorkspaceTab; label: string; desc: string }[] = [
|
||||
{ key: "dashboard", label: "대시보드", desc: "운영·학습·검수 현황" },
|
||||
{ key: "dashboard", label: "대시보드", desc: "운영, 학습, 검수 현황" },
|
||||
{ key: "catalog", label: "카탈로그", desc: "공개 범위와 구성 정보" },
|
||||
{ key: "personas", label: "페르소나", desc: "전체 목록과 검수" },
|
||||
];
|
||||
|
|
@ -123,7 +123,7 @@ const TAB_GUIDANCE: Record<
|
|||
},
|
||||
clinical: {
|
||||
title: "임상 배경은 관찰 사실과 내부 가설을 분리해서 작성",
|
||||
desc: "가족·관계·학교/직장·촉발 사건은 SSOT 자료에서 확인되는 사실을 우선한다. CCD는 내담자가 직접 설명하지 않는 내부 모델이다.",
|
||||
desc: "가족, 관계, 학교/직장, 촉발 사건은 SSOT 자료에서 확인되는 사실을 우선한다. CCD는 내담자가 직접 설명하지 않는 내부 모델이다.",
|
||||
items: [
|
||||
"가족/관계에는 현재 기능에 영향을 주는 반복 패턴을 적는다.",
|
||||
"촉발 사건은 날짜보다 상담 장면에서 드러날 행동 변화에 초점을 둔다.",
|
||||
|
|
@ -150,7 +150,7 @@ const TAB_GUIDANCE: Record<
|
|||
},
|
||||
speech: {
|
||||
title: "말투는 캐릭터 장식이 아니라 상담 기술 평가의 입력값",
|
||||
desc: "말버릇, 군말, 비언어 단서는 상담자가 반영·명료화·공감하기 어렵거나 쉬운 지점을 만든다.",
|
||||
desc: "말버릇, 군말, 비언어 단서는 상담자가 반영, 명료화, 공감하기 어렵거나 쉬운 지점을 만든다.",
|
||||
items: [
|
||||
"문장 길이는 짧음/중간/김처럼 운영 가능한 값으로 둔다.",
|
||||
"군말과 말버릇은 과하지 않게 2~4개 정도가 적당하다.",
|
||||
|
|
@ -159,7 +159,7 @@ const TAB_GUIDANCE: Record<
|
|||
},
|
||||
safety: {
|
||||
title: "역린과 금기어는 모델이 말하지 말아야 할 말이 아니라 반응 조건",
|
||||
desc: "금기어 목록만 늘리면 시뮬레이션이 경직된다. 어떤 개입에서 어떤 방어·침묵·분노가 생기는지까지 작성해야 한다.",
|
||||
desc: "금기어 목록만 늘리면 시뮬레이션이 경직된다. 어떤 개입에서 어떤 방어, 침묵, 분노가 생기는지까지 작성해야 한다.",
|
||||
items: [
|
||||
"민감 영역은 내담자가 회피하거나 과민 반응하는 주제다.",
|
||||
"상담자 금기는 훈련자가 하면 안 되는 개입 방식이다.",
|
||||
|
|
@ -177,14 +177,12 @@ const TAB_GUIDANCE: Record<
|
|||
},
|
||||
};
|
||||
|
||||
|
||||
function completenessLabel(result: ValidationResult): string {
|
||||
if (result.errors.length === 0 && result.warnings.length === 0) return "완료";
|
||||
if (result.errors.length === 0) return "검토";
|
||||
return "미완";
|
||||
}
|
||||
|
||||
|
||||
function average(values: number[]): number | null {
|
||||
if (values.length === 0) return null;
|
||||
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
||||
|
|
@ -194,6 +192,21 @@ function percentLabel(value: number | null | undefined): string {
|
|||
return formatOptionalPercent(value, "자료 없음");
|
||||
}
|
||||
|
||||
// 표시 이름 데이터에 가운뎃점이 2개 들어 있다("서연(가명) · 고2 · 우울/자살사고").
|
||||
// 데이터는 그대로 두고 화면에서만 "서연(가명) · 고2" + "우울/자살사고" 두 줄로 나눠
|
||||
// 한 줄에 가운뎃점이 1개만 남게 한다. 가운뎃점이 1개 이하면 원문을 그대로 쓴다.
|
||||
function splitPersonaName(name: string): { head: string; tail: string } {
|
||||
const parts = name
|
||||
.split("·")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
if (parts.length < 3) return { head: name, tail: "" };
|
||||
return {
|
||||
head: parts.slice(0, 2).join(" · "),
|
||||
tail: parts.slice(2).join(", "),
|
||||
};
|
||||
}
|
||||
|
||||
function personaOrigin(code: string, source?: string): "system" | "custom" {
|
||||
if (/^P(?:[1-9]|1[01])$/i.test(code)) return "system";
|
||||
return source === "seed" ? "system" : "custom";
|
||||
|
|
@ -207,7 +220,6 @@ function reviewStatusDescription(status: PersonaReviewStatus): string {
|
|||
return "새 회기 배정에서 제외된 보관 버전입니다.";
|
||||
}
|
||||
|
||||
|
||||
function TextField({
|
||||
label,
|
||||
value,
|
||||
|
|
@ -403,12 +415,15 @@ function EmptyState({ title, desc }: { title: string; desc: string }) {
|
|||
return <UiEmptyState className="ps-empty" title={title} description={desc} />;
|
||||
}
|
||||
|
||||
// 페르소나 스튜디오는 Kicker 가 21곳이라 저작 화면 한 폭에 accent dot 이 8개까지 몰렸다.
|
||||
// 이 화면의 Kicker 는 전부 "섹션 제목 장식"일 뿐 현재 상태(정상/주의/라이브)를 전달하지 않으므로
|
||||
// dot={false} 로 통일한다. 실제 상태 신호는 그대로 <Dot tone="..."> 컴포넌트가 담당한다.
|
||||
function GuidancePanel({ tab }: { tab: StudioTab }) {
|
||||
const guidance = TAB_GUIDANCE[tab];
|
||||
return (
|
||||
<div className="ps-guidance">
|
||||
<div>
|
||||
<Kicker>작성 가이드</Kicker>
|
||||
<Kicker dot={false}>작성 가이드</Kicker>
|
||||
<b>{guidance.title}</b>
|
||||
<p>{guidance.desc}</p>
|
||||
</div>
|
||||
|
|
@ -1653,7 +1668,7 @@ export default function PersonaStudio() {
|
|||
<article className="ps-performance">
|
||||
<div className="ps-section-head">
|
||||
<div>
|
||||
<Kicker>학습 성과</Kicker>
|
||||
<Kicker dot={false}>학습 성과</Kicker>
|
||||
<h2>평가와 관계 진척도</h2>
|
||||
</div>
|
||||
<span>최근 학습 궤적 {dashboardPoints.length}건</span>
|
||||
|
|
@ -1694,7 +1709,7 @@ export default function PersonaStudio() {
|
|||
<article className="ps-review-summary">
|
||||
<div className="ps-section-head">
|
||||
<div>
|
||||
<Kicker>검수 현황</Kicker>
|
||||
<Kicker dot={false}>검수 현황</Kicker>
|
||||
<h2>결정이 필요한 버전</h2>
|
||||
</div>
|
||||
<Button
|
||||
|
|
@ -1729,7 +1744,7 @@ export default function PersonaStudio() {
|
|||
<section className="ps-active-table">
|
||||
<div className="ps-section-head">
|
||||
<div>
|
||||
<Kicker>페르소나별 현황</Kicker>
|
||||
<Kicker dot={false}>페르소나별 현황</Kicker>
|
||||
<h2>최근 학습 기록</h2>
|
||||
</div>
|
||||
<span>교수자 대시보드의 실제 기록 기준</span>
|
||||
|
|
@ -1749,6 +1764,7 @@ export default function PersonaStudio() {
|
|||
{catalog.length ? (
|
||||
catalog.slice(0, 8).map((persona) => {
|
||||
const usage = usageByPersona.get(normalizeCode(persona.code));
|
||||
const name = splitPersonaName(persona.display_name);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -1763,7 +1779,8 @@ export default function PersonaStudio() {
|
|||
>
|
||||
<span>
|
||||
<b>{persona.code}</b>
|
||||
<small>{persona.display_name}</small>
|
||||
<small>{name.head}</small>
|
||||
{name.tail ? <small>{name.tail}</small> : null}
|
||||
</span>
|
||||
<strong>{usage?.learners ?? 0}명</strong>
|
||||
<strong>{usage?.activeSessions ?? 0}회</strong>
|
||||
|
|
@ -1801,10 +1818,10 @@ export default function PersonaStudio() {
|
|||
<section className="ps-catalog-page" aria-label="페르소나 카탈로그 정보">
|
||||
<article className="ps-catalog-hero">
|
||||
<div>
|
||||
<Kicker>카탈로그란</Kicker>
|
||||
<Kicker dot={false}>카탈로그란</Kicker>
|
||||
<h2>학습자가 회기를 시작할 때 선택하는 승인본 모음입니다.</h2>
|
||||
<p>
|
||||
검수 완료된 최신 버전만 공개하고, 초안·검수 중·보관 상태는
|
||||
검수 완료된 최신 버전만 공개하고, 초안, 검수 중, 보관 상태는
|
||||
운영자에게만 보입니다.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -1821,7 +1838,7 @@ export default function PersonaStudio() {
|
|||
<section className="ps-catalog-block">
|
||||
<div className="ps-section-head">
|
||||
<div>
|
||||
<Kicker>공개 설정</Kicker>
|
||||
<Kicker dot={false}>공개 설정</Kicker>
|
||||
<h2>카탈로그 운영 규칙</h2>
|
||||
</div>
|
||||
<Badge tone="accent">승인본만 공개</Badge>
|
||||
|
|
@ -1852,7 +1869,7 @@ export default function PersonaStudio() {
|
|||
<section className="ps-catalog-block">
|
||||
<div className="ps-section-head">
|
||||
<div>
|
||||
<Kicker>구성</Kicker>
|
||||
{/* eyebrow "구성"은 바로 아래 "이론 분포"와 같은 말이라 제거 */}
|
||||
<h2>이론 분포</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1860,7 +1877,7 @@ export default function PersonaStudio() {
|
|||
{Object.entries(catalogTheoryCounts).length ? (
|
||||
Object.entries(catalogTheoryCounts).map(([theory, count]) => (
|
||||
<div key={theory}>
|
||||
<span>{personaTheoryLabel(theory)}</span>
|
||||
<span>{personaTheoryLabel(theory)}</span>
|
||||
<b>{count}</b>
|
||||
<i>
|
||||
<span
|
||||
|
|
@ -1883,7 +1900,7 @@ export default function PersonaStudio() {
|
|||
<section className="ps-catalog-block ps-catalog-block--wide">
|
||||
<div className="ps-section-head">
|
||||
<div>
|
||||
<Kicker>버전과 출처</Kicker>
|
||||
<Kicker dot={false}>버전과 출처</Kicker>
|
||||
<h2>공개본 상태</h2>
|
||||
</div>
|
||||
<Button
|
||||
|
|
@ -1895,34 +1912,38 @@ export default function PersonaStudio() {
|
|||
</Button>
|
||||
</div>
|
||||
<div className="ps-catalog-list">
|
||||
{catalog.map((persona) => (
|
||||
<button
|
||||
type="button"
|
||||
key={persona.code}
|
||||
onClick={() =>
|
||||
setSearchParams({
|
||||
view: "personas",
|
||||
persona: `approved:${persona.code}`,
|
||||
})
|
||||
}
|
||||
>
|
||||
<span className="ps-code-box">{persona.code}</span>
|
||||
<span>
|
||||
<b>{persona.display_name}</b>
|
||||
<small>
|
||||
{personaTheoryLabel(persona.theory_target[0] ?? "미지정")} ·{" "}
|
||||
{personaDifficultyLabel(persona.difficulty)}
|
||||
</small>
|
||||
</span>
|
||||
<span>
|
||||
{personaOrigin(persona.code, persona.source) === "system"
|
||||
? "시스템"
|
||||
: "추가"}
|
||||
</span>
|
||||
<strong>v{persona.version ?? 1}</strong>
|
||||
<Icon name="chevron-right" size={16} />
|
||||
</button>
|
||||
))}
|
||||
{catalog.map((persona) => {
|
||||
const name = splitPersonaName(persona.display_name);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={persona.code}
|
||||
onClick={() =>
|
||||
setSearchParams({
|
||||
view: "personas",
|
||||
persona: `approved:${persona.code}`,
|
||||
})
|
||||
}
|
||||
>
|
||||
<span className="ps-code-box">{persona.code}</span>
|
||||
<span>
|
||||
<b>{name.head}</b>
|
||||
{name.tail ? <small>{name.tail}</small> : null}
|
||||
<small>
|
||||
{personaTheoryLabel(persona.theory_target[0] ?? "미지정")} ·{" "}
|
||||
{personaDifficultyLabel(persona.difficulty)}
|
||||
</small>
|
||||
</span>
|
||||
<span>
|
||||
{personaOrigin(persona.code, persona.source) === "system"
|
||||
? "시스템"
|
||||
: "추가"}
|
||||
</span>
|
||||
<strong>v{persona.version ?? 1}</strong>
|
||||
<Icon name="chevron-right" size={16} />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
|
@ -1946,69 +1967,73 @@ export default function PersonaStudio() {
|
|||
</div>
|
||||
{reviewQueue.length ? (
|
||||
<div className="ps-review-board">
|
||||
{reviewQueue.map((persona) => (
|
||||
<article key={persona.persona_id} className="ps-review-card">
|
||||
<div className="ps-review-card__top">
|
||||
<span className="ps-code-box">{persona.code}</span>
|
||||
<Badge tone={personaReviewTone(persona.status)}>
|
||||
{personaReviewStatusLabel(persona.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
<h3>{persona.display_name}</h3>
|
||||
<p>{reviewStatusDescription(persona.status)}</p>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>버전</dt>
|
||||
<dd>v{persona.version}</dd>
|
||||
{reviewQueue.map((persona) => {
|
||||
const name = splitPersonaName(persona.display_name);
|
||||
return (
|
||||
<article key={persona.persona_id} className="ps-review-card">
|
||||
<div className="ps-review-card__top">
|
||||
<span className="ps-code-box">{persona.code}</span>
|
||||
<Badge tone={personaReviewTone(persona.status)}>
|
||||
{personaReviewStatusLabel(persona.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<dt>요청</dt>
|
||||
<dd>{formatDateTime(persona.created_at)}</dd>
|
||||
<h3>{name.head}</h3>
|
||||
{name.tail ? <p className="ps-name-tail">{name.tail}</p> : null}
|
||||
<p>{reviewStatusDescription(persona.status)}</p>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>버전</dt>
|
||||
<dd>v{persona.version}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>요청</dt>
|
||||
<dd>{formatDateTime(persona.created_at)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>출처</dt>
|
||||
<dd>{persona.is_synthetic ? "작성·합성" : "근거 기반"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="ps-review-card__actions">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
setQuery({ persona: `draft:${persona.persona_id}` })
|
||||
}
|
||||
>
|
||||
상세
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => openDraftEditor(persona.persona_id)}
|
||||
>
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
void decideReview(persona.persona_id, "reject")
|
||||
}
|
||||
disabled={busy !== null || persona.status !== "review"}
|
||||
>
|
||||
반려
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
void decideReview(persona.persona_id, "approve")
|
||||
}
|
||||
disabled={busy !== null || persona.status !== "review"}
|
||||
>
|
||||
승인
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<dt>출처</dt>
|
||||
<dd>{persona.is_synthetic ? "작성·합성" : "근거 기반"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="ps-review-card__actions">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
setQuery({ persona: `draft:${persona.persona_id}` })
|
||||
}
|
||||
>
|
||||
상세
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => openDraftEditor(persona.persona_id)}
|
||||
>
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
void decideReview(persona.persona_id, "reject")
|
||||
}
|
||||
disabled={busy !== null || persona.status !== "review"}
|
||||
>
|
||||
반려
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
void decideReview(persona.persona_id, "approve")
|
||||
}
|
||||
disabled={busy !== null || persona.status !== "review"}
|
||||
>
|
||||
승인
|
||||
</Button>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
|
|
@ -2088,6 +2113,7 @@ export default function PersonaStudio() {
|
|||
{personaRows.length ? (
|
||||
personaRows.map((row) => {
|
||||
const usage = usageByPersona.get(normalizeCode(row.code));
|
||||
const name = splitPersonaName(row.name);
|
||||
return (
|
||||
<div
|
||||
className="ps-persona-row"
|
||||
|
|
@ -2104,7 +2130,8 @@ export default function PersonaStudio() {
|
|||
>
|
||||
<span className="ps-code-box">{row.code}</span>
|
||||
<span>
|
||||
<b>{row.name}</b>
|
||||
<b>{name.head}</b>
|
||||
{name.tail ? <small>{name.tail}</small> : null}
|
||||
<small>
|
||||
{personaDifficultyLabel(row.difficulty)} ·{" "}
|
||||
{row.theory.map(personaTheoryLabel).join(", ") ||
|
||||
|
|
@ -2189,6 +2216,7 @@ export default function PersonaStudio() {
|
|||
);
|
||||
|
||||
const detailData = selectedReview ? selectedDraftDetail : null;
|
||||
const detailName = splitPersonaName(selectedRow?.name ?? "");
|
||||
const personaDetailView = selectedRow ? (
|
||||
<section className="ps-detail-page" aria-label={`${selectedRow.name} 상세`}>
|
||||
<div className="ps-drill-head">
|
||||
|
|
@ -2201,10 +2229,13 @@ export default function PersonaStudio() {
|
|||
목록으로
|
||||
</Button>
|
||||
<div>
|
||||
<Kicker>
|
||||
<Kicker dot={false}>
|
||||
{selectedRow.code} · v{selectedRow.version}
|
||||
</Kicker>
|
||||
<h2>{selectedRow.name}</h2>
|
||||
<h2>{detailName.head}</h2>
|
||||
{detailName.tail ? (
|
||||
<p className="ps-name-tail">{detailName.tail}</p>
|
||||
) : null}
|
||||
<p>
|
||||
{selectedRow.kind === "approved"
|
||||
? "학습자에게 공개된 현재 버전"
|
||||
|
|
@ -2258,8 +2289,11 @@ export default function PersonaStudio() {
|
|||
{detailTab === "overview" ? (
|
||||
<div className="ps-detail-grid">
|
||||
<article className="ps-detail-primary">
|
||||
<Kicker>페르소나 소개</Kicker>
|
||||
<h3>{selectedRow.name}</h3>
|
||||
<Kicker dot={false}>페르소나 소개</Kicker>
|
||||
<h3>{detailName.head}</h3>
|
||||
{detailName.tail ? (
|
||||
<p className="ps-name-tail">{detailName.tail}</p>
|
||||
) : null}
|
||||
<p>
|
||||
{selectedApproved?.presenting_summary ||
|
||||
stringValue(objectValue(detailData?.presenting), "complaint") ||
|
||||
|
|
@ -2301,7 +2335,7 @@ export default function PersonaStudio() {
|
|||
</article>
|
||||
{detailData ? (
|
||||
<article className="ps-detail-wide">
|
||||
<Kicker>작성 정보</Kicker>
|
||||
<Kicker dot={false}>작성 정보</Kicker>
|
||||
<dl className="ps-settings-list">
|
||||
<div>
|
||||
<dt>출처</dt>
|
||||
|
|
@ -2368,7 +2402,7 @@ export default function PersonaStudio() {
|
|||
<section className="ps-detail-wide">
|
||||
<div className="ps-section-head">
|
||||
<div>
|
||||
<Kicker>학습 궤적</Kicker>
|
||||
<Kicker dot={false}>학습 궤적</Kicker>
|
||||
<h2>최근 평가 기록</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2400,7 +2434,7 @@ export default function PersonaStudio() {
|
|||
) : (
|
||||
<div className="ps-detail-grid">
|
||||
<article className="ps-detail-wide">
|
||||
<Kicker>버전·공개 설정</Kicker>
|
||||
<Kicker dot={false}>버전·공개 설정</Kicker>
|
||||
<dl className="ps-settings-list">
|
||||
<div>
|
||||
<dt>현재 상태</dt>
|
||||
|
|
@ -2447,7 +2481,7 @@ export default function PersonaStudio() {
|
|||
<main className="ps-root">
|
||||
<header className="ps-head">
|
||||
<div>
|
||||
<Kicker>페르소나</Kicker>
|
||||
{/* eyebrow "페르소나"는 아래 h1(페르소나 운영/카탈로그/관리/상세)과 같은 말이라 제거 */}
|
||||
<h1>
|
||||
{detailKey
|
||||
? "페르소나 상세"
|
||||
|
|
@ -2519,15 +2553,19 @@ export default function PersonaStudio() {
|
|||
</AppShell>
|
||||
);
|
||||
|
||||
const draftName = splitPersonaName(draft.display_name.trim());
|
||||
const legacyAuthoring = (
|
||||
<AppShell contextLabel="페르소나" navRole="teacher" wide>
|
||||
<main className="ps-root">
|
||||
<header className="ps-head">
|
||||
<div>
|
||||
<Kicker>
|
||||
<Kicker dot={false}>
|
||||
{editingId ? "페르소나 수정" : "새로운 페르소나 만들기"}
|
||||
</Kicker>
|
||||
<h1>{draft.display_name.trim() || "새 페르소나"}</h1>
|
||||
<h1>{draftName.head || "새 페르소나"}</h1>
|
||||
{draftName.tail ? (
|
||||
<p className="ps-name-tail">{draftName.tail}</p>
|
||||
) : null}
|
||||
<p>
|
||||
단계별로 작성하고 언제든 이전 단계로 돌아가 수정할 수 있습니다.
|
||||
</p>
|
||||
|
|
@ -2598,8 +2636,13 @@ export default function PersonaStudio() {
|
|||
<section className="ps-editor">
|
||||
<div className={surfaceClassName("ps-editor__top")}>
|
||||
<div>
|
||||
<Kicker>{editingId ? "초안 편집" : "새 초안"}</Kicker>
|
||||
<h2>{draft.display_name.trim() || "이름 미정"}</h2>
|
||||
<Kicker dot={false}>
|
||||
{editingId ? "초안 편집" : "새 초안"}
|
||||
</Kicker>
|
||||
<h2>{draftName.head || "이름 미정"}</h2>
|
||||
{draftName.tail ? (
|
||||
<p className="ps-name-tail">{draftName.tail}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<Badge tone={validation.errors.length === 0 ? "accent" : "warn"}>
|
||||
{completenessLabel(validation)}
|
||||
|
|
@ -2609,8 +2652,9 @@ export default function PersonaStudio() {
|
|||
<Card className="ps-source-panel">
|
||||
<div className="ps-source-panel__head">
|
||||
<div>
|
||||
<Kicker>RAG 근거로 초안 만들기</Kicker>
|
||||
<b>첨부 파일·실제 기록·교재 가이드 기반 생성</b>
|
||||
{/* eyebrow "RAG 근거로 초안 만들기"는 아래 문장과 같은 말이라 제거하고,
|
||||
가운뎃점 2개(첨부 파일·실제 기록·교재 가이드)는 쉼표로 풀었다. */}
|
||||
<b>첨부 파일, 실제 기록, 교재 가이드로 초안 만들기</b>
|
||||
</div>
|
||||
<Badge tone={maskedEntities.length > 0 ? "warn" : "neutral"}>
|
||||
{sourceReferences.length > 0
|
||||
|
|
@ -2644,7 +2688,7 @@ export default function PersonaStudio() {
|
|||
}}
|
||||
/>
|
||||
<div>
|
||||
<Kicker>SSOT 첨부</Kicker>
|
||||
<Kicker dot={false}>SSOT 첨부</Kicker>
|
||||
<b>{sourceFilename || "파일을 드래그하거나 선택"}</b>
|
||||
<span>
|
||||
txt, md, csv, json, html, xml에 더해 자유 양식
|
||||
|
|
@ -2692,7 +2736,7 @@ export default function PersonaStudio() {
|
|||
value={sourceText}
|
||||
onChange={setSourceText}
|
||||
rows={8}
|
||||
placeholder="첨부 파일을 읽거나, 내담자 기록·사례 요약·교재 기준·상담 장면 메모를 붙여넣으세요."
|
||||
placeholder="첨부 파일을 읽거나, 내담자 기록, 사례 요약, 교재 기준, 상담 장면 메모를 붙여넣으세요."
|
||||
/>
|
||||
</div>
|
||||
<div className="ps-source-panel__actions">
|
||||
|
|
@ -2787,7 +2831,7 @@ export default function PersonaStudio() {
|
|||
|
||||
<aside className="ps-inspector" aria-label="검증 상태">
|
||||
<div className={surfaceClassName("ps-inspector__block")}>
|
||||
<Kicker>검증</Kicker>
|
||||
<Kicker dot={false}>검증</Kicker>
|
||||
<div className="ps-check">
|
||||
{validation.errors.length === 0 &&
|
||||
validation.warnings.length === 0 ? (
|
||||
|
|
@ -2812,7 +2856,7 @@ export default function PersonaStudio() {
|
|||
</div>
|
||||
|
||||
<div className={surfaceClassName("ps-inspector__block")}>
|
||||
<Kicker>생성 근거</Kicker>
|
||||
<Kicker dot={false}>생성 근거</Kicker>
|
||||
<div className="ps-check">
|
||||
{generationSummary ? (
|
||||
<div className="ps-check-row">
|
||||
|
|
@ -2849,8 +2893,11 @@ export default function PersonaStudio() {
|
|||
/>
|
||||
<span>
|
||||
<b>{item.title}</b>
|
||||
{/* 한 줄에 가운뎃점 3개가 몰려 읽기 어려워 두 줄로 나눴다(줄당 1개). */}
|
||||
<small>
|
||||
{item.source_id} · doc {item.doc_id ?? "-"}
|
||||
</small>
|
||||
<small>
|
||||
{item.source_id} · doc {item.doc_id ?? "-"} ·{" "}
|
||||
{item.chunk_count} chunks ·{" "}
|
||||
{item.content_hash.slice(0, 10)}
|
||||
</small>
|
||||
|
|
@ -2878,7 +2925,7 @@ export default function PersonaStudio() {
|
|||
</div>
|
||||
|
||||
<div className={surfaceClassName("ps-inspector__block")}>
|
||||
<Kicker>검수 큐</Kicker>
|
||||
<Kicker dot={false}>검수 큐</Kicker>
|
||||
{reviews.length > 0 ? (
|
||||
<div className="ps-review-cards">
|
||||
{reviews.map((persona) => (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue