feat: polish minimal summary loader interactions

This commit is contained in:
JOJO 2026-04-04 20:52:50 +08:00
parent 0b923d6c53
commit 0c213b0a9f
6 changed files with 96 additions and 45 deletions

View File

@ -52,6 +52,8 @@
<template v-if="blockDisplayMode === 'minimal'"> <template v-if="blockDisplayMode === 'minimal'">
<MinimalBlocks <MinimalBlocks
:actions="msg.actions || []" :actions="msg.actions || []"
:conversation-running="streamingMessage"
:is-latest-message="index === latestMessageIndex"
:icon-style="iconStyleSafe" :icon-style="iconStyleSafe"
:get-tool-icon="getToolIcon" :get-tool-icon="getToolIcon"
:get-tool-status-text="getToolStatusText" :get-tool-status-text="getToolStatusText"
@ -423,6 +425,7 @@ const userName = computed(() => {
const filteredMessages = computed(() => const filteredMessages = computed(() =>
(props.messages || []).filter(m => !(m && m.metadata && m.metadata.system_injected_image) && m.role !== 'system') (props.messages || []).filter(m => !(m && m.metadata && m.metadata.system_injected_image) && m.role !== 'system')
); );
const latestMessageIndex = computed(() => filteredMessages.value.length - 1);
const DEFAULT_GENERATING_TEXT = '生成中…'; const DEFAULT_GENERATING_TEXT = '生成中…';
const rootEl = ref<HTMLElement | null>(null); const rootEl = ref<HTMLElement | null>(null);

View File

@ -113,6 +113,8 @@ interface BlockGroup {
const props = defineProps<{ const props = defineProps<{
actions: Action[]; actions: Action[];
conversationRunning?: boolean;
isLatestMessage?: boolean;
iconStyle?: (name: string) => any; iconStyle?: (name: string) => any;
getToolIcon?: (tool: any) => string; getToolIcon?: (tool: any) => string;
getToolStatusText?: (tool: any) => string; getToolStatusText?: (tool: any) => string;
@ -129,6 +131,15 @@ 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 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;
};
// action // action
const getSummaryLoader = (groupId: string) => { const getSummaryLoader = (groupId: string) => {
@ -237,11 +248,11 @@ const getSummaryPreview = (actions: Action[]) => {
if (!currentStep) return ''; if (!currentStep) return '';
if (currentStep.type === 'thinking') { if (currentStep.type === 'thinking') {
// 50 // 25...
const content = currentStep.content || ''; const content = currentStep.content || '';
const firstLineEnd = content.indexOf('\n'); const firstLineEnd = content.indexOf('\n');
const textToShow = firstLineEnd > 0 ? content.substring(0, firstLineEnd) : content; const textToShow = firstLineEnd > 0 ? content.substring(0, firstLineEnd) : content;
return textToShow.length > 50 ? textToShow.substring(0, 50) + '...' : textToShow; return truncateSummaryPreview(textToShow);
} else if (currentStep.type === 'tool') { } else if (currentStep.type === 'tool') {
// intent_rendered intent_full // intent_rendered intent_full
const tool = currentStep.tool; const tool = currentStep.tool;
@ -254,7 +265,7 @@ const getSummaryPreview = (actions: Action[]) => {
// intentintentintent // intentintentintent
const firstLineEnd = intentText.indexOf('\n'); const firstLineEnd = intentText.indexOf('\n');
const textToShow = firstLineEnd > 0 ? intentText.substring(0, firstLineEnd) : intentText; const textToShow = firstLineEnd > 0 ? intentText.substring(0, firstLineEnd) : intentText;
return textToShow.length > 50 ? textToShow.substring(0, 50) + '...' : textToShow; return truncateSummaryPreview(textToShow);
} }
// intentintent // intentintent
@ -274,6 +285,10 @@ const getSummaryPreview = (actions: Action[]) => {
// //
const isSummaryRunning = (actions: Action[], groupId: string) => { const isSummaryRunning = (actions: Action[], groupId: string) => {
if (!props.conversationRunning || !props.isLatestMessage) {
return false;
}
// group blockGroups // group blockGroups
const currentIndex = blockGroups.value.findIndex(g => g.id === groupId); const currentIndex = blockGroups.value.findIndex(g => g.id === groupId);
if (currentIndex === -1) return false; if (currentIndex === -1) return false;
@ -465,10 +480,11 @@ watch(() => props.actions, () => {
/* 摘要行(只有文字) */ /* 摘要行(只有文字) */
.summary-line-text { .summary-line-text {
display: flex; display: grid;
align-items: center; grid-template-columns: minmax(0, 1fr) auto;
justify-content: space-between; align-items: start;
padding: 0; column-gap: 8px;
padding: 0 20px 0 15px;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: background-color 0.2s;
} }
@ -478,24 +494,26 @@ watch(() => props.actions, () => {
} }
.summary-content-wrapper { .summary-content-wrapper {
flex: 1; min-width: 0;
font-size: 15px; font-size: 15px;
color: var(--claude-text-secondary); color: var(--claude-text-secondary);
line-height: 1.7; line-height: 1.7;
padding: 0 20px 0 15px; padding: 0;
} }
.summary-preview { .summary-preview {
display: block; display: block;
overflow: hidden; white-space: normal;
text-overflow: ellipsis; overflow-wrap: anywhere;
white-space: nowrap; word-break: break-word;
color: inherit; color: inherit;
} }
.summary-status-icon { .summary-status-icon {
flex-shrink: 0; flex-shrink: 0;
margin-left: 8px; margin-left: 0;
align-self: start;
margin-top: calc((1.7em - 18px) / 2);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@ -12,46 +12,43 @@
<style scoped> <style scoped>
.bouncing-squares-loader { .bouncing-squares-loader {
width: 40px; --square-size: 3px;
--square-gap: 10px;
display: grid;
grid-template-columns: repeat(3, var(--square-size));
column-gap: var(--square-gap);
align-items: end;
width: calc(3 * var(--square-size) + 2 * var(--square-gap));
height: 20px; height: 20px;
position: relative;
} }
.square { .square {
width: 3px; width: var(--square-size);
height: 3px; height: var(--square-size);
position: absolute;
background-color: var(--claude-text-tertiary); background-color: var(--claude-text-tertiary);
left: 15%; transform-origin: center bottom;
transform-origin: 50%;
animation: bounce-square .5s alternate infinite ease; animation: bounce-square .5s alternate infinite ease;
} }
@keyframes bounce-square { @keyframes bounce-square {
0% { 0% {
top: 20px; transform: translateY(0) scaleX(1.7) scaleY(0.7);
height: 2px;
transform: scaleX(1.7);
} }
40% { 40% {
height: 3px; transform: translateY(-8px) scaleX(1) scaleY(1);
transform: scaleX(1);
} }
100% { 100% {
top: 0%; transform: translateY(-17px) scaleX(1) scaleY(1);
} }
} }
.square:nth-child(2) { .square:nth-child(2) {
left: 45%;
animation-delay: .2s; animation-delay: .2s;
} }
.square:nth-child(3) { .square:nth-child(3) {
left: auto;
right: 15%;
animation-delay: .3s; animation-delay: .3s;
} }
</style> </style>

View File

@ -19,20 +19,19 @@
<style scoped> <style scoped>
.ripple-loader { .ripple-loader {
--cell-size: 3px; --cell-size: 3px;
--cell-spacing: 2px; --cell-gap: 4px;
--cells: 3; display: grid;
--total-size: calc(var(--cells) * (var(--cell-size) + 2 * var(--cell-spacing))); grid-template-columns: repeat(3, var(--cell-size));
display: flex; grid-template-rows: repeat(3, var(--cell-size));
flex-wrap: wrap; gap: var(--cell-gap);
width: var(--total-size); width: calc(3 * var(--cell-size) + 2 * var(--cell-gap));
height: var(--total-size); height: calc(3 * var(--cell-size) + 2 * var(--cell-gap));
} }
.cell { .cell {
flex: 0 0 var(--cell-size); width: var(--cell-size);
margin: var(--cell-spacing); height: var(--cell-size);
background-color: transparent; background-color: transparent;
box-sizing: border-box;
animation: 1.5s ripple ease infinite; animation: 1.5s ripple ease infinite;
} }

View File

@ -18,8 +18,42 @@ export const loaderPool: Component[] = [
RippleLoader RippleLoader
]; ];
// 随机获取一个加载动画 // 至少间隔多少次后才允许重复(例如 4 表示最近 4 次不重复)
const MIN_NON_REPEAT_WINDOW = 4;
const recentPickedIndices: number[] = [];
const pickRandomIndex = (indices: number[]) => {
const randomIndex = Math.floor(Math.random() * indices.length);
return indices[randomIndex];
};
// 随机获取一个加载动画(带“近期不重复”约束)
export function getRandomLoader(): Component { export function getRandomLoader(): Component {
const randomIndex = Math.floor(Math.random() * loaderPool.length); const poolSize = loaderPool.length;
return loaderPool[randomIndex]; if (poolSize === 0) {
throw new Error('loaderPool 不能为空');
}
// 不重复窗口不能超过 poolSize - 1否则无解
const nonRepeatWindow = Math.max(0, Math.min(MIN_NON_REPEAT_WINDOW, poolSize - 1));
let candidateIndices = Array.from({ length: poolSize }, (_, i) => i);
if (nonRepeatWindow > 0 && recentPickedIndices.length > 0) {
const blocked = new Set(recentPickedIndices.slice(-nonRepeatWindow));
const filtered = candidateIndices.filter((idx) => !blocked.has(idx));
if (filtered.length > 0) {
candidateIndices = filtered;
}
}
const pickedIndex = pickRandomIndex(candidateIndices);
recentPickedIndices.push(pickedIndex);
// 控制历史队列长度,避免无限增长
const maxHistory = Math.max(poolSize * 2, nonRepeatWindow + 1);
if (recentPickedIndices.length > maxHistory) {
recentPickedIndices.splice(0, recentPickedIndices.length - maxHistory);
}
return loaderPool[pickedIndex];
} }

View File

@ -112,8 +112,8 @@
} }
.scroll-lock-toggle { .scroll-lock-toggle {
position: absolute; position: fixed;
right: 28px; right: 8px;
bottom: 140px; bottom: 140px;
z-index: 25; z-index: 25;
display: flex; display: flex;
@ -126,7 +126,7 @@
} }
.chat-container--immersive .scroll-lock-toggle { .chat-container--immersive .scroll-lock-toggle {
right: calc((100% - min(900px, 100%)) / 2 + 24px); right: 8px;
} }
.chat-container--mobile { .chat-container--mobile {