feat: polish minimal summary loader interactions
This commit is contained in:
parent
0b923d6c53
commit
0c213b0a9f
@ -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);
|
||||||
|
|||||||
@ -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[]) => {
|
|||||||
// 开启intent模式且有intent时,只显示intent
|
// 开启intent模式且有intent时,只显示intent
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 没有intent或未开启intent模式时显示状态
|
// 没有intent或未开启intent模式时显示状态
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user