refactor(chat): remove the post-answer suggestion chips and the table that fed them
Measured use of the three chips above the composer was negligible. They were also not what they appeared to be: the server looked up a fixed triplet by session theme and passed it as metadata that overrode anything the model produced, so the same ten hardcoded sets served every user regardless of question or chart. That is a plausible reason nobody pressed them. Both copies of the per-theme table are gone, reply metadata narrows to the session title, and the two parse entry points collapse into one now that they return the same shape. The write schema still tolerates a suggestions field so a client on the previous bundle does not lose its message mid-deploy, and stored answers containing the legacy hidden block are still stripped rather than shown raw. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,27 +4,6 @@ export type ReplyTheme = ConsultationDomain;
|
||||
|
||||
import type { ConsultationReplyMetadata } from "./consultation-reply-metadata.ts";
|
||||
|
||||
const fallbackSuggestions: Record<ReplyTheme, readonly [string, string, string]> = {
|
||||
career: ["我更适合怎样的职业路径?", "未来一年事业上要避开什么?", "我该如何发挥自己的优势?"],
|
||||
marriage: ["我在关系里容易重复什么模式?", "怎样的伴侣更适合我?", "未来一年关系上要注意什么?"],
|
||||
wealth: ["我的财富增长方式是什么?", "接下来财务上要避开什么?", "我该如何稳定提升收入?"],
|
||||
health: ["近期压力主要来自哪里?", "怎样安排休息与恢复?", "哪些信号值得持续观察?"],
|
||||
education: ["我更适合怎样的学习路径?", "现在该补哪项能力?", "什么阶段适合考试或进修?"],
|
||||
migration: ["迁居方向该优先考虑什么?", "置业与流动之间如何取舍?", "海外发展要注意哪些条件?"],
|
||||
family: ["家庭关系中该建立什么边界?", "我承担的责任是否失衡?", "接下来适合如何沟通?"],
|
||||
annual: ["未来一年最重要的主题是什么?", "哪些阶段适合主动推进?", "哪些阶段更适合调整?"],
|
||||
timing: ["接下来最值得把握的阶段是什么?", "哪些时期更适合主动行动?", "我现在应该优先准备什么?"],
|
||||
general: ["未来一年,事业和收入该关注什么?", "我的关系模式是什么?", "未来哪些阶段值得把握?"],
|
||||
};
|
||||
|
||||
function readSuggestions(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return [...new Set(value
|
||||
.filter((item): item is string => typeof item === "string")
|
||||
.map((item) => item.replace(/\s+/g, " ").trim().slice(0, 80))
|
||||
.filter(Boolean))].slice(0, 3);
|
||||
}
|
||||
|
||||
function readTitle(value: string): string | undefined {
|
||||
const title = value.replace(/\s+/g, " ").trim();
|
||||
if (!title || /[\d\p{P}\p{S}]/u.test(title)) return undefined;
|
||||
@@ -36,37 +15,26 @@ function readTitle(value: string): string | undefined {
|
||||
return words.length >= 3 && words.length <= 7 && title.length <= 64 ? title : undefined;
|
||||
}
|
||||
|
||||
// AYANAM_SUGGESTIONS blocks are no longer produced anywhere, but stored answers from
|
||||
// before the follow-up chips were removed still carry them, so they stay strippable
|
||||
// rather than leaking a raw HTML comment into a replayed transcript.
|
||||
function stripAgentReplyMetadata(value: string) {
|
||||
let suggestions: string[] = [];
|
||||
let title: string | undefined;
|
||||
const withoutSuggestions = value.replace(/<!--AYANAM_SUGGESTIONS:(\[[\s\S]*?\])-->/g, (_, json: string) => {
|
||||
try {
|
||||
suggestions = readSuggestions(JSON.parse(json));
|
||||
} catch {
|
||||
suggestions = [];
|
||||
}
|
||||
return "";
|
||||
});
|
||||
const text = withoutSuggestions.replace(/<!--AYANAM_TITLE:([\s\S]*?)-->/g, (_, rawTitle: string) => {
|
||||
title = readTitle(rawTitle);
|
||||
return "";
|
||||
}).replace(/<!--AYANAM_[\s\S]*$/, "").trim();
|
||||
const text = value
|
||||
.replace(/<!--AYANAM_SUGGESTIONS:\[[\s\S]*?\]-->/g, "")
|
||||
.replace(/<!--AYANAM_TITLE:([\s\S]*?)-->/g, (_, rawTitle: string) => {
|
||||
title = readTitle(rawTitle);
|
||||
return "";
|
||||
})
|
||||
.replace(/<!--AYANAM_[\s\S]*$/, "")
|
||||
.trim();
|
||||
|
||||
return { text, suggestions, title };
|
||||
return { text, title };
|
||||
}
|
||||
|
||||
export function parseAgentReply(value: string, theme: ReplyTheme, metadata?: ConsultationReplyMetadata) {
|
||||
export function parseAgentReply(value: string, metadata?: ConsultationReplyMetadata) {
|
||||
const parsed = stripAgentReplyMetadata(value);
|
||||
return {
|
||||
text: parsed.text,
|
||||
suggestions: metadata?.suggestions ?? (parsed.suggestions.length === 3 ? parsed.suggestions : [...fallbackSuggestions[theme]]),
|
||||
title: metadata?.title ?? parsed.title,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseAgentReplyBody(value: string) {
|
||||
const parsed = stripAgentReplyMetadata(value);
|
||||
return { text: parsed.text, title: parsed.title };
|
||||
return { text: parsed.text, title: metadata?.title ?? parsed.title };
|
||||
}
|
||||
|
||||
export function resolveSessionTitle(question: string, modelTitle?: string): string {
|
||||
|
||||
@@ -8,7 +8,6 @@ export type AgentActivityView = Readonly<{
|
||||
export type ChatMessage = {
|
||||
readonly role: "user" | "assistant";
|
||||
readonly text: string;
|
||||
readonly suggestions?: readonly string[];
|
||||
readonly techniqueTruth?: string;
|
||||
readonly agentExecutionReceipt?: AgentExecutionReceipt;
|
||||
readonly workflowReceipt?: WorkflowReceipt;
|
||||
|
||||
@@ -10,6 +10,9 @@ import { consultationDomainSchema, type ConsultationDomain } from "./consultatio
|
||||
const chatMessageSchema = z.object({
|
||||
role: z.enum(["user", "assistant"]),
|
||||
text: z.string().max(100_000),
|
||||
// Nothing writes suggestions since the follow-up chips were removed, but this schema
|
||||
// is strict and a client running the previous bundle still sends them; rejecting the
|
||||
// whole write would lose that user's message rather than a dead field.
|
||||
suggestions: z.array(z.string().max(200)).max(3).optional(),
|
||||
techniqueTruth: z.string().max(120).optional(),
|
||||
agentExecutionReceipt: agentExecutionReceiptSchema.optional(),
|
||||
|
||||
@@ -1,30 +1,11 @@
|
||||
import { z } from "zod";
|
||||
import type { ReplyTheme } from "./agent-reply.ts";
|
||||
|
||||
export const consultationReplyMetadataSchema = z.object({
|
||||
title: z.string().trim().min(1).max(64),
|
||||
suggestions: z.tuple([
|
||||
z.string().trim().min(1).max(80),
|
||||
z.string().trim().min(1).max(80),
|
||||
z.string().trim().min(1).max(80),
|
||||
]),
|
||||
}).strict();
|
||||
|
||||
export type ConsultationReplyMetadata = z.infer<typeof consultationReplyMetadataSchema>;
|
||||
|
||||
const fallbackSuggestions: Record<ReplyTheme, readonly [string, string, string]> = {
|
||||
career: ["我更适合怎样的职业路径?", "未来一年事业上要避开什么?", "我该如何发挥自己的优势?"],
|
||||
marriage: ["我在关系里容易重复什么模式?", "怎样的伴侣更适合我?", "未来一年关系上要注意什么?"],
|
||||
wealth: ["我的财富增长方式是什么?", "接下来财务上要避开什么?", "我该如何稳定提升收入?"],
|
||||
health: ["近期压力主要来自哪里?", "怎样安排休息与恢复?", "哪些信号值得持续观察?"],
|
||||
education: ["我更适合怎样的学习路径?", "现在该补哪项能力?", "什么阶段适合考试或进修?"],
|
||||
migration: ["迁居方向该优先考虑什么?", "置业与流动之间如何取舍?", "海外发展要注意哪些条件?"],
|
||||
family: ["家庭关系中该建立什么边界?", "我承担的责任是否失衡?", "接下来适合如何沟通?"],
|
||||
annual: ["未来一年最重要的主题是什么?", "哪些阶段适合主动推进?", "哪些阶段更适合调整?"],
|
||||
timing: ["接下来最值得把握的阶段是什么?", "哪些时期更适合主动行动?", "我现在应该优先准备什么?"],
|
||||
general: ["未来一年,事业和收入该关注什么?", "我的关系模式是什么?", "未来哪些阶段值得把握?"],
|
||||
};
|
||||
|
||||
function safeQuestionTitle(question: string) {
|
||||
const normalized = question
|
||||
.replace(/\s+/g, " ")
|
||||
@@ -38,9 +19,6 @@ function safeQuestionTitle(question: string) {
|
||||
return `${characters.join("")}主题咨询`.slice(0, 14);
|
||||
}
|
||||
|
||||
export function createConsultationReplyMetadata(input: { theme: ReplyTheme; question: string }): ConsultationReplyMetadata {
|
||||
return consultationReplyMetadataSchema.parse({
|
||||
title: safeQuestionTitle(input.question),
|
||||
suggestions: [...fallbackSuggestions[input.theme]],
|
||||
});
|
||||
export function createConsultationReplyMetadata(input: { question: string }): ConsultationReplyMetadata {
|
||||
return consultationReplyMetadataSchema.parse({ title: safeQuestionTitle(input.question) });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user