feat: streamline conversational birth-time rectification
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import path from "node:path";
|
||||
import {
|
||||
conversationalRectificationCommandSchema,
|
||||
type ConversationalRectificationCommand,
|
||||
@@ -25,6 +26,7 @@ import type { BirthTimeJourneyEngine, RectificationQuestionnaire } from "../../.
|
||||
import type { CandidateResult, LifeEvent } from "../../../lib/birth-time-evidence.ts";
|
||||
import type { CandidateDifferenceBuild } from "../../../lib/birth-time-dynamic-choice-internal.ts";
|
||||
import { conversationalRectificationCreationPolicyFromEnvironment } from "../../../lib/conversational-rectification/creation-policy.ts";
|
||||
import { MAXIMUM_SCOREABLE_EVENTS } from "../../../lib/conversational-rectification/convergence.ts";
|
||||
import {
|
||||
conversationalRectificationLatencyBucket,
|
||||
createConversationalRectificationTelemetry,
|
||||
@@ -37,6 +39,9 @@ import {
|
||||
export const runtime = "nodejs";
|
||||
export const maxDuration = 60;
|
||||
|
||||
const jyotishSkillPath = process.env.JYOTISH_SKILL_PATH?.trim()
|
||||
|| path.resolve(process.cwd(), "..", "skills", "jyotish-vedic-astrology");
|
||||
|
||||
type AuthenticatedRequest = Readonly<{
|
||||
userId: string;
|
||||
context: unknown;
|
||||
@@ -349,7 +354,7 @@ function scoreableLifeEvents(
|
||||
precision: item.datePrecision as "day" | "month" | "year",
|
||||
date: item.dateValue,
|
||||
} as LifeEvent];
|
||||
}).slice(-6);
|
||||
}).slice(-MAXIMUM_SCOREABLE_EVENTS);
|
||||
}
|
||||
|
||||
function sampleTimes(scan: RectificationQuestionnaire): readonly { readonly sampleIndex: number; readonly time: string }[] {
|
||||
@@ -665,7 +670,8 @@ async function productionNarrativeGenerator(): Promise<RectificationNarrativeGen
|
||||
id: `conversational-rectification-${model.id}`,
|
||||
name: "Conversational Rectification Narrator",
|
||||
model: model.model,
|
||||
instructions: "Return only the exact JSON object requested by the user prompt. Use only supplied packet facts. Never invent times, layers, references, scores, dates, or confirmation state.",
|
||||
skills: [jyotishSkillPath],
|
||||
instructions: "Return only the exact JSON object requested by the user prompt. Use the Jyotish Skill only to choose a natural, one-question-at-a-time evidence strategy and wording. Treat supplied packet facts as the exclusive source of candidate times, status, dates, layers, scores, references, and confirmation permissions. Never invent, recalculate, or confirm candidate data.",
|
||||
});
|
||||
return {
|
||||
modelId: model.id,
|
||||
|
||||
@@ -293,17 +293,19 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
.sidebar-label { min-height: 44px; display: flex; align-items: center; color: var(--sidebar-muted-foreground); font-size: var(--type-overline); font-weight: 500; letter-spacing: 1.5px; }
|
||||
.session-nav-toggle { min-height: 44px; padding: 0 var(--space-2); border: 0; border-radius: var(--radius-md); background: transparent; color: var(--sidebar-muted-foreground); cursor: pointer; font-size: var(--type-overline); transition: background-color 120ms ease-out, color 120ms ease-out; }
|
||||
.session-list { min-height: 0; display: flex; flex-direction: column; gap: var(--space-1); }
|
||||
.session-row { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) 44px; align-items: center; border-radius: var(--radius-md); color: var(--sidebar-muted-foreground); }
|
||||
.session-row { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) 44px; align-items: center; border-radius: var(--radius-md); color: var(--sidebar-muted-foreground); transition: background-color 120ms ease-out, color 120ms ease-out; }
|
||||
.session-row:hover, .session-row:focus-within { background: var(--sidebar-accent); color: var(--sidebar-accent-foreground); }
|
||||
.session-row:has(.session-main[data-active="true"]) { background: var(--sidebar-accent); color: var(--sidebar-accent-foreground); }
|
||||
.session-main { position: relative; width: 100%; min-height: 52px; display: grid; gap: 2px; padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4); border: 0; border-radius: var(--radius-md); background: transparent; color: inherit; cursor: pointer; text-align: left; transition: background-color 120ms ease-out, color 120ms ease-out, transform 120ms ease-out; }
|
||||
.session-main[data-active="true"] { color: var(--sidebar-accent-foreground); background: var(--sidebar-accent); }
|
||||
.session-main[data-active="true"] { color: var(--sidebar-accent-foreground); background: transparent; }
|
||||
.session-main[data-active="true"]::before { position: absolute; border-radius: 3px; content: ""; top: var(--space-3); bottom: var(--space-3); left: var(--space-1); width: 2px; background: var(--sidebar-ring); }
|
||||
.session-title { min-width: 0; display: flex; align-items: center; gap: var(--space-1); overflow: hidden; line-height: 1.35; font-size: var(--type-body-sm); font-weight: 500; }
|
||||
.session-title > svg { width: var(--space-3); height: var(--space-3); flex: 0 0 auto; color: var(--sidebar-ring); }
|
||||
.session-main small { color: inherit; line-height: 1.3; opacity: .72; font-size: var(--type-overline); }
|
||||
.session-menu-trigger { width: 44px; height: 44px; display: grid; place-items: center; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--sidebar-muted-foreground); cursor: pointer; opacity: .64; transition: opacity 120ms ease-out, background-color 120ms ease-out, color 120ms ease-out; }
|
||||
.session-menu-trigger { width: 44px; height: 44px; display: grid; place-items: center; padding: 0; border: 0; border-radius: 0; background: transparent; color: inherit; cursor: pointer; opacity: .64; transition: opacity 120ms ease-out, color 120ms ease-out; }
|
||||
.session-menu-trigger > svg { width: 18px; height: 18px; }
|
||||
.session-row:hover .session-menu-trigger, .session-row:focus-within .session-menu-trigger, .session-menu-trigger[aria-expanded="true"] { opacity: 1; }
|
||||
.session-menu-trigger:hover, .session-menu-trigger[aria-expanded="true"] { background: var(--sidebar-accent); color: var(--sidebar-accent-foreground); }
|
||||
.session-menu-trigger:hover, .session-menu-trigger[aria-expanded="true"] { background: transparent; color: inherit; }
|
||||
.session-actions { position: absolute; z-index: 4; top: calc(100% - var(--space-1)); right: 0; min-width: calc(var(--space-24) + var(--space-12)); display: grid; gap: var(--space-1); padding: var(--space-2); border: 1px solid var(--sidebar-border); border-radius: var(--radius-md); background: var(--color-canvas); box-shadow: var(--shadow-elevated); }
|
||||
.session-actions button { width: 100%; min-height: 44px; display: flex; align-items: center; gap: var(--space-2); padding: 0 var(--space-3); border: 0; border-radius: var(--radius-md); background: transparent; color: var(--color-ink-secondary); cursor: pointer; text-align: left; font-size: var(--type-body-sm); }
|
||||
.session-actions button > svg { width: 16px; height: 16px; }
|
||||
@@ -464,13 +466,6 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
.conversational-narrative .message-markdown > :first-child { margin-top: 0; }
|
||||
.conversational-narrative .message-markdown > :last-child { margin-bottom: 0; }
|
||||
.conversational-narrative .message-markdown h2, .conversational-narrative .message-markdown h3 { margin: var(--space-4) 0 var(--space-2); font-family: var(--font-sans); font-size: var(--type-body-md); font-weight: 600; letter-spacing: 0; }
|
||||
.conversational-domain-picker { display: grid; gap: var(--space-3); margin: 0; padding: 0; border: 0; }
|
||||
.conversational-domain-picker .conversational-domain-question { margin-bottom: var(--space-2); color: var(--color-ink); font-size: var(--type-body-md); font-weight: 600; line-height: 1.5; }
|
||||
.conversational-domain-picker > div { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: var(--space-2); }
|
||||
.conversational-domain-picker button { display: grid; gap: var(--space-1); padding: var(--space-2) var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); cursor: pointer; text-align: left; }
|
||||
.conversational-domain-picker button > span { font-size: var(--type-body-sm); font-weight: 600; }
|
||||
.conversational-domain-picker button > small { color: var(--color-ink-secondary); font-size: var(--type-overline); font-weight: 400; }
|
||||
.conversational-domain-picker button[aria-pressed="true"] { border-color: var(--color-action); background: var(--color-action-soft); color: var(--color-action); }
|
||||
.conversational-answer-pending { min-width: 0; display: grid; grid-template-columns: 40px minmax(0, 1fr); align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border: 1px solid color-mix(in srgb, var(--color-action) 24%, var(--color-border)); border-radius: var(--radius-lg); background: var(--color-action-soft); }
|
||||
.conversational-answer-pending .app-loading-symbol { width: 40px; height: 40px; }
|
||||
.conversational-answer-pending .app-loading-mark { width: 22px; height: 22px; }
|
||||
@@ -480,8 +475,6 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
.conversational-composer { display: grid; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); }
|
||||
.conversational-composer label { display: grid; gap: var(--space-1); font-size: var(--type-body-sm); font-weight: 600; }
|
||||
.conversational-composer label span { color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 400; line-height: 1.5; }
|
||||
.conversational-event-date { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); }
|
||||
.conversational-event-date label { gap: var(--space-2); color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 500; }
|
||||
.conversational-composer textarea { width: 100%; min-width: 0; min-height: 112px; resize: vertical; padding: var(--space-3); border: 1px solid var(--color-border-strong); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); line-height: 1.55; }
|
||||
.conversational-composer-footer { min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
|
||||
.conversational-composer-footer small { color: var(--color-ink-tertiary); font-size: var(--type-overline); }
|
||||
@@ -781,10 +774,31 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background:
|
||||
[data-sonner-toaster] { right: var(--space-4) !important; left: var(--space-4) !important; width: auto !important; }
|
||||
.conversational-rectification { width: 100%; max-width: 100%; gap: var(--space-4); overflow-x: clip; }
|
||||
.conversational-narrative-body, .conversational-composer, .conversational-evidence-recap, .conversational-candidate, .conversational-confirmation, .conversational-original-question, .conversational-empty-state { padding: var(--space-3); }
|
||||
.conversational-domain-picker > div, .conversational-candidate dl { grid-template-columns: minmax(0, 1fr); }
|
||||
.conversational-candidate dl { grid-template-columns: minmax(0, 1fr); }
|
||||
.conversational-composer-footer { align-items: stretch; flex-direction: column; }
|
||||
.conversational-composer-footer button, .conversational-confirmation button, .conversational-original-question button { width: 100%; }
|
||||
}
|
||||
|
||||
/* Conversational rectification shell */
|
||||
.conversation.is-rectification { overflow: hidden; padding-bottom: 0; }
|
||||
.rectification-chat { height: 100%; min-height: 0; display: flex; flex-direction: column; color: var(--color-ink); }
|
||||
.rectification-message-list { min-height: 0; flex: 1; overflow-y: auto; overscroll-behavior: contain; padding-bottom: var(--space-6); }
|
||||
.rectification-message-details { margin-top: var(--space-3); padding-top: var(--space-2); border-top: 1px solid var(--color-border); color: var(--color-ink-secondary); font-size: var(--type-caption); }
|
||||
.rectification-message-details summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; font-size: var(--type-body-sm); font-weight: 600; }
|
||||
.rectification-message-details p { margin: 0 0 var(--space-2); font-size: var(--type-caption); line-height: 1.6; }
|
||||
.rectification-message-details ul { display: grid; gap: var(--space-2); margin: var(--space-3) 0 0; padding: 0; list-style: none; }
|
||||
.rectification-message-details li { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--space-3); padding-top: var(--space-2); border-top: 1px solid var(--color-border); }
|
||||
.rectification-message-details button, .rectification-correction-target button { min-height: 44px; padding-inline: var(--space-3); border: 0; background: transparent; color: var(--color-action); cursor: pointer; }
|
||||
.rectification-composer-wrap { position: static; flex: 0 0 auto; }
|
||||
.rectification-correction-target { width: min(760px, 100%); display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin: 0 auto var(--space-2); color: var(--color-ink-secondary); font-size: var(--type-caption); }
|
||||
.rectification-correction-target p { margin: 0; }
|
||||
.conversational-empty-state { width: min(760px, 100%); margin: var(--space-8) auto; color: var(--color-ink-secondary); }
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.rectification-message-details li { align-items: stretch; grid-template-columns: minmax(0, 1fr); }
|
||||
.rectification-correction-target { align-items: stretch; display: grid; }
|
||||
}
|
||||
|
||||
/* Starter workbench */
|
||||
.welcome {
|
||||
width: min(1040px, 100%);
|
||||
|
||||
+44
-22
@@ -836,6 +836,7 @@ export default function Home() {
|
||||
createDurableRectificationQuestionHandoffClient(),
|
||||
);
|
||||
const resumeRectificationSession = useRef<(session: ChatSession) => void>(() => undefined);
|
||||
const rectificationOpenInFlight = useRef(false);
|
||||
const rectificationContinuationInFlight = useRef(false);
|
||||
const uiPreview = useRef(false);
|
||||
const uiPreviewMode = useRef<string | null>(null);
|
||||
@@ -853,7 +854,8 @@ export default function Home() {
|
||||
const visibleRectificationTurn = activeSession?.id === rectificationSessionId
|
||||
? rectificationInitialTurn
|
||||
: null;
|
||||
const rectificationSurfaceOpen = activeRectificationSession && visibleRectificationTurn !== null;
|
||||
const rectificationSurfaceOpen = activeRectificationSession
|
||||
&& activeSession.id === rectificationSessionId;
|
||||
const visibleSessions = sessions
|
||||
.filter((session) => showArchivedSessions ? archivedSessionIds.includes(session.id) : !archivedSessionIds.includes(session.id))
|
||||
.sort((left, right) => Number(pinnedSessionIds.includes(right.id)) - Number(pinnedSessionIds.includes(left.id)));
|
||||
@@ -1759,7 +1761,7 @@ export default function Home() {
|
||||
event.preventDefault();
|
||||
if (!selectedBirthPlace(profileDraft) || !account || profileSaving) return;
|
||||
setProfileSaving(true);
|
||||
setBirthTimeAssessmentPhase("saving_profile");
|
||||
setBirthTimeAssessmentPhase("entering_home");
|
||||
setAccountError("");
|
||||
try {
|
||||
await persistProfile(profileDraft);
|
||||
@@ -1906,7 +1908,8 @@ export default function Home() {
|
||||
pendingConsultationQuestion: string | null = null,
|
||||
sourceSessionOverride: ChatSession | null = null,
|
||||
) {
|
||||
if (!account || !modelCatalog || creatingSession || rectificationLoading || rectificationMutationPending
|
||||
if (!account || !modelCatalog || creatingSession || rectificationLoading || rectificationOpenInFlight.current
|
||||
|| rectificationMutationPending
|
||||
|| rectificationContinuationInFlight.current) return;
|
||||
const sourceSession = sourceSessionOverride ?? activeSession;
|
||||
if (!sourceSession) return;
|
||||
@@ -1936,6 +1939,7 @@ export default function Home() {
|
||||
? null
|
||||
: rectificationQuestionHandoff.current.peek()?.question)
|
||||
?? null;
|
||||
rectificationOpenInFlight.current = true;
|
||||
setDraft("");
|
||||
setDraftTheme(null);
|
||||
setDraftEntrypoint(null);
|
||||
@@ -1943,6 +1947,18 @@ export default function Home() {
|
||||
setRectificationInitialTurn(null);
|
||||
setRectificationError("");
|
||||
setRectificationLoading(true);
|
||||
if (!reusingRectificationSession) {
|
||||
setSessions((current) => [
|
||||
rectificationSession,
|
||||
...current.filter((session) => session.id !== rectificationSession.id),
|
||||
]);
|
||||
}
|
||||
if (sourceSession.id !== rectificationSession.id) {
|
||||
setRectificationReturnSessionId(sourceSession.id);
|
||||
}
|
||||
setRectificationSessionId(rectificationSession.id);
|
||||
activeSessionIdRef.current = rectificationSession.id;
|
||||
setActiveSessionId(rectificationSession.id);
|
||||
try {
|
||||
let turn: ConversationalRectificationTurn;
|
||||
if (action !== "resume" || !account.rectificationCase) {
|
||||
@@ -2008,12 +2024,6 @@ export default function Home() {
|
||||
}
|
||||
setRectificationInitialTurn(turn);
|
||||
synchronizeRectificationQuestion(turn, sourceSession);
|
||||
if (sourceSession.id !== boundSession.id) {
|
||||
setRectificationReturnSessionId(sourceSession.id);
|
||||
}
|
||||
setRectificationSessionId(boundSession.id);
|
||||
activeSessionIdRef.current = boundSession.id;
|
||||
setActiveSessionId(boundSession.id);
|
||||
setComposerNotice(sessionSyncFailed ? "校正已经开始,但会话关联暂时未同步到云端。" : "");
|
||||
} catch (caught) {
|
||||
const message = caught instanceof Error
|
||||
@@ -2021,7 +2031,14 @@ export default function Home() {
|
||||
: "生时校正暂时无法继续,请稍后重试。";
|
||||
setRectificationError(message);
|
||||
setComposerNotice(message);
|
||||
if (!reusingRectificationSession) {
|
||||
setSessions((current) => current.filter((session) => session.id !== rectificationSession.id));
|
||||
setRectificationSessionId(null);
|
||||
activeSessionIdRef.current = sourceSession.id;
|
||||
setActiveSessionId(sourceSession.id);
|
||||
}
|
||||
} finally {
|
||||
rectificationOpenInFlight.current = false;
|
||||
setRectificationLoading(false);
|
||||
}
|
||||
}
|
||||
@@ -2874,7 +2891,7 @@ export default function Home() {
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className={`conversation ${activeSession?.messages.length ? "" : "is-empty"}`}>
|
||||
<div className={`conversation ${activeSession?.messages.length ? "" : "is-empty"} ${rectificationSurfaceOpen ? "is-rectification" : ""}`}>
|
||||
{!rectificationSurfaceOpen && (!activeSession?.messages.length ? (
|
||||
<div className="welcome">
|
||||
{!profileComplete ? (
|
||||
@@ -3049,18 +3066,23 @@ export default function Home() {
|
||||
<div ref={conversationEnd} />
|
||||
</div>
|
||||
))}
|
||||
{rectificationSurfaceOpen && visibleRectificationTurn && (
|
||||
<section className="rectification-session-surface" aria-label="生时校正">
|
||||
<ConversationalBirthTimeRectification
|
||||
initialTurn={visibleRectificationTurn}
|
||||
pendingConsultationQuestion={rectificationPendingQuestion}
|
||||
continuationPending={rectificationContinuationPending}
|
||||
onPendingChange={setRectificationMutationPending}
|
||||
onTurn={handleConversationalRectificationTurn}
|
||||
onContinueOriginalQuestion={(question) => void continueRectificationOriginalQuestion(question)}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
{rectificationSurfaceOpen && (!visibleRectificationTurn && rectificationError ? (
|
||||
<div className="conversational-rectification-start-error" role="alert">
|
||||
<p>{rectificationError}</p>
|
||||
<button type="button" disabled={rectificationLoading} onClick={() => void openBirthTimeRectification(null, activeSession ?? null)}>
|
||||
{rectificationLoading ? "正在重试…" : "重新建立校正记录"}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<ConversationalBirthTimeRectification
|
||||
initialTurn={visibleRectificationTurn}
|
||||
pendingConsultationQuestion={rectificationPendingQuestion}
|
||||
continuationPending={rectificationContinuationPending}
|
||||
onPendingChange={setRectificationMutationPending}
|
||||
onTurn={handleConversationalRectificationTurn}
|
||||
onContinueOriginalQuestion={(question) => void continueRectificationOriginalQuestion(question)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!rectificationSurfaceOpen && <div className={`composer-wrap ${starterHomeVisible ? "composer-wrap-starter" : ""}`}>
|
||||
|
||||
Reference in New Issue
Block a user