feat(ai): show reasoning character count when collapsed

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-07-16 17:31:21 +08:00
1 parent 0f335119e3
commit 7c43c03254
10 files changed
+77

No files matched your search

@@ -78,6 +78,7 @@ import { AI_TABLE_MENTION_CANDIDATE_LIMIT, AI_TABLE_MENTION_SCHEMA_LIMIT, filter
import { isAiPromptImeCompositionEvent, shouldSubmitAiPromptOnKeydown } from "@/lib/ai/aiPromptKeyboard";
import { looksLikeActionProposal, containsChinese } from "@/lib/ai/aiProposalDetect";
import { visibleToActualIndex } from "@/lib/ai/aiMessageEdit";
import { shouldShowReasoningCharCount, reasoningCharCountClass } from "@/lib/ai/aiReasoningPresentation";
const { t } = useI18n();
const settings = useSettingsStore();
@@ -1871,6 +1872,7 @@ async function openExternalUrl(url: string) {
<ChevronRight class="h-3 w-3 transition-transform duration-200" :class="{ 'rotate-90': reasoningExpanded }" />
<Loader2 v-if="msg.isThinking" class="h-3 w-3 animate-spin" />
<span>{{ t("ai.reasoningProcess") }}</span>
<span v-if="shouldShowReasoningCharCount(msg.reasoning, reasoningExpanded)" :class="reasoningCharCountClass(!!msg.isThinking)">{{ msg.reasoning?.length ?? 0 }} {{ t("ai.chars") }}</span>
</button>
<div
class="overflow-hidden transition-[max-height,opacity] duration-200 ease-in-out"
+1
View File
@@ -1314,6 +1314,7 @@ export default {
newChat: "New Chat",
thinking: "Thinking...",
reasoningProcess: "Thinking process",
chars: "chars",
stopGenerating: "Stop generating",
scrollToBottom: "Back to bottom",
tableMentionEmpty: "No matching tables or SQL files",
+1
View File
@@ -1273,6 +1273,7 @@ export default withEnglishFallback({
newChat: "Nuevo chat",
thinking: "Procesando...",
reasoningProcess: "Proceso de razonamiento",
chars: "car.",
stopGenerating: "Detener generación",
scrollToBottom: "Volver al final",
tableMentionEmpty: "Sin tablas ni archivos SQL coincidentes",
+1
View File
@@ -1254,6 +1254,7 @@ export default withEnglishFallback({
newChat: "Nuova Chat",
thinking: "Riflessione...",
reasoningProcess: "Processo di riflessione",
chars: "car.",
stopGenerating: "Interrompi generazione",
scrollToBottom: "Torna in fondo",
tableMentionEmpty: "Nessuna tabella o file SQL corrispondente",
+1
View File
@@ -1277,6 +1277,7 @@ export default withEnglishFallback({
newChat: "新しいチャット",
thinking: "思考中...",
reasoningProcess: "思考プロセス",
chars: "文字",
stopGenerating: "生成を停止",
scrollToBottom: "一番下へ戻る",
tableMentionEmpty: "一致するテーブルまたは SQL ファイルがありません",
+1
View File
@@ -1275,6 +1275,7 @@ export default withEnglishFallback({
newChat: "Nova Conversa",
thinking: "Pensando...",
reasoningProcess: "Processo de raciocínio",
chars: "car.",
stopGenerating: "Parar geração",
scrollToBottom: "Voltar ao fim",
tableMentionEmpty: "Nenhuma tabela ou arquivo SQL correspondente",
+1
View File
@@ -1314,6 +1314,7 @@ export default withEnglishFallback({
newChat: "新对话",
thinking: "思考中...",
reasoningProcess: "思考过程",
chars: "字",
stopGenerating: "停止生成",
scrollToBottom: "回到底部",
tableMentionEmpty: "没有匹配的表或 SQL 文件",
+1
View File
@@ -1256,6 +1256,7 @@ export default withEnglishFallback({
newChat: "新對話",
thinking: "思考中……",
reasoningProcess: "思考過程",
chars: "字",
stopGenerating: "停止產生",
scrollToBottom: "回到底部",
tableMentionEmpty: "沒有相符的資料表或 SQL 檔案",
@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import { shouldShowReasoningCharCount, reasoningCharCountClass, REASONING_CHAR_COUNT_BASE_CLASS, REASONING_CHAR_COUNT_PULSE_CLASS } from "@/lib/ai/aiReasoningPresentation";
describe("shouldShowReasoningCharCount", () => {
it("shows the counter when reasoning exists and the panel is collapsed", () => {
expect(shouldShowReasoningCharCount("正在分析表结构...", false)).toBe(true);
});
it("hides the counter when the panel is expanded", () => {
expect(shouldShowReasoningCharCount("正在分析表结构...", true)).toBe(false);
});
it("hides the counter when reasoning is empty", () => {
expect(shouldShowReasoningCharCount("", false)).toBe(false);
});
it("hides the counter when reasoning is undefined", () => {
expect(shouldShowReasoningCharCount(undefined, false)).toBe(false);
});
});
describe("reasoningCharCountClass", () => {
it("includes the pulse class while the model is still thinking", () => {
const cls = reasoningCharCountClass(true);
expect(cls).toContain(REASONING_CHAR_COUNT_BASE_CLASS);
expect(cls).toContain(REASONING_CHAR_COUNT_PULSE_CLASS);
});
it("uses only the base class when thinking has finished", () => {
expect(reasoningCharCountClass(false)).toBe(REASONING_CHAR_COUNT_BASE_CLASS);
});
});
@@ -0,0 +1,36 @@
/**
* Pure presentation logic for the collapsed reasoning-process char counter.
*
* Kept side-effect-free so it can be unit-tested without a DOM harness, matching
* the existing pattern of `aiAgentStepPresentation.ts`. The AiAssistant template
* consumes these helpers so its render decisions (show/hide, pulse class, text)
* are covered by Vitest rather than only by the rendered template.
*/
/** Base CSS classes for the collapsed reasoning char counter span. */
export const REASONING_CHAR_COUNT_BASE_CLASS = "tabular-nums text-muted-foreground/60";
/** Extra class added while the model is still producing reasoning deltas. */
export const REASONING_CHAR_COUNT_PULSE_CLASS = "animate-pulse";
/**
* Decide whether the collapsed reasoning char counter should render.
* Visible only when there is reasoning text AND the panel is collapsed,
* so the counter acts as a "still alive" heartbeat without forcing the
* user to open the panel.
*
* @param reasoning Accumulated reasoning text (may be empty during initial thinking).
* @param expanded Whether the reasoning panel is currently expanded.
*/
export function shouldShowReasoningCharCount(reasoning: string | undefined, expanded: boolean): boolean {
return !expanded && !!reasoning && reasoning.length > 0;
}
/**
* Return the CSS class string for the counter span.
* Adds a pulse animation while the model is still thinking so the number
* reads as actively growing rather than frozen.
*/
export function reasoningCharCountClass(isThinking: boolean): string {
return isThinking ? `${REASONING_CHAR_COUNT_BASE_CLASS} ${REASONING_CHAR_COUNT_PULSE_CLASS}` : REASONING_CHAR_COUNT_BASE_CLASS;
}