Files
dbx/apps/desktop/src/lib/__tests__/aiProposalDetect.spec.ts
T
Abeautifulsnow bbd3ccf864 fix(ai): add inline proposal confirmation buttons
* fix(ai): add quick proposal confirmation buttons (all locales)

When the assistant proposes an action, render Yes/No buttons below the
message so the user does not need to type a short reply — fixing the
issue where short replies caused the LLM to re-ask instead of executing.

- aiProposalDetect.ts: pure detectors for proposal sentences/short replies
- AiAssistant.vue: inline Yes/No buttons on last assistant proposal message
- ai.ts: system-prompt rule to skip re-confirmation on short affirmatives
- i18n: 3 keys added to all 7 locales (en/zh-CN/zh-TW/ja/es/it/pt-BR)

Tests: 55/55. TypeCheck: passed.

* fix(ai): add i18n keys and UI changes for proposal confirmation
2026-06-23 18:25:57 +08:00

144 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import { containsChinese, isShortAffirmative, isShortNegative, looksLikeActionProposal } from "@/lib/aiProposalDetect";
describe("looksLikeActionProposal", () => {
// ---- zh-CN proposals ----
it("detects zh proposal: 需要我执行这个查询吗?", () => {
expect(looksLikeActionProposal("需要我执行这个查询吗?")).toBe(true);
});
it("detects zh proposal: 要不要我拉取这两个表的数据?", () => {
expect(looksLikeActionProposal("要不要我拉取这两个表的数据?")).toBe(true);
});
it("detects zh proposal: 是否需要我帮你查一下订单表?", () => {
expect(looksLikeActionProposal("是否需要我帮你查一下订单表?")).toBe(true);
});
it("detects zh proposal: 我来执行这个 SQL?", () => {
expect(looksLikeActionProposal("我来执行这个 SQL?")).toBe(true);
});
it("detects zh proposal in last line of multi-line message", () => {
const multi = "我已经查看了 orders 表的结构。\n现在需要我查看一下 users 表吗?";
expect(looksLikeActionProposal(multi)).toBe(true);
});
it("detects zh proposal: 我去看看这两个表的数据?", () => {
expect(looksLikeActionProposal("需要我去看看这两个表的数据?")).toBe(true);
});
// ---- en proposals ----
it("detects en proposal: Should I execute this query?", () => {
expect(looksLikeActionProposal("Should I execute this query?")).toBe(true);
});
it("detects en proposal: Do you want me to fetch the data?", () => {
expect(looksLikeActionProposal("Do you want me to fetch the data?")).toBe(true);
});
it("detects en proposal: Shall I run the analysis?", () => {
expect(looksLikeActionProposal("Shall I run the analysis?")).toBe(true);
});
it("detects en proposal: Can I pull the sample data?", () => {
expect(looksLikeActionProposal("Can I pull the sample data?")).toBe(true);
});
// ---- negative cases (NOT proposals) ----
it("rejects generic clarifying question without action", () => {
expect(looksLikeActionProposal("请问 orders 表有哪些字段?")).toBe(false);
});
it("rejects zh question without ask phrase", () => {
expect(looksLikeActionProposal("这个查询的执行结果正确吗?")).toBe(false);
});
it("rejects en question without ask phrase", () => {
expect(looksLikeActionProposal("What columns does the orders table have?")).toBe(false);
});
it("rejects statement ending with period", () => {
expect(looksLikeActionProposal("需要我执行这个查询。")).toBe(false);
});
it("rejects empty content", () => {
expect(looksLikeActionProposal("")).toBe(false);
});
it("rejects en question with no actionable verb", () => {
expect(looksLikeActionProposal("Should I proceed?")).toBe(false);
});
it("rejects zh proposal ending with period", () => {
expect(looksLikeActionProposal("需要我查看订单表的数据。")).toBe(false);
});
it("rejects message with no question mark", () => {
expect(looksLikeActionProposal("需要我执行查询")).toBe(false);
});
it("rejects mixed punctuation but not a question", () => {
expect(looksLikeActionProposal("可以了,谢谢!")).toBe(false);
});
});
describe("isShortAffirmative", () => {
// zh
it("matches 需要", () => expect(isShortAffirmative("需要")).toBe(true));
it("matches 好", () => expect(isShortAffirmative("好")).toBe(true));
it("matches 好的", () => expect(isShortAffirmative("好的")).toBe(true));
it("matches 可以", () => expect(isShortAffirmative("可以")).toBe(true));
it("matches 是", () => expect(isShortAffirmative("是")).toBe(true));
it("matches 对", () => expect(isShortAffirmative("对")).toBe(true));
it("matches 嗯", () => expect(isShortAffirmative("嗯")).toBe(true));
// zh with punctuation
it("strips punctuation: 好!", () => expect(isShortAffirmative("好!")).toBe(true));
it("strips punctuation: 好的。", () => expect(isShortAffirmative("好的。")).toBe(true));
// en
it("matches yes", () => expect(isShortAffirmative("yes")).toBe(true));
it("matches Yes (capitalized)", () => expect(isShortAffirmative("Yes")).toBe(true));
it("matches ok", () => expect(isShortAffirmative("ok")).toBe(true));
it("matches OK", () => expect(isShortAffirmative("OK")).toBe(true));
it("matches sure", () => expect(isShortAffirmative("sure")).toBe(true));
it("matches go ahead", () => expect(isShortAffirmative("go ahead")).toBe(true));
it("matches do it", () => expect(isShortAffirmative("do it")).toBe(true));
it("matches proceed", () => expect(isShortAffirmative("proceed")).toBe(true));
// negative
it("rejects long message", () => expect(isShortAffirmative("好的,我来看看具体情况")).toBe(false));
it("rejects unrelated word", () => expect(isShortAffirmative("其他")).toBe(false));
it("rejects empty", () => expect(isShortAffirmative("")).toBe(false));
});
describe("isShortNegative", () => {
// zh
it("matches 不用", () => expect(isShortNegative("不用")).toBe(true));
it("matches 不用了", () => expect(isShortNegative("不用了")).toBe(true));
it("matches 不需要", () => expect(isShortNegative("不需要")).toBe(true));
it("matches 不要", () => expect(isShortNegative("不要")).toBe(true));
it("matches 先不", () => expect(isShortNegative("先不")).toBe(true));
// zh with punctuation
it("strips punctuation: 不用了。", () => expect(isShortNegative("不用了。")).toBe(true));
// en
it("matches no", () => expect(isShortNegative("no")).toBe(true));
it("matches nope", () => expect(isShortNegative("nope")).toBe(true));
it("matches not needed", () => expect(isShortNegative("not needed")).toBe(true));
it("matches don't", () => expect(isShortNegative("don't")).toBe(true));
it("matches cancel", () => expect(isShortNegative("cancel")).toBe(true));
// negative
it("rejects long message", () => expect(isShortNegative("不用了,谢谢,我自己来查")).toBe(false));
it("rejects empty", () => expect(isShortNegative("")).toBe(false));
});
describe("containsChinese", () => {
it("detects Chinese characters", () => expect(containsChinese("需要我执行")).toBe(true));
it("rejects pure ASCII", () => expect(containsChinese("Should I execute")).toBe(false));
it("handles mixed CJK punctuation-only", () => expect(containsChinese("?。,")).toBe(false));
});