feat(ai): show reasoning character count when collapsed
This commit is contained in:
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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1277,6 +1277,7 @@ export default withEnglishFallback({
|
||||
newChat: "新しいチャット",
|
||||
thinking: "思考中...",
|
||||
reasoningProcess: "思考プロセス",
|
||||
chars: "文字",
|
||||
stopGenerating: "生成を停止",
|
||||
scrollToBottom: "一番下へ戻る",
|
||||
tableMentionEmpty: "一致するテーブルまたは SQL ファイルがありません",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1314,6 +1314,7 @@ export default withEnglishFallback({
|
||||
newChat: "新对话",
|
||||
thinking: "思考中...",
|
||||
reasoningProcess: "思考过程",
|
||||
chars: "字",
|
||||
stopGenerating: "停止生成",
|
||||
scrollToBottom: "回到底部",
|
||||
tableMentionEmpty: "没有匹配的表或 SQL 文件",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user