feat(chat): 极简模式摘要行改为单行渐变+工具统计总结
This commit is contained in:
parent
478d24c4dd
commit
2264a86851
@ -34,7 +34,9 @@
|
|||||||
</span>
|
</span>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span
|
<span
|
||||||
v-for="(char, idx) in getAnimatedSummaryChars(getSummaryPreview(group.actions))"
|
v-for="(char, idx) in getAnimatedSummaryChars(
|
||||||
|
getSummaryLineText(group.actions, group.id)
|
||||||
|
)"
|
||||||
:key="`${group.id}-${idx}`"
|
:key="`${group.id}-${idx}`"
|
||||||
class="summary-char"
|
class="summary-char"
|
||||||
:style="{ animationDelay: `${(idx + 1) * 0.12}s` }"
|
:style="{ animationDelay: `${(idx + 1) * 0.12}s` }"
|
||||||
@ -44,7 +46,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ getSummaryPreview(group.actions) }}
|
{{ getSummaryLineText(group.actions, group.id) }}
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -208,7 +210,6 @@ const personalizationStore = usePersonalizationStore();
|
|||||||
const expandedGroups = ref(new Set<string>());
|
const expandedGroups = ref(new Set<string>());
|
||||||
const thinkingRefs = new Map<string, HTMLElement>();
|
const thinkingRefs = new Map<string, HTMLElement>();
|
||||||
const summaryLoaders = new Map<string, Component>();
|
const summaryLoaders = new Map<string, Component>();
|
||||||
const SUMMARY_PREVIEW_MAX_CHARS = 25;
|
|
||||||
const TOOL_REEL_ITEM_HEIGHT = 26;
|
const TOOL_REEL_ITEM_HEIGHT = 26;
|
||||||
const TOOL_REEL_INTERVAL_MS = 1450;
|
const TOOL_REEL_INTERVAL_MS = 1450;
|
||||||
const TOOL_REEL_ROLL_MS = 520;
|
const TOOL_REEL_ROLL_MS = 520;
|
||||||
@ -230,14 +231,6 @@ const toolReelStates = reactive<Record<string, ToolReelState>>({});
|
|||||||
const toolReelIntervals = new Map<string, number>();
|
const toolReelIntervals = new Map<string, number>();
|
||||||
const toolReelTimeouts = new Map<string, number[]>();
|
const toolReelTimeouts = new Map<string, number[]>();
|
||||||
|
|
||||||
const truncateSummaryPreview = (raw: string) => {
|
|
||||||
const text = typeof raw === 'string' ? raw : '';
|
|
||||||
if (text.length > SUMMARY_PREVIEW_MAX_CHARS) {
|
|
||||||
return `${text.slice(0, SUMMARY_PREVIEW_MAX_CHARS)}...`;
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAnimatedSummaryChars = (text: string) => Array.from(text || '');
|
const getAnimatedSummaryChars = (text: string) => Array.from(text || '');
|
||||||
|
|
||||||
const getFirstLine = (raw: string) => {
|
const getFirstLine = (raw: string) => {
|
||||||
@ -254,7 +247,7 @@ const getToolSummaryText = (action: Action) => {
|
|||||||
const intentText = tool.intent_rendered || tool.intent_full || '';
|
const intentText = tool.intent_rendered || tool.intent_full || '';
|
||||||
|
|
||||||
if (intentEnabled && intentText) {
|
if (intentEnabled && intentText) {
|
||||||
return truncateSummaryPreview(getFirstLine(intentText));
|
return getFirstLine(intentText);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tool.status === 'preparing') {
|
if (tool.status === 'preparing') {
|
||||||
@ -426,10 +419,13 @@ const finishToolReel = (groupId: string, actions: Action[]) => {
|
|||||||
}, TOOL_REEL_ROLL_MS);
|
}, TOOL_REEL_ROLL_MS);
|
||||||
pushToolReelTimeout(groupId, settleTimeout);
|
pushToolReelTimeout(groupId, settleTimeout);
|
||||||
|
|
||||||
const cleanupTimeout = window.setTimeout(() => {
|
const cleanupTimeout = window.setTimeout(
|
||||||
clearToolReelTimers(groupId);
|
() => {
|
||||||
delete toolReelStates[groupId];
|
clearToolReelTimers(groupId);
|
||||||
}, TOOL_REEL_ROLL_MS + TOOL_REEL_SETTLE_MS + 260);
|
delete toolReelStates[groupId];
|
||||||
|
},
|
||||||
|
TOOL_REEL_ROLL_MS + TOOL_REEL_SETTLE_MS + 260
|
||||||
|
);
|
||||||
pushToolReelTimeout(groupId, cleanupTimeout);
|
pushToolReelTimeout(groupId, cleanupTimeout);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -438,7 +434,11 @@ const syncToolReels = () => {
|
|||||||
const activeGroups = new Set<string>();
|
const activeGroups = new Set<string>();
|
||||||
|
|
||||||
blockGroups.value.forEach((group) => {
|
blockGroups.value.forEach((group) => {
|
||||||
if (group.type !== 'summary' || !group.actions || !shouldShowToolReel(group.actions, group.id)) {
|
if (
|
||||||
|
group.type !== 'summary' ||
|
||||||
|
!group.actions ||
|
||||||
|
!shouldShowToolReel(group.actions, group.id)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -584,22 +584,64 @@ const blockGroups = computed(() => {
|
|||||||
return groups;
|
return groups;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取摘要组的预览文本(始终显示最新的思考/工具)
|
type ToolCategory = 'read' | 'command' | 'edit' | 'search' | 'webpage' | 'other';
|
||||||
const getSummaryPreview = (actions: Action[]) => {
|
|
||||||
// 优先返回正在 streaming 的最后一个
|
const TOOL_CATEGORY_MAP: Record<string, ToolCategory> = {
|
||||||
|
// 读取文件
|
||||||
|
read_file: 'read',
|
||||||
|
read_skill: 'read',
|
||||||
|
|
||||||
|
// 运行指令
|
||||||
|
run_command: 'command',
|
||||||
|
terminal_session: 'command',
|
||||||
|
terminal_input: 'command',
|
||||||
|
terminal_snapshot: 'command',
|
||||||
|
|
||||||
|
// 编辑文件
|
||||||
|
create_file: 'edit',
|
||||||
|
write_file: 'edit',
|
||||||
|
edit_file: 'edit',
|
||||||
|
delete_file: 'edit',
|
||||||
|
rename_file: 'edit',
|
||||||
|
create_folder: 'edit',
|
||||||
|
save_webpage: 'edit',
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
web_search: 'search',
|
||||||
|
|
||||||
|
// 查看网页
|
||||||
|
extract_webpage: 'webpage'
|
||||||
|
};
|
||||||
|
|
||||||
|
const CATEGORY_LABELS: Record<ToolCategory, (count: number) => string> = {
|
||||||
|
read: (n) => `读取了 ${n} 个文件`,
|
||||||
|
command: (n) => `运行了 ${n} 个指令`,
|
||||||
|
edit: (n) => `编辑了 ${n} 次文件`,
|
||||||
|
search: (n) => `搜索了 ${n} 次`,
|
||||||
|
webpage: (n) => `查看了 ${n} 次网页`,
|
||||||
|
other: (n) => `执行了 ${n} 次其他操作`
|
||||||
|
};
|
||||||
|
|
||||||
|
const CATEGORY_ORDER: ToolCategory[] = ['read', 'command', 'edit', 'search', 'webpage', 'other'];
|
||||||
|
|
||||||
|
const getToolCategory = (action: Action): ToolCategory => {
|
||||||
|
const name = action.tool?.name;
|
||||||
|
if (typeof name !== 'string') return 'other';
|
||||||
|
return TOOL_CATEGORY_MAP[name] || 'other';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 运行中:显示当前最新步骤的意图或状态(单行,由 CSS 截断)
|
||||||
|
const getRunningSummaryText = (actions: Action[]): string => {
|
||||||
const streamingActions = actions.filter((a) => a.streaming);
|
const streamingActions = actions.filter((a) => a.streaming);
|
||||||
|
|
||||||
let currentStep;
|
let currentStep;
|
||||||
if (streamingActions.length > 0) {
|
if (streamingActions.length > 0) {
|
||||||
// 如果有 streaming 的,返回最后一个 streaming 的
|
|
||||||
currentStep = streamingActions[streamingActions.length - 1];
|
currentStep = streamingActions[streamingActions.length - 1];
|
||||||
} else {
|
} else {
|
||||||
// 如果没有 streaming 的(全部完成了),优先返回最后一个工具
|
|
||||||
const toolActions = actions.filter((a) => a.type === 'tool');
|
const toolActions = actions.filter((a) => a.type === 'tool');
|
||||||
if (toolActions.length > 0) {
|
if (toolActions.length > 0) {
|
||||||
currentStep = toolActions[toolActions.length - 1];
|
currentStep = toolActions[toolActions.length - 1];
|
||||||
} else {
|
} else {
|
||||||
// 如果没有工具,返回最后一个思考
|
|
||||||
currentStep = actions[actions.length - 1];
|
currentStep = actions[actions.length - 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -607,13 +649,11 @@ const getSummaryPreview = (actions: Action[]) => {
|
|||||||
if (!currentStep) return '';
|
if (!currentStep) return '';
|
||||||
|
|
||||||
if (currentStep.type === 'thinking') {
|
if (currentStep.type === 'thinking') {
|
||||||
// 思考内容:最多显示到第一个换行,且不超过25字符(不含...)
|
|
||||||
const content = currentStep.content || '';
|
const content = currentStep.content || '';
|
||||||
const firstLineEnd = content.indexOf('\n');
|
return getFirstLine(content);
|
||||||
const textToShow = firstLineEnd > 0 ? content.substring(0, firstLineEnd) : content;
|
}
|
||||||
return truncateSummaryPreview(textToShow);
|
|
||||||
} else if (currentStep.type === 'tool') {
|
if (currentStep.type === 'tool') {
|
||||||
// 工具:优先显示 intent_rendered 或 intent_full
|
|
||||||
const tool = currentStep.tool;
|
const tool = currentStep.tool;
|
||||||
if (!tool) return '';
|
if (!tool) return '';
|
||||||
|
|
||||||
@ -621,13 +661,9 @@ const getSummaryPreview = (actions: Action[]) => {
|
|||||||
const intentText = tool.intent_rendered || tool.intent_full || '';
|
const intentText = tool.intent_rendered || tool.intent_full || '';
|
||||||
|
|
||||||
if (intentEnabled && intentText) {
|
if (intentEnabled && intentText) {
|
||||||
// 开启intent模式且有intent时,只显示intent
|
return getFirstLine(intentText);
|
||||||
const firstLineEnd = intentText.indexOf('\n');
|
|
||||||
const textToShow = firstLineEnd > 0 ? intentText.substring(0, firstLineEnd) : intentText;
|
|
||||||
return truncateSummaryPreview(textToShow);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 没有intent或未开启intent模式时显示状态
|
|
||||||
if (tool.status === 'preparing') {
|
if (tool.status === 'preparing') {
|
||||||
return `准备调用 ${tool.name || '工具'}...`;
|
return `准备调用 ${tool.name || '工具'}...`;
|
||||||
}
|
}
|
||||||
@ -635,15 +671,64 @@ const getSummaryPreview = (actions: Action[]) => {
|
|||||||
return `正在调用 ${tool.name || '工具'}...`;
|
return `正在调用 ${tool.name || '工具'}...`;
|
||||||
}
|
}
|
||||||
if (tool.status === 'completed') {
|
if (tool.status === 'completed') {
|
||||||
return truncateSummaryPreview(tool.display_name || tool.name || '工具执行完成');
|
return tool.display_name || tool.name || '工具执行完成';
|
||||||
}
|
}
|
||||||
return currentStep.streaming
|
return currentStep.streaming ? '正在执行工具...' : tool.display_name || tool.name || '执行工具';
|
||||||
? '正在执行工具...'
|
|
||||||
: truncateSummaryPreview(tool.display_name || tool.name || '工具执行完成');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 完成后:汇总本组所有工具的执行次数
|
||||||
|
const getCompletedSummaryText = (actions: Action[]): string => {
|
||||||
|
const toolActions = actions.filter((a) => a.type === 'tool');
|
||||||
|
|
||||||
|
if (toolActions.length === 0) {
|
||||||
|
// 没有工具时回退到最后一条思考的单行内容
|
||||||
|
const lastThinking = actions.filter((a) => a.type === 'thinking').pop();
|
||||||
|
if (lastThinking) {
|
||||||
|
return getFirstLine(lastThinking.content || '');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有一个工具时,直接显示该工具的 intent/名称
|
||||||
|
if (toolActions.length === 1) {
|
||||||
|
return getToolSummaryText(toolActions[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const counts: Record<ToolCategory, number> = {
|
||||||
|
read: 0,
|
||||||
|
command: 0,
|
||||||
|
edit: 0,
|
||||||
|
search: 0,
|
||||||
|
webpage: 0,
|
||||||
|
other: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
toolActions.forEach((action) => {
|
||||||
|
counts[getToolCategory(action)]++;
|
||||||
|
});
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
CATEGORY_ORDER.forEach((category) => {
|
||||||
|
const count = counts[category];
|
||||||
|
if (count > 0) {
|
||||||
|
parts.push(CATEGORY_LABELS[category](count));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return parts.join(',');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 摘要行统一入口:运行中显示实时进度,完成后显示统计总结
|
||||||
|
const getSummaryLineText = (actions: Action[], groupId: string): string => {
|
||||||
|
if (isSummaryRunning(actions, groupId)) {
|
||||||
|
return getRunningSummaryText(actions);
|
||||||
|
}
|
||||||
|
return getCompletedSummaryText(actions);
|
||||||
|
};
|
||||||
|
|
||||||
// 判断摘要组是否正在运行(显示加载动画还是对勾)
|
// 判断摘要组是否正在运行(显示加载动画还是对勾)
|
||||||
const isSummaryRunning = (actions: Action[], groupId: string) => {
|
const isSummaryRunning = (actions: Action[], groupId: string) => {
|
||||||
if (!props.conversationRunning || !props.isLatestMessage) {
|
if (!props.conversationRunning || !props.isLatestMessage) {
|
||||||
@ -890,10 +975,11 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.summary-preview {
|
.summary-preview {
|
||||||
display: block;
|
display: block;
|
||||||
white-space: normal;
|
white-space: nowrap;
|
||||||
overflow-wrap: anywhere;
|
overflow: hidden;
|
||||||
word-break: break-word;
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
mask-image: linear-gradient(to right, black 85%, transparent 100%);
|
||||||
|
-webkit-mask-image: linear-gradient(to right, black 85%, transparent 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-tool-reel-window {
|
.summary-tool-reel-window {
|
||||||
@ -1090,8 +1176,7 @@ onBeforeUnmount(() => {
|
|||||||
margin-top: calc((1em - 1.7em) / 2);
|
margin-top: calc((1em - 1.7em) / 2);
|
||||||
/* 滚动条样式(参考 git 状态侧边栏文件列表,滚动槽透明贴合灰色底色) */
|
/* 滚动条样式(参考 git 状态侧边栏文件列表,滚动槽透明贴合灰色底色) */
|
||||||
scrollbar-width: thin; /* Firefox */
|
scrollbar-width: thin; /* Firefox */
|
||||||
scrollbar-color: color-mix(in srgb, var(--claude-text-secondary) 55%, transparent)
|
scrollbar-color: color-mix(in srgb, var(--claude-text-secondary) 55%, transparent) transparent;
|
||||||
transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.thinking-content::-webkit-scrollbar {
|
.thinking-content::-webkit-scrollbar {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user