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'">
<MinimalBlocks
:actions="msg.actions || []"
:conversation-running="streamingMessage"
:is-latest-message="index === latestMessageIndex"
:icon-style="iconStyleSafe"
:get-tool-icon="getToolIcon"
:get-tool-status-text="getToolStatusText"
@ -423,6 +425,7 @@ const userName = computed(() => {
const filteredMessages = computed(() =>
(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 rootEl = ref<HTMLElement | null>(null);

View File

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

View File

@ -12,46 +12,43 @@
<style scoped>
.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;
position: relative;
}
.square {
width: 3px;
height: 3px;
position: absolute;
width: var(--square-size);
height: var(--square-size);
background-color: var(--claude-text-tertiary);
left: 15%;
transform-origin: 50%;
transform-origin: center bottom;
animation: bounce-square .5s alternate infinite ease;
}
@keyframes bounce-square {
0% {
top: 20px;
height: 2px;
transform: scaleX(1.7);
transform: translateY(0) scaleX(1.7) scaleY(0.7);
}
40% {
height: 3px;
transform: scaleX(1);
transform: translateY(-8px) scaleX(1) scaleY(1);
}
100% {
top: 0%;
transform: translateY(-17px) scaleX(1) scaleY(1);
}
}
.square:nth-child(2) {
left: 45%;
animation-delay: .2s;
}
.square:nth-child(3) {
left: auto;
right: 15%;
animation-delay: .3s;
}
</style>

View File

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

View File

@ -18,8 +18,42 @@ export const loaderPool: Component[] = [
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 {
const randomIndex = Math.floor(Math.random() * loaderPool.length);
return loaderPool[randomIndex];
const poolSize = loaderPool.length;
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 {
position: absolute;
right: 28px;
position: fixed;
right: 8px;
bottom: 140px;
z-index: 25;
display: flex;
@ -126,7 +126,7 @@
}
.chat-container--immersive .scroll-lock-toggle {
right: calc((100% - min(900px, 100%)) / 2 + 24px);
right: 8px;
}
.chat-container--mobile {