* 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
144 lines
6.3 KiB
TypeScript
144 lines
6.3 KiB
TypeScript
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));
|
||
});
|