From 0a5b8cd23930e8ffe93d8779519977523cacfa12 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Thu, 30 Jul 2026 00:41:48 +0800 Subject: [PATCH] fix: restore rectification agent message actions --- docs/BUG_HISTORY.md | 16 +++ .../v4/cases/[caseId]/regenerate/route.ts | 22 ++++ .../src/components/rectification-v4-panel.tsx | 118 +++++++++++++++++- frontend/src/hooks/use-rectification-v4.ts | 4 + .../lib/rectification-agent/renderer-agent.ts | 41 ++++++ .../src/lib/rectification-v4/case-service.ts | 35 ++++++ frontend/src/lib/rectification-v4/client.ts | 6 + .../src/lib/rectification-v4/memory-store.ts | 31 +++++ frontend/src/lib/rectification-v4/store.ts | 9 ++ .../lib/rectification-v4/supabase-store.ts | 22 ++++ ...ation_v4_current_question_regeneration.sql | 80 ++++++++++++ ...ersational-rectification-component.test.ts | 47 ++++++- .../conversational-visible-narrative.test.ts | 3 +- .../tests/rectification-v4-migration.test.ts | 14 +++ .../tests/rectification-v4-service.test.ts | 69 ++++++++++ 15 files changed, 513 insertions(+), 4 deletions(-) create mode 100644 frontend/src/app/api/rectification/v4/cases/[caseId]/regenerate/route.ts create mode 100644 frontend/supabase/migrations/20260729020000_rectification_v4_current_question_regeneration.sql diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index b6d279c8..5e4ae6c7 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -1676,3 +1676,19 @@ - 防复发:跨语言持久化指纹必须使用已知向量验证 JSON 数字规范化,不能只在各自语言内断言自洽。 - 相关记录:BUG-087、BUG-092 - 修复版本:`rectification-v5-matrix-scoring-1`(仅修复输入规范化,算法版本不变) + +## BUG-094 | V5 Agent 消息操作栏在 V4 面板切换后消失 + +- 状态:resolved +- 首次发现:2026-07-29 +- 最近更新:2026-07-29 +- 影响面:生时校正 V5 Agent 对话、助手消息反馈与当前问题重新生成 +- 用户现象:助手消息气泡下不再显示点赞、点踩、复制和重新生成操作。 +- 触发条件:生时校正页面使用 `RectificationV4Panel` 渲染 V5 Agent 会话。 +- 根因:旧对话组件中的消息操作栏没有迁入 V4/V5 共用面板,同时新 V4 API 没有与当前语义问题绑定的重新生成命令。 +- 修复:复用现有消息操作栏样式,仅为 `v5_agent` 的稳定助手消息恢复操作;重新生成只重写当前已验证 Semantic Question Opportunity 的自然语言实现,并通过用户、Case 版本、当前目标和 action ID 原子校验,不重跑事件提取、候选评分、诊断或 Job。 +- 验证:组件资格与反馈互斥测试、V4 service 幂等重放与数据不变量测试、Renderer 安全回落测试、迁移契约测试,以及 staging 精确 SHA 部署和浏览器验收。 +- 防复发:测试锁定 V5 Agent 操作栏、仅当前问题可重跑、legacy/shadow 不启用新 Renderer、重跑不改变 turns/events/snapshots/profile 且 completed Job 不被改写。 +- 相关记录:BUG-090、BUG-091、BUG-092、BUG-093 +- 复发自:无 +- 修复版本:`birth-time-rectification-v6` / `rectification-agent-v6-1` diff --git a/frontend/src/app/api/rectification/v4/cases/[caseId]/regenerate/route.ts b/frontend/src/app/api/rectification/v4/cases/[caseId]/regenerate/route.ts new file mode 100644 index 00000000..d98398c6 --- /dev/null +++ b/frontend/src/app/api/rectification/v4/cases/[caseId]/regenerate/route.ts @@ -0,0 +1,22 @@ +import { NextResponse } from "next/server"; +import { caseActionRequestSchema } from "@/lib/rectification-v4/contracts"; +import { rectificationV4Context, rectificationV4Error, requestBody, routeId } from "../../../_server"; + +export const runtime = "nodejs"; + +export async function POST(request: Request, { params }: { params: Promise<{ caseId: string }> }) { + try { + const body = await requestBody(request, caseActionRequestSchema); + const context = await rectificationV4Context(); + const result = await context.service.regenerateQuestion({ + ...body, + userId: context.userId, + caseId: routeId((await params).caseId), + }); + return result + ? NextResponse.json(result) + : NextResponse.json({ error: "当前问题不能重新生成,请刷新后重试。" }, { status: 409 }); + } catch (error) { + return rectificationV4Error(error); + } +} diff --git a/frontend/src/components/rectification-v4-panel.tsx b/frontend/src/components/rectification-v4-panel.tsx index 53b64b3b..8a42dcb5 100644 --- a/frontend/src/components/rectification-v4-panel.tsx +++ b/frontend/src/components/rectification-v4-panel.tsx @@ -1,6 +1,6 @@ "use client"; -import { ArrowUp } from "lucide-react"; +import { ArrowUp, Check, Copy, RotateCcw, ThumbsDown, ThumbsUp } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { useRectificationV4 } from "@/hooks/use-rectification-v4"; import type { ChatMessageView } from "@/lib/chat-message-view"; @@ -29,6 +29,28 @@ type RectificationV4PanelProps = Readonly<{ onContinueOriginalQuestion?: (continuation: RectificationV4Continuation) => void; }>; +export function toggleRectificationFeedback( + current: "up" | "down" | undefined, + requested: "up" | "down", +): "up" | "down" | undefined { + return current === requested ? undefined : requested; +} + +export function canRegenerateRectificationMessage(input: Readonly<{ + message: ChatMessageView; + currentMessageKey: string | null; + deploymentMode: RectificationV4ApiResponse["case"]["deploymentMode"] | null; + busy: boolean; + canAnswer: boolean; +}>): boolean { + return input.deploymentMode === "v5_agent" + && input.message.role === "assistant" + && input.message.state === "settled" + && input.message.renderKey === input.currentMessageKey + && !input.busy + && input.canAnswer; +} + export function rectificationV4ChatMessages( data: RectificationV4ApiResponse | null, processing: boolean, @@ -129,6 +151,9 @@ export function RectificationV4Panel(props: RectificationV4PanelProps) { onPendingChange: props.onPendingChange, }); const [draft, setDraft] = useState(""); + const [feedback, setFeedback] = useState>({}); + const [copiedMessageKey, setCopiedMessageKey] = useState(null); + const [regeneratingMessageKey, setRegeneratingMessageKey] = useState(null); const composer = useRef(null); const conversationEnd = useRef(null); const caseValue = controller.data?.case; @@ -145,6 +170,10 @@ export function RectificationV4Panel(props: RectificationV4PanelProps) { && !processing && !controller.pending && ["awaiting_answer", "range_ready"].includes(caseValue?.status ?? ""); + const currentMessageKey = caseValue?.currentQuestion + ? `rectification-current-${caseValue.currentQuestion.id}` + : null; + const busy = processing || controller.pending || regeneratingMessageKey !== null; const canAcceptRange = caseValue?.status === "range_ready" && Boolean(caseValue.latestSnapshot?.canAcceptRange) && !caseValue.acceptedRange; @@ -174,6 +203,27 @@ export function RectificationV4Panel(props: RectificationV4PanelProps) { if (result) setDraft(""); } + async function copyMessage(message: ChatMessageView) { + try { + await navigator.clipboard.writeText(message.text); + setCopiedMessageKey(message.renderKey); + window.setTimeout(() => setCopiedMessageKey((current) => ( + current === message.renderKey ? null : current + )), 1_500); + } catch { + // Clipboard permission failures must not interrupt the conversation. + } + } + + async function regenerateMessage(messageKey: string) { + setRegeneratingMessageKey(messageKey); + try { + await controller.regenerate(); + } finally { + setRegeneratingMessageKey((current) => current === messageKey ? null : current); + } + } + function continueOriginalQuestion() { if (!caseValue?.acceptedRange || !handoff) return; props.onContinueOriginalQuestion?.({ @@ -189,7 +239,71 @@ export function RectificationV4Panel(props: RectificationV4PanelProps) { <>
- {messages.map((message) => )} + {messages.map((message) => { + const showActions = caseValue?.deploymentMode === "v5_agent" + && message.role === "assistant" + && message.state === "settled" + && Boolean(message.text); + const regenerating = regeneratingMessageKey === message.renderKey; + const canRegenerate = canRegenerateRectificationMessage({ + message, + currentMessageKey, + deploymentMode: caseValue?.deploymentMode ?? null, + busy, + canAnswer, + }); + return ( +
+ + {showActions && !regenerating && ( +
+ + + + +
+ )} +
+ ); + })} {controller.error &&

{controller.error}

}
diff --git a/frontend/src/hooks/use-rectification-v4.ts b/frontend/src/hooks/use-rectification-v4.ts index 981a1f9c..abc027d2 100644 --- a/frontend/src/hooks/use-rectification-v4.ts +++ b/frontend/src/hooks/use-rectification-v4.ts @@ -16,6 +16,7 @@ import { loadRectificationV4, loadRectificationV4Handoff, loadRectificationV4Job, + regenerateRectificationV4Question, transitionRectificationV4, } from "@/lib/rectification-v4/client"; @@ -136,6 +137,9 @@ export function useRectificationV4(input: { answer: (answer: string, modelId?: string | null) => data ? mutate(() => answerRectificationV4(data.case.id, data.case.version, answer, modelId)) : Promise.resolve(null), + regenerate: () => data + ? mutate(() => regenerateRectificationV4Question(data.case.id, data.case.version)) + : Promise.resolve(null), pause: () => data ? mutate(() => transitionRectificationV4(data.case.id, data.case.version, "pause")) : Promise.resolve(null), diff --git a/frontend/src/lib/rectification-agent/renderer-agent.ts b/frontend/src/lib/rectification-agent/renderer-agent.ts index 5aa2d6f0..b7d3ca02 100644 --- a/frontend/src/lib/rectification-agent/renderer-agent.ts +++ b/frontend/src/lib/rectification-agent/renderer-agent.ts @@ -1,5 +1,6 @@ import path from "node:path"; import { Agent } from "@mastra/core/agent"; +import { z } from "zod"; import { defaultLanguageModel, resolveLanguageModel } from "@/mastra/model"; import type { CandidateSnapshot, LifeEventRevision, PendingEvidence, RectificationV4Case } from "../rectification-v4/contracts.ts"; import { publicMessageSchema, type PublicMessage, type QuestionOpportunity, type ValidatedDecision } from "./contracts.ts"; @@ -14,6 +15,7 @@ const multiQuestionMoves = /(?:另外|还有|同时再说|并且告诉我|顺便 const cannedQuestion = /(?:承接[“\"']?.{0,80}[”\"']?,?请再说一件|接下来请继续讲另一件|我会顺着你的叙述继续核对)/; const exactClockMinute = /(?:[01]?\d|2[0-3])[::][0-5]\d|(?:[零〇一二两三四五六七八九十]{1,3}|(?:[01]?\d|2[0-3]))点(?:[零〇一二两三四五六七八九十]{1,3}|[0-5]?\d)分/; const exactMinuteClaim = /(?:唯一|准确|精确|确切|确认|确定|代表).{0,12}(?:出生|生时)?(?:时间|时刻|分钟)|(?:出生|生时)(?:时间|时刻|分钟)?.{0,12}(?:唯一|准确|精确|确切|确认|确定|代表|就是)/; +const questionRealizationSchema = z.object({ question: z.string().trim().min(1).max(1_000) }).strict(); function agentFor(modelId: string | null): { id: string; agent: Agent } | null { const selected = (modelId ? resolveLanguageModel(modelId) : null) ?? defaultLanguageModel(); @@ -199,3 +201,42 @@ export async function renderPublicTurn(input: Readonly<{ return fallback; } } + +export async function regenerateQuestionRealization(input: Readonly<{ + caseValue: RectificationV4Case; + currentPrompt: string; + latestAnswer: string; + acceptedEvents: readonly LifeEventRevision[]; + opportunity: QuestionOpportunity; + timeoutMs?: number; +}>): Promise { + const selected = agentFor(input.caseValue.narrationModelId); + if (!selected) return input.currentPrompt; + try { + const result = await selected.agent.generate(JSON.stringify({ + task: "Rewrite the current question naturally without changing its semantic target. Return one question only.", + currentPrompt: input.currentPrompt, + latestAnswer: input.latestAnswer, + recentEvents: input.acceptedEvents.slice(-5).map((event) => ({ + summary: event.summary, + date: event.dateRange.label, + subject: event.subject, + })), + selectedOpportunity: { + kind: input.opportunity.kind, + goal: input.opportunity.goal, + requestedFields: input.opportunity.requestedFields, + anchors: input.opportunity.anchors, + contextFacts: input.opportunity.contextFacts, + forbiddenMoves: input.opportunity.forbiddenMoves, + }, + }), { + abortSignal: AbortSignal.timeout(input.timeoutMs ?? 15_000), + structuredOutput: { schema: questionRealizationSchema, jsonPromptInjection: "inline" }, + }); + const question = questionRealizationSchema.parse(result.object).question; + return validateQuestionRealization(question, input.opportunity).valid ? question : input.currentPrompt; + } catch { + return input.currentPrompt; + } +} diff --git a/frontend/src/lib/rectification-v4/case-service.ts b/frontend/src/lib/rectification-v4/case-service.ts index 8766a152..db4fc5fc 100644 --- a/frontend/src/lib/rectification-v4/case-service.ts +++ b/frontend/src/lib/rectification-v4/case-service.ts @@ -8,6 +8,7 @@ import type { import { rectificationAgentV5Protocol, rectificationV4AlgorithmVersion, rectificationV4Protocol } from "./contracts.ts"; import { selectRectificationDeploymentMode } from "../rectification-agent/feature-policy.ts"; import { CURRENT_RECTIFICATION_PROMPT_VERSION, CURRENT_RECTIFICATION_SKILL_VERSION } from "../rectification-agent/contracts.ts"; +import { regenerateQuestionRealization } from "../rectification-agent/renderer-agent.ts"; import { calculationSpecHash, evidenceSetHash } from "./fingerprints.ts"; import { openingQuestion } from "./opening-question.ts"; import type { RectificationV4Store } from "./store.ts"; @@ -88,6 +89,40 @@ export function createRectificationV4CaseService(store: RectificationV4Store, op return response(input.userId, saved.case, saved.job.id); }, + async regenerateQuestion(input: { + readonly userId: string; + readonly caseId: string; + readonly actionId: string; + readonly expectedCaseVersion: number; + }) { + const current = await store.loadCase(input.userId, input.caseId); + if (!current?.currentQuestion || current.deploymentMode !== "v5_agent") return null; + const validated = await store.loadLatestValidatedDecision(input.userId, input.caseId); + const opportunity = validated?.selectedOpportunity; + if (!opportunity) return null; + const [events, turns] = await Promise.all([ + store.loadEvents(input.userId, input.caseId), + store.loadTurns(input.userId, input.caseId), + ]); + const prompt = await regenerateQuestionRealization({ + caseValue: current, + currentPrompt: current.currentQuestion.prompt, + latestAnswer: turns.at(-1)?.answer ?? "", + acceptedEvents: events, + opportunity, + }); + const nextQuestion = { + ...current.currentQuestion, + id: randomUUID(), + prompt, + }; + return response(input.userId, await store.replaceCurrentQuestion({ + ...input, + question: nextQuestion, + now: now().toISOString(), + })); + }, + async reviseEvent(input: { readonly userId: string; readonly caseId: string; diff --git a/frontend/src/lib/rectification-v4/client.ts b/frontend/src/lib/rectification-v4/client.ts index 5c8af417..b0fe65ff 100644 --- a/frontend/src/lib/rectification-v4/client.ts +++ b/frontend/src/lib/rectification-v4/client.ts @@ -63,6 +63,12 @@ export function answerRectificationV4(caseId: string, expectedCaseVersion: numbe }); } +export function regenerateRectificationV4Question(caseId: string, expectedCaseVersion: number) { + return post(`/api/rectification/v4/cases/${caseId}/regenerate`, { + actionId: globalThis.crypto.randomUUID(), expectedCaseVersion, + }); +} + export function transitionRectificationV4( caseId: string, expectedCaseVersion: number, diff --git a/frontend/src/lib/rectification-v4/memory-store.ts b/frontend/src/lib/rectification-v4/memory-store.ts index aef2c457..37f6522c 100644 --- a/frontend/src/lib/rectification-v4/memory-store.ts +++ b/frontend/src/lib/rectification-v4/memory-store.ts @@ -69,6 +69,14 @@ export function createRectificationV4MemoryStore(): RectificationV4Store & { .filter((turn) => turn.caseId === caseId) .sort((left, right) => left.caseVersion - right.caseVersion || left.createdAt.localeCompare(right.createdAt)); }, + async loadLatestValidatedDecision(userId, caseId) { + const caseValue = cases.get(caseId); + if (!caseValue || caseValue.userId !== userId) return null; + const latest = [...agentRuns.values()] + .filter((run) => run.caseId === caseId) + .sort((left, right) => right.caseVersion - left.caseVersion || right.createdAt.localeCompare(left.createdAt))[0]; + return latest ? validatedDecisions.get(latest.jobId) ?? latest.validatedDecision : null; + }, async createCase(input) { const replay = actionResults.get(`${input.case.userId}:${input.actionId}`); if (replay) return owned(input.case.userId, replay.caseId); @@ -91,6 +99,29 @@ export function createRectificationV4MemoryStore(): RectificationV4Store & { actionResults.set(`${input.case.userId}:${input.actionId}`, { caseId: input.case.id, jobId: null }); return input.case; }, + async replaceCurrentQuestion(input) { + const key = `${input.userId}:${input.actionId}`; + const replay = actionResults.get(key); + if (replay) return owned(input.userId, replay.caseId); + const current = owned(input.userId, input.caseId); + if (current.version !== input.expectedCaseVersion) throw new RectificationV4StoreError("stale_version"); + if (current.deploymentMode !== "v5_agent" + || !["awaiting_answer", "range_ready"].includes(current.status) + || !current.currentQuestion) throw new RectificationV4StoreError("invalid_state"); + const updated: RectificationV4Case = { + ...current, + version: current.version + 1, + currentQuestion: { + ...input.question, + domain: current.currentQuestion.domain, + targetEventId: current.currentQuestion.targetEventId, + }, + updatedAt: input.now, + }; + cases.set(current.id, updated); + actionResults.set(key, { caseId: current.id, jobId: null }); + return updated; + }, async submitAnswer(input) { const key = `${input.userId}:${input.actionId}`; const replay = actionResults.get(key); diff --git a/frontend/src/lib/rectification-v4/store.ts b/frontend/src/lib/rectification-v4/store.ts index 812d3dcc..1601d21c 100644 --- a/frontend/src/lib/rectification-v4/store.ts +++ b/frontend/src/lib/rectification-v4/store.ts @@ -45,7 +45,16 @@ export interface RectificationV4Store { loadCase(userId: string, caseId: string): Promise; loadEvents(userId: string, caseId: string): Promise; loadTurns(userId: string, caseId: string): Promise; + loadLatestValidatedDecision(userId: string, caseId: string): Promise; createCase(input: { readonly case: RectificationV4Case; readonly actionId: string }): Promise; + replaceCurrentQuestion(input: { + readonly userId: string; + readonly caseId: string; + readonly actionId: string; + readonly expectedCaseVersion: number; + readonly question: RectificationV4Question; + readonly now: string; + }): Promise; submitAnswer(input: { readonly userId: string; readonly caseId: string; diff --git a/frontend/src/lib/rectification-v4/supabase-store.ts b/frontend/src/lib/rectification-v4/supabase-store.ts index 045bd25e..3ae3e1d9 100644 --- a/frontend/src/lib/rectification-v4/supabase-store.ts +++ b/frontend/src/lib/rectification-v4/supabase-store.ts @@ -1,4 +1,5 @@ import type { SupabaseClient } from "@supabase/supabase-js"; +import { validatedDecisionSchema, type ValidatedDecision } from "../rectification-agent/contracts.ts"; import { candidateSnapshotSchema, lifeEventRevisionSchema, @@ -199,6 +200,14 @@ export function createRectificationV4SupabaseStore(supabase: SupabaseClient): Re loadCase: loadCaseById, loadEvents: loadEventsByCase, loadTurns: loadTurnsByCase, + async loadLatestValidatedDecision(userId, caseId): Promise { + const { data, error } = await supabase.from("birth_time_rectification_agent_runs") + .select("validated_decision_json").eq("case_id", caseId).eq("user_id", userId) + .order("case_version", { ascending: false }).order("created_at", { ascending: false }) + .limit(1).maybeSingle(); + if (error) throw storeError(error); + return data ? validatedDecisionSchema.parse((data as Row).validated_decision_json) : null; + }, async createCase(input) { const id = String(await rpc("create_birth_time_rectification_v5_case", { p_user_id: input.case.userId, @@ -222,6 +231,19 @@ export function createRectificationV4SupabaseStore(supabase: SupabaseClient): Re if (!value) throw new RectificationV4StoreError("not_found"); return value; }, + async replaceCurrentQuestion(input) { + const id = String(await rpc("replace_birth_time_rectification_v4_current_question", { + p_user_id: input.userId, + p_case_id: input.caseId, + p_action_id: input.actionId, + p_expected_version: input.expectedCaseVersion, + p_question: input.question, + p_now: input.now, + })); + const value = await loadCaseById(input.userId, id); + if (!value) throw new RectificationV4StoreError("not_found"); + return value; + }, async submitAnswer(input) { const jobId = String(await rpc("submit_birth_time_rectification_v4_answer", { p_user_id: input.userId, diff --git a/frontend/supabase/migrations/20260729020000_rectification_v4_current_question_regeneration.sql b/frontend/supabase/migrations/20260729020000_rectification_v4_current_question_regeneration.sql new file mode 100644 index 00000000..210d837d --- /dev/null +++ b/frontend/supabase/migrations/20260729020000_rectification_v4_current_question_regeneration.sql @@ -0,0 +1,80 @@ +begin; + +create or replace function public.replace_birth_time_rectification_v4_current_question( + p_user_id uuid, + p_case_id uuid, + p_action_id uuid, + p_expected_version bigint, + p_question jsonb, + p_now timestamptz +) returns uuid +language plpgsql security definer set search_path = '' as $$ +declare + v_case public.birth_time_rectification_v4_cases%rowtype; + v_case_id uuid; + v_question jsonb; +begin + select action.case_id into v_case_id + from public.birth_time_rectification_v4_actions action + where action.user_id = p_user_id and action.action_id = p_action_id; + if v_case_id is not null then return v_case_id; end if; + + select value.* into v_case + from public.birth_time_rectification_v4_cases value + where value.id = p_case_id and value.user_id = p_user_id + for update; + if not found then raise exception 'rectification_v4_case_not_found'; end if; + + select action.case_id into v_case_id + from public.birth_time_rectification_v4_actions action + where action.user_id = p_user_id and action.action_id = p_action_id; + if v_case_id is not null then return v_case_id; end if; + + if v_case.version <> p_expected_version then raise exception 'stale_rectification_v4_case'; end if; + if v_case.deployment_mode <> 'v5_agent' + or v_case.status not in ('awaiting_answer', 'range_ready') + or v_case.current_question is null then + raise exception 'rectification_v4_question_not_regenerable'; + end if; + if p_question is null or pg_catalog.jsonb_typeof(p_question) <> 'object' then + raise exception 'invalid_rectification_v4_question'; + end if; + if nullif(pg_catalog.btrim(p_question->>'id'), '') is null + or (p_question->>'id') !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + or nullif(pg_catalog.btrim(p_question->>'prompt'), '') is null + or pg_catalog.length(pg_catalog.btrim(p_question->>'prompt')) > 1000 + or p_question->>'recallCost' not in ('low', 'medium', 'high') + or nullif(pg_catalog.btrim(p_question->>'reason'), '') is null + or pg_catalog.length(pg_catalog.btrim(p_question->>'reason')) > 240 then + raise exception 'invalid_rectification_v4_question'; + end if; + + v_question := p_question || pg_catalog.jsonb_build_object( + 'domain', v_case.current_question->'domain', + 'targetEventId', v_case.current_question->'targetEventId' + ); + + update public.birth_time_rectification_v4_cases + set version = p_expected_version + 1, + current_question = v_question, + updated_at = p_now + where id = p_case_id; + + insert into public.birth_time_rectification_v4_actions( + user_id, action_id, case_id, created_at + ) values ( + p_user_id, p_action_id, p_case_id, p_now + ); + + return p_case_id; +end; +$$; + +revoke all on function public.replace_birth_time_rectification_v4_current_question( + uuid, uuid, uuid, bigint, jsonb, timestamptz +) from public, anon, authenticated, service_role; +grant execute on function public.replace_birth_time_rectification_v4_current_question( + uuid, uuid, uuid, bigint, jsonb, timestamptz +) to service_role; + +commit; diff --git a/frontend/tests/conversational-rectification-component.test.ts b/frontend/tests/conversational-rectification-component.test.ts index 667c1e43..7030d996 100644 --- a/frontend/tests/conversational-rectification-component.test.ts +++ b/frontend/tests/conversational-rectification-component.test.ts @@ -1,7 +1,11 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; -import { rectificationV4ChatMessages } from "../src/components/rectification-v4-panel.tsx"; +import { + canRegenerateRectificationMessage, + rectificationV4ChatMessages, + toggleRectificationFeedback, +} from "../src/components/rectification-v4-panel.tsx"; import type { RectificationV4ApiResponse } from "../src/lib/rectification-v4/contracts.ts"; const id = "00000000-0000-4000-8000-000000000901"; @@ -87,6 +91,11 @@ test("v4 rectification reuses the ordinary session message list, composer, and m assert.match(component, /className="composer"/); assert.match(component, /