feat: streamline conversational birth-time rectification
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
export type BirthTimeAssessmentPhase = "saving_profile" | "assessing";
|
||||
export type BirthTimeAssessmentPhase = "saving_profile" | "assessing" | "entering_home";
|
||||
|
||||
const progressCopy = {
|
||||
saving_profile: {
|
||||
@@ -9,6 +9,10 @@ const progressCopy = {
|
||||
title: "正在生成生时评估",
|
||||
detail: "正在读取已有资料并准备下一步,不需要重复操作。",
|
||||
},
|
||||
entering_home: {
|
||||
title: "正在进入首页",
|
||||
detail: "出生资料已保存,正在为你准备首页。",
|
||||
},
|
||||
} as const satisfies Record<BirthTimeAssessmentPhase, { readonly title: string; readonly detail: string }>;
|
||||
|
||||
export function BirthTimeAssessmentOverlay({ phase }: { readonly phase: BirthTimeAssessmentPhase | null }) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { ArrowUp } from "lucide-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { ChatMessageContent } from "./chat-message-content.tsx";
|
||||
import { AgentAvatar, ChatMessageRow } from "./chat-message-row.tsx";
|
||||
import { Button } from "./ui/button.tsx";
|
||||
import { Textarea } from "./ui/textarea.tsx";
|
||||
import {
|
||||
useConversationalRectification,
|
||||
type ConversationalRectificationController,
|
||||
@@ -20,46 +20,52 @@ const domainLabels = {
|
||||
career: "事业与身份",
|
||||
education: "学业与学习",
|
||||
finance: "收入与资产",
|
||||
health_pressure: "健康与重大压力",
|
||||
relocation: "搬迁与居住地",
|
||||
relationship: "重要关系",
|
||||
family: "家庭变化",
|
||||
other: "其他关键经历",
|
||||
} as const satisfies Readonly<Record<EvidenceDomain, string>>;
|
||||
|
||||
function orderedEvidenceDomains(turn: ConversationalRectificationTurn): EvidenceDomain[] {
|
||||
function nextEvidenceDomains(turn: ConversationalRectificationTurn): EvidenceDomain[] {
|
||||
const provided = new Set(turn.evidenceRecap.flatMap((item) => item.domain ? [item.domain] : []));
|
||||
return [...(turn.evidenceRequest?.domains ?? [])].sort((left, right) =>
|
||||
Number(provided.has(left)) - Number(provided.has(right)));
|
||||
return [...(turn.evidenceRequest?.domains ?? [])]
|
||||
.sort((left, right) => Number(provided.has(left)) - Number(provided.has(right)))
|
||||
.slice(0, 2);
|
||||
}
|
||||
|
||||
function visibleTurnNarrative(turn: ConversationalRectificationTurn): string {
|
||||
if (
|
||||
turn.status !== "active"
|
||||
|| turn.evidenceRecap.length === 0
|
||||
|| /(?:已记录|已修订):/.test(turn.narrative)
|
||||
) {
|
||||
if (["paused", "abandoned", "completed"].includes(turn.status)) {
|
||||
return turn.narrative;
|
||||
}
|
||||
|
||||
const latestEvidence = turn.evidenceRecap.at(-1)!;
|
||||
const candidate = turn.candidate;
|
||||
const candidateRange = candidate.rangeStart && candidate.rangeEnd
|
||||
? `${candidate.rangeStart}–${candidate.rangeEnd}`
|
||||
: candidate.representativeTime ?? "当前候选范围";
|
||||
const nextDomains = orderedEvidenceDomains(turn)
|
||||
.slice(0, 2)
|
||||
.map((domain) => domainLabels[domain]);
|
||||
const nextStep = candidate.status === "ready_for_confirmation"
|
||||
? "当前证据已形成候选总结;请先核对上方候选时间,再决定是否确认或继续补充经历。"
|
||||
: nextDomains.length > 0
|
||||
? `下一步:请优先补充一件${nextDomains.join("或")}领域已经发生的事件,并选择发生年月。`
|
||||
: "下一步:请继续补充一件已经发生并带有年月的真实经历。";
|
||||
const suggestedDomains = nextEvidenceDomains(turn).map((domain) => domainLabels[domain]);
|
||||
if (turn.evidenceRecap.length === 0) {
|
||||
const examples = suggestedDomains.length > 0
|
||||
? suggestedDomains.join("或")
|
||||
: "工作、搬迁、关系或学业";
|
||||
return `为了帮助校正出生时间,请先说一件${examples}方面已经发生的重要经历。最好带上年月,直接像聊天一样描述即可。`;
|
||||
}
|
||||
|
||||
const latestEvidence = turn.evidenceRecap.at(-1)!;
|
||||
if (turn.candidate.status === "ready_for_confirmation") {
|
||||
return [
|
||||
`${latestEvidence.isCorrection ? "已修订" : "已记录"}:${latestEvidence.dateLabel} · ${latestEvidence.summary}。`,
|
||||
"目前已经形成一个待确认候选。你可以展开下方详情核对,也可以继续补充一件带年月的真实经历。",
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
const candidateRange = turn.candidate.rangeStart && turn.candidate.rangeEnd
|
||||
? `${turn.candidate.rangeStart}–${turn.candidate.rangeEnd}`
|
||||
: turn.candidate.representativeTime ?? "当前候选范围";
|
||||
const nextQuestion = suggestedDomains.length > 0
|
||||
? `接下来请说一件${suggestedDomains.join("或")}方面已经发生的事,尽量带上年月。`
|
||||
: "接下来请再说一件已经发生的真实经历,尽量带上年月。";
|
||||
return [
|
||||
`${latestEvidence.isCorrection ? "已修订" : "已记录"}:${latestEvidence.dateLabel} · ${latestEvidence.summary}。`,
|
||||
`候选范围当前为 ${candidateRange};范围暂未变化不代表提交失败,系统会结合后续经历继续比较相邻分钟。`,
|
||||
nextStep,
|
||||
].join("\n");
|
||||
`候选范围现在是 ${candidateRange}。范围暂未变化不代表提交失败,我会结合后续经历继续比较相邻分钟。`,
|
||||
nextQuestion,
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
type SurfaceProps = Readonly<{
|
||||
@@ -73,53 +79,11 @@ function safely(request: Promise<unknown>) {
|
||||
void request.catch(() => undefined);
|
||||
}
|
||||
|
||||
function CandidateSummary({ turn }: { readonly turn: ConversationalRectificationTurn }) {
|
||||
const candidate = turn.candidate;
|
||||
const confirmed = candidate.status === "confirmed" && turn.status === "completed";
|
||||
const status = confirmed
|
||||
? "已明确确认"
|
||||
: turn.status === "completed"
|
||||
? "范围已保存 · 分钟未确认"
|
||||
: candidate.status === "ready_for_confirmation"
|
||||
? "待确认 · 未验证"
|
||||
: "待验证 · 未确认";
|
||||
const recordedCount = turn.evidenceRecap.length;
|
||||
const nextDomains = orderedEvidenceDomains(turn)
|
||||
.slice(0, 2)
|
||||
.map((domain) => domainLabels[domain]);
|
||||
|
||||
return (
|
||||
<section className="conversational-candidate" aria-labelledby="conversational-candidate-title">
|
||||
<header>
|
||||
<h3 id="conversational-candidate-title">候选时间</h3>
|
||||
<span className={confirmed ? "is-confirmed" : "is-unverified"}>{status}</span>
|
||||
</header>
|
||||
{candidate.representativeTime ? (
|
||||
<dl>
|
||||
<div>
|
||||
<dt>代表时间</dt>
|
||||
<dd><time dateTime={candidate.representativeTime}>{candidate.representativeTime}</time></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>候选范围</dt>
|
||||
<dd>{candidate.rangeStart && candidate.rangeEnd
|
||||
? `${candidate.rangeStart}—${candidate.rangeEnd}`
|
||||
: "尚未缩小"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
) : <p>尚未形成可供确认的具体时间。</p>}
|
||||
{!confirmed && <p>候选仍待真实经历验证,未经你的明确确认不会成为当前排盘时间。</p>}
|
||||
{!confirmed && recordedCount > 0 && turn.status !== "completed" && (
|
||||
<div className="conversational-candidate-progress" role="status">
|
||||
<strong>已记录 {recordedCount} 条经历</strong>
|
||||
<span>系统至少需要 3 条时间明确、可评分的经历后才会可靠缩小范围;范围不变不代表提交失败。</span>
|
||||
{nextDomains.length > 0 && (
|
||||
<span>下一步优先补充:{nextDomains.join("或")}领域已经发生的事件。</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
function candidateStatus(turn: ConversationalRectificationTurn): string {
|
||||
if (turn.status === "completed" && turn.candidate.status === "confirmed") return "已确认";
|
||||
if (turn.status === "completed") return "范围已保存,分钟未确认";
|
||||
if (turn.candidate.status === "ready_for_confirmation") return "待确认,尚未验证";
|
||||
return "待验证";
|
||||
}
|
||||
|
||||
export function ConversationalRectificationSurface({
|
||||
@@ -129,14 +93,11 @@ export function ConversationalRectificationSurface({
|
||||
onContinueOriginalQuestion,
|
||||
}: SurfaceProps) {
|
||||
const composer = useRef<HTMLTextAreaElement>(null);
|
||||
const [eventYear, setEventYear] = useState("");
|
||||
const [eventMonth, setEventMonth] = useState("");
|
||||
const currentYear = new Date().getFullYear();
|
||||
const eventYears = Array.from({ length: 101 }, (_, index) => currentYear - index);
|
||||
const turn = controller.turn;
|
||||
const pendingQuestion = turn?.status === "completed"
|
||||
? turn.pendingConsultationQuestion
|
||||
: turn?.pendingConsultationQuestion ?? pendingConsultationQuestion ?? null;
|
||||
|
||||
if (!turn) {
|
||||
return (
|
||||
<section className="conversational-rectification" aria-busy={controller.pending} aria-label="生时校正对话">
|
||||
@@ -147,262 +108,166 @@ export function ConversationalRectificationSurface({
|
||||
}
|
||||
|
||||
const canAnswer = turn.actions.includes("answer") && turn.status !== "abandoned" && turn.status !== "completed";
|
||||
const requestedDomains = orderedEvidenceDomains(turn);
|
||||
const providedDomains = new Set(
|
||||
turn.evidenceRecap.flatMap((item) => item.domain ? [item.domain] : []),
|
||||
);
|
||||
const priorityDomain = requestedDomains.find((domain) => !providedDomains.has(domain))
|
||||
?? requestedDomains[0]
|
||||
?? null;
|
||||
const canConfirm = turn.actions.includes("confirm")
|
||||
&& turn.candidate.status === "ready_for_confirmation"
|
||||
&& Boolean(turn.candidate.representativeTime);
|
||||
const canContinue = turn.actions.includes("continue_original_question")
|
||||
&& Boolean(pendingQuestion)
|
||||
&& Boolean(onContinueOriginalQuestion);
|
||||
const submit = async () => {
|
||||
if (!canAnswer || !controller.draft.trim() || controller.pending) return;
|
||||
const dateLabel = eventYear
|
||||
? `${eventYear} 年${eventMonth ? ` ${Number(eventMonth)} 月` : ""}`
|
||||
: "时间不确定";
|
||||
const answer = `发生时间:${dateLabel}\n事件详情:${controller.draft.trim()}`;
|
||||
try {
|
||||
await controller.answer(undefined, answer);
|
||||
setEventYear("");
|
||||
setEventMonth("");
|
||||
await controller.answer(undefined, controller.draft.trim());
|
||||
composer.current?.focus();
|
||||
} catch {
|
||||
// The controller owns the visible request error and keeps the draft available for retry.
|
||||
// The controller keeps the draft and owns the visible error.
|
||||
}
|
||||
};
|
||||
const focusComposer = () => composer.current?.focus();
|
||||
|
||||
return (
|
||||
<section
|
||||
className="conversational-rectification"
|
||||
aria-busy={controller.pending}
|
||||
aria-label="生时校正对话"
|
||||
>
|
||||
<CandidateSummary turn={turn} />
|
||||
|
||||
<article className="conversational-narrative" aria-label="校正分析">
|
||||
<div className="conversational-narrative-body">
|
||||
<ChatMessageContent text={visibleTurnNarrative(turn)} />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{requestedDomains.length > 0 && (
|
||||
<fieldset className="conversational-domain-picker">
|
||||
<legend className="conversational-domain-question">这轮想先补充哪个领域?</legend>
|
||||
<div>
|
||||
{requestedDomains.map((domain) => (
|
||||
<button
|
||||
aria-label={`${domainLabels[domain]},${domain === priorityDomain
|
||||
? "下一步建议"
|
||||
: providedDomains.has(domain) ? "已提供,可继续补充" : "可补充"}`}
|
||||
aria-pressed={controller.selectedDomain === domain}
|
||||
data-evidence-domain={domain}
|
||||
disabled={controller.pending || !canAnswer}
|
||||
key={domain}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
controller.selectDomain(domain);
|
||||
focusComposer();
|
||||
}}
|
||||
>
|
||||
<span>{domainLabels[domain]}</span>
|
||||
<small>{domain === priorityDomain
|
||||
? "下一步建议"
|
||||
: providedDomains.has(domain) ? "已提供,可继续补充" : "可补充"}</small>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
)}
|
||||
|
||||
{controller.pending && canAnswer && (
|
||||
<div
|
||||
aria-atomic="true"
|
||||
aria-label="正在核对经历"
|
||||
aria-live="polite"
|
||||
className="conversational-answer-pending"
|
||||
role="status"
|
||||
>
|
||||
<div className="app-loading-symbol" aria-hidden="true">
|
||||
<span className="app-loading-orbit" />
|
||||
<span className="app-loading-mark" />
|
||||
</div>
|
||||
<div>
|
||||
<strong>正在核对这段经历</strong>
|
||||
<span>正在比较相邻候选分钟;完成后会继续提问或更新候选范围。</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canAnswer && <form
|
||||
className="conversational-composer"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void submit();
|
||||
}}
|
||||
>
|
||||
{controller.correctionTarget && (
|
||||
<div className="conversational-correction-target" role="status">
|
||||
<p>
|
||||
正在更正:<strong>{controller.correctionTarget.dateLabel} · {controller.correctionTarget.summary}</strong>
|
||||
</p>
|
||||
<span>一次只更正一条事件。提交后会保留原记录用于审计,但候选评分只使用更正后的有效证据。</span>
|
||||
<button
|
||||
className="button-secondary"
|
||||
disabled={controller.pending}
|
||||
type="button"
|
||||
onClick={() => controller.cancelEvidenceCorrection()}
|
||||
>
|
||||
取消更正
|
||||
</button>
|
||||
</div>
|
||||
<section className="rectification-chat" aria-busy={controller.pending} aria-label="生时校正对话">
|
||||
<div className="message-list rectification-message-list">
|
||||
<span className="sr-only" aria-live="polite">{controller.pending ? "Jyotisha 正在核对经历" : ""}</span>
|
||||
{turn.evidenceRecap.map((entry) => (
|
||||
<ChatMessageRow
|
||||
key={entry.id}
|
||||
message={{
|
||||
role: "user",
|
||||
text: `${entry.dateLabel} · ${entry.summary}${entry.isCorrection ? "(已修订)" : ""}`,
|
||||
renderKey: entry.id,
|
||||
state: "settled",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{controller.pending && controller.draft.trim() && (
|
||||
<ChatMessageRow
|
||||
message={{ role: "user", text: controller.draft.trim(), renderKey: "pending-evidence", state: "settled" }}
|
||||
/>
|
||||
)}
|
||||
<label htmlFor="conversational-rectification-answer">
|
||||
{controller.correctionTarget ? "填写更正后的真实经历" : "补充真实经历"}
|
||||
<span>大概年份也可以;不确定的部分请直接说不确定。</span>
|
||||
</label>
|
||||
<div className="conversational-event-date" role="group" aria-label="经历发生时间">
|
||||
<label>
|
||||
年份
|
||||
<select
|
||||
aria-label="经历发生年份"
|
||||
disabled={controller.pending || !canAnswer}
|
||||
value={eventYear}
|
||||
onChange={(event) => {
|
||||
setEventYear(event.target.value);
|
||||
if (!event.target.value) setEventMonth("");
|
||||
}}
|
||||
>
|
||||
<option value="">不确定</option>
|
||||
{eventYears.map((year) => <option key={year} value={year}>{year} 年</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
月份(可选)
|
||||
<select
|
||||
aria-label="经历发生月份"
|
||||
disabled={controller.pending || !canAnswer || !eventYear}
|
||||
value={eventMonth}
|
||||
onChange={(event) => setEventMonth(event.target.value)}
|
||||
>
|
||||
<option value="">不确定</option>
|
||||
{Array.from({ length: 12 }, (_, index) => index + 1).map((month) => (
|
||||
<option key={month} value={month}>{month} 月</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<textarea
|
||||
id="conversational-rectification-answer"
|
||||
disabled={controller.pending || !canAnswer}
|
||||
maxLength={4_000}
|
||||
placeholder={controller.correctionTarget
|
||||
? "例如:其实是 2020 年 11 月离职"
|
||||
: "请描述一件已经发生的具体事件"}
|
||||
ref={composer}
|
||||
rows={4}
|
||||
value={controller.draft}
|
||||
onChange={(event) => controller.setDraft(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
void submit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="conversational-composer-footer">
|
||||
<small>Ctrl/⌘ + Enter 发送</small>
|
||||
<button
|
||||
className="button-primary"
|
||||
disabled={controller.pending || !canAnswer || !controller.draft.trim()}
|
||||
type="submit"
|
||||
>
|
||||
{controller.pending ? "正在核对…" : "发送这段经历"}
|
||||
</button>
|
||||
</div>
|
||||
</form>}
|
||||
|
||||
{turn.evidenceRecap.length > 0 && (
|
||||
<section className="conversational-evidence-recap" aria-labelledby="conversational-evidence-title">
|
||||
<header>
|
||||
<h3 id="conversational-evidence-title">已记录的真实经历</h3>
|
||||
<span>{turn.evidenceRecap.length} 条</span>
|
||||
</header>
|
||||
<ul>
|
||||
{turn.evidenceRecap.map((entry) => (
|
||||
<li key={entry.id}>
|
||||
<div>
|
||||
<time>{entry.dateLabel}</time>
|
||||
<p>{entry.summary}</p>
|
||||
{entry.isCorrection && <span className="conversational-correction-badge">已修订</span>}
|
||||
</div>
|
||||
<button
|
||||
aria-label={`更正这条经历:${entry.summary}`}
|
||||
disabled={controller.pending || !canAnswer}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
controller.beginEvidenceCorrection(entry.id);
|
||||
focusComposer();
|
||||
}}
|
||||
>
|
||||
更正
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{turn.actions.includes("confirm")
|
||||
&& turn.candidate.status === "ready_for_confirmation"
|
||||
&& turn.candidate.representativeTime && (
|
||||
<section className="conversational-confirmation" aria-labelledby="conversational-confirmation-title">
|
||||
<h3 id="conversational-confirmation-title">采用候选前请明确确认</h3>
|
||||
<p>这一步不会自动采用候选。确认后,原始填报时间仍会保留。</p>
|
||||
<button
|
||||
className="button-primary"
|
||||
disabled={controller.pending}
|
||||
type="button"
|
||||
onClick={() => safely(controller.confirm(turn.candidate.representativeTime ?? undefined))}
|
||||
>
|
||||
{controller.pending
|
||||
? "确认中…"
|
||||
: `确认将 ${turn.candidate.representativeTime} 设为当前排盘时间`}
|
||||
</button>
|
||||
</section>
|
||||
<article className="message message-assistant" aria-label="Jyotisha">
|
||||
<AgentAvatar />
|
||||
<div className="message-content">
|
||||
<div className="message-bubble">
|
||||
<ChatMessageContent text={visibleTurnNarrative(turn)} />
|
||||
<details className="rectification-message-details">
|
||||
<summary>
|
||||
{turn.candidate.representativeTime
|
||||
? `当前候选 ${turn.candidate.representativeTime} · ${candidateStatus(turn)}`
|
||||
: `校正进度 · ${candidateStatus(turn)}`}
|
||||
</summary>
|
||||
<p>
|
||||
{turn.candidate.rangeStart && turn.candidate.rangeEnd
|
||||
? `候选范围 ${turn.candidate.rangeStart}—${turn.candidate.rangeEnd};已记录 ${turn.evidenceRecap.length} 条经历。`
|
||||
: `已记录 ${turn.evidenceRecap.length} 条经历,尚未缩小候选范围。`}
|
||||
</p>
|
||||
<p>候选只用于继续验证;这一步不会自动采用候选,未经发布门禁与明确确认不会成为当前排盘时间。</p>
|
||||
{turn.evidenceRecap.length > 0 && (
|
||||
<ul aria-label="已记录的真实经历">
|
||||
{turn.evidenceRecap.map((entry) => (
|
||||
<li key={entry.id}>
|
||||
<span>{entry.dateLabel} · {entry.summary}{entry.isCorrection ? "(已修订)" : ""}</span>
|
||||
{canAnswer && (
|
||||
<button
|
||||
aria-label={`更正这条经历:${entry.summary}`}
|
||||
disabled={controller.pending}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
controller.beginEvidenceCorrection(entry.id);
|
||||
composer.current?.focus();
|
||||
}}
|
||||
>
|
||||
更正
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{controller.pending && canAnswer && (
|
||||
<ChatMessageRow message={{ role: "assistant", text: "", renderKey: "rectification-thinking", state: "thinking" }} />
|
||||
)}
|
||||
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="conversational-status"
|
||||
>
|
||||
{turn.status === "paused" && <p>校正已暂停,输入与现有证据都已保留。</p>}
|
||||
{turn.status === "abandoned" && <p>本次校正已放弃,候选时间没有应用。</p>}
|
||||
{turn.status === "completed" && turn.candidate.status === "confirmed"
|
||||
&& <p>候选时间已经过你的明确确认。</p>}
|
||||
{turn.status === "completed" && turn.candidate.status !== "confirmed"
|
||||
&& <p>本次校正已结束并保存候选范围;没有把候选代表时间设为当前排盘时间。</p>}
|
||||
{controller.error && <p className="form-error" role="alert">{controller.error}</p>}
|
||||
{turn.status === "paused" && <ChatMessageRow message={{ role: "assistant", text: "校正已暂停,输入与现有证据都已保留。", renderKey: "rectification-paused", state: "settled" }} />}
|
||||
{turn.status === "abandoned" && <ChatMessageRow message={{ role: "assistant", text: "本次校正已放弃,候选时间没有应用。", renderKey: "rectification-abandoned", state: "settled" }} />}
|
||||
{turn.status === "completed" && <ChatMessageRow message={{
|
||||
role: "assistant",
|
||||
text: turn.candidate.status === "confirmed"
|
||||
? "候选时间已经过你的明确确认。"
|
||||
: "候选范围已保存,代表时间没有自动设为当前排盘时间。",
|
||||
renderKey: "rectification-completed",
|
||||
state: "settled",
|
||||
}} />}
|
||||
{controller.error && <p className="error-message" role="alert">{controller.error}</p>}
|
||||
</div>
|
||||
|
||||
{turn.actions.includes("continue_original_question")
|
||||
&& pendingQuestion
|
||||
&& onContinueOriginalQuestion && (
|
||||
<section className="conversational-original-question">
|
||||
<p>原问题:{pendingQuestion}</p>
|
||||
<button
|
||||
className="button-primary"
|
||||
disabled={controller.pending || continuationPending}
|
||||
type="button"
|
||||
onClick={() => onContinueOriginalQuestion?.(pendingQuestion)}
|
||||
>
|
||||
{continuationPending
|
||||
? "正在继续回答原问题…"
|
||||
: turn.candidate.status === "confirmed"
|
||||
? "使用新确认时间继续回答原问题"
|
||||
: "返回原对话并继续回答"}
|
||||
</button>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{(canAnswer || canConfirm || canContinue) && <div className="composer-wrap rectification-composer-wrap">
|
||||
{(canConfirm || canContinue) && (
|
||||
<div className="composer-suggestions" aria-label="生时校正操作">
|
||||
{canConfirm && (
|
||||
<button
|
||||
aria-label={`确认将 ${turn.candidate.representativeTime} 设为当前排盘时间;当前分钟尚未验证`}
|
||||
disabled={controller.pending}
|
||||
type="button"
|
||||
onClick={() => safely(controller.confirm(turn.candidate.representativeTime ?? undefined))}
|
||||
>
|
||||
确认采用 {turn.candidate.representativeTime}(尚未验证)
|
||||
</button>
|
||||
)}
|
||||
{canContinue && (
|
||||
<button
|
||||
disabled={controller.pending || continuationPending}
|
||||
type="button"
|
||||
onClick={() => onContinueOriginalQuestion?.(pendingQuestion!)}
|
||||
>
|
||||
{continuationPending ? "正在继续回答…" : "返回原问题"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{canAnswer && <>
|
||||
{controller.correctionTarget && (
|
||||
<div className="rectification-correction-target" role="status">
|
||||
<p>正在更正:{controller.correctionTarget.dateLabel} · {controller.correctionTarget.summary}</p>
|
||||
<button disabled={controller.pending} type="button" onClick={() => controller.cancelEvidenceCorrection()}>
|
||||
取消更正
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<form className="composer" onSubmit={(event) => { event.preventDefault(); void submit(); }}>
|
||||
<label className="sr-only" htmlFor="conversational-rectification-answer">
|
||||
{controller.correctionTarget ? "输入更正后的经历" : "回答生时校正问题"}
|
||||
</label>
|
||||
<Textarea
|
||||
id="conversational-rectification-answer"
|
||||
ref={composer}
|
||||
autoFocus
|
||||
disabled={controller.pending}
|
||||
maxLength={4_000}
|
||||
placeholder={controller.correctionTarget
|
||||
? "例如:更正为 2020 年 11 月离职"
|
||||
: "像聊天一样回答即可,例如:2018 年 6 月去了上海工作"}
|
||||
rows={2}
|
||||
value={controller.draft}
|
||||
onChange={(event) => controller.setDraft(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
event.currentTarget.form?.requestSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button aria-label={controller.pending ? "正在核对" : "发送"} disabled={controller.pending || !controller.draft.trim()} size="icon" type="submit">
|
||||
<ArrowUp aria-hidden="true" />
|
||||
</Button>
|
||||
</form>
|
||||
<div className="composer-footer">
|
||||
<p>{controller.pending ? "正在核对这段经历…" : "Enter 发送 · Shift + Enter 换行"}</p>
|
||||
</div>
|
||||
</>}
|
||||
</div>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -416,9 +281,7 @@ type ConversationalBirthTimeRectificationProps = Readonly<{
|
||||
onContinueOriginalQuestion?: (question: string) => void;
|
||||
}>;
|
||||
|
||||
export function ConversationalBirthTimeRectification(
|
||||
props: ConversationalBirthTimeRectificationProps,
|
||||
) {
|
||||
export function ConversationalBirthTimeRectification(props: ConversationalBirthTimeRectificationProps) {
|
||||
const pendingChange = useRef(props.onPendingChange);
|
||||
useEffect(() => {
|
||||
pendingChange.current = props.onPendingChange;
|
||||
|
||||
Reference in New Issue
Block a user