feat(chat): add randomized summary loaders in minimal mode
This commit is contained in:
parent
8d5a034c70
commit
3b710020fe
@ -1,12 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="minimal-blocks-container">
|
<div class="minimal-blocks-container">
|
||||||
<!-- 等待动画 -->
|
|
||||||
<div v-if="showGenerating" class="generating-placeholder">
|
|
||||||
<span v-for="(letter, letterIdx) in '思考中...'.split('')" :key="letterIdx" class="letter" :style="{ animationDelay: `${letterIdx * 0.1}s` }">
|
|
||||||
{{ letter }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 按真实顺序显示分组 -->
|
<!-- 按真实顺序显示分组 -->
|
||||||
<template v-for="group in blockGroups" :key="group.id">
|
<template v-for="group in blockGroups" :key="group.id">
|
||||||
<!-- 摘要组(thinking/tool) -->
|
<!-- 摘要组(thinking/tool) -->
|
||||||
@ -20,9 +13,14 @@
|
|||||||
<div class="summary-content-wrapper">
|
<div class="summary-content-wrapper">
|
||||||
<span class="summary-preview">{{ getSummaryPreview(group.actions) }}</span>
|
<span class="summary-preview">{{ getSummaryPreview(group.actions) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<svg class="expand-icon" :class="{ expanded: expandedGroups.has(group.id) }" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<!-- 加载动画或完成图标 -->
|
||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
<div class="summary-status-icon">
|
||||||
</svg>
|
<component v-if="isSummaryRunning(group.actions, group.id)" :is="getSummaryLoader(group.id)" />
|
||||||
|
<svg v-else class="check-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<polyline points="7.5 12 10.5 15 16.5 9" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 步骤容器(展开时显示所有步骤) -->
|
<!-- 步骤容器(展开时显示所有步骤) -->
|
||||||
@ -78,9 +76,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, nextTick } from 'vue';
|
import { ref, computed, watch, nextTick, Component } from 'vue';
|
||||||
import { usePersonalizationStore } from '@/stores/personalization';
|
import { usePersonalizationStore } from '@/stores/personalization';
|
||||||
import { renderEnhancedToolResult } from './actions/toolRenderers';
|
import { renderEnhancedToolResult } from './actions/toolRenderers';
|
||||||
|
import { getRandomLoader } from './loaders/index';
|
||||||
|
|
||||||
interface Action {
|
interface Action {
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -128,6 +127,46 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const personalizationStore = usePersonalizationStore();
|
const personalizationStore = usePersonalizationStore();
|
||||||
const expandedGroups = ref(new Set<string>());
|
const expandedGroups = ref(new Set<string>());
|
||||||
|
const thinkingRefs = new Map<string, HTMLElement>();
|
||||||
|
const summaryLoaders = new Map<string, Component>();
|
||||||
|
|
||||||
|
// 获取摘要组的加载动画组件(每次action类型切换时随机一次)
|
||||||
|
const getSummaryLoader = (groupId: string) => {
|
||||||
|
const group = blockGroups.value.find(g => g.id === groupId);
|
||||||
|
if (!group || !group.actions) return getRandomLoader();
|
||||||
|
|
||||||
|
// 找到当前正在streaming的action,或最后一个action
|
||||||
|
const streamingAction = group.actions.find(a => a.streaming);
|
||||||
|
const currentAction = streamingAction || group.actions[group.actions.length - 1];
|
||||||
|
|
||||||
|
if (!currentAction) return getRandomLoader();
|
||||||
|
|
||||||
|
// 找到当前action之前最近的一个不同类型的action
|
||||||
|
// 用于判断是否发生了类型切换(thinking <-> tool)
|
||||||
|
const currentIndex = group.actions.indexOf(currentAction);
|
||||||
|
let lastDifferentTypeIndex = -1;
|
||||||
|
|
||||||
|
for (let i = currentIndex - 1; i >= 0; i--) {
|
||||||
|
if (group.actions[i].type !== currentAction.type) {
|
||||||
|
lastDifferentTypeIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用"类型切换点"作为key
|
||||||
|
// 如果是第一个action,或者类型发生了切换,就会生成新的key
|
||||||
|
// 如果是相同类型的连续action(如多个工具并行),使用相同的key
|
||||||
|
const typeSegmentKey = lastDifferentTypeIndex >= 0
|
||||||
|
? `${group.actions[lastDifferentTypeIndex].id}-${currentAction.type}`
|
||||||
|
: `start-${currentAction.type}`;
|
||||||
|
|
||||||
|
const cacheKey = `${groupId}-${typeSegmentKey}`;
|
||||||
|
|
||||||
|
if (!summaryLoaders.has(cacheKey)) {
|
||||||
|
summaryLoaders.set(cacheKey, getRandomLoader());
|
||||||
|
}
|
||||||
|
return summaryLoaders.get(cacheKey);
|
||||||
|
};
|
||||||
|
|
||||||
// 将 actions 分组:连续的 thinking/tool 为一组,text 单独为一组
|
// 将 actions 分组:连续的 thinking/tool 为一组,text 单独为一组
|
||||||
const blockGroups = computed(() => {
|
const blockGroups = computed(() => {
|
||||||
@ -141,11 +180,14 @@ const blockGroups = computed(() => {
|
|||||||
} else if (action.type === 'text') {
|
} else if (action.type === 'text') {
|
||||||
// 先保存当前的 thinking/tool 组
|
// 先保存当前的 thinking/tool 组
|
||||||
if (currentGroup.length > 0) {
|
if (currentGroup.length > 0) {
|
||||||
|
// 使用第一个 action 的 id 作为 group id,保证稳定性
|
||||||
|
const firstActionId = currentGroup[0].id || currentGroup[0].blockId || `summary-${groupIndex}`;
|
||||||
groups.push({
|
groups.push({
|
||||||
type: 'summary',
|
type: 'summary',
|
||||||
id: `summary-${groupIndex++}`,
|
id: `summary-${firstActionId}`,
|
||||||
actions: currentGroup
|
actions: currentGroup
|
||||||
});
|
});
|
||||||
|
groupIndex++;
|
||||||
currentGroup = [];
|
currentGroup = [];
|
||||||
}
|
}
|
||||||
// 添加 text 组
|
// 添加 text 组
|
||||||
@ -160,9 +202,11 @@ const blockGroups = computed(() => {
|
|||||||
|
|
||||||
// 处理最后剩余的 thinking/tool 组
|
// 处理最后剩余的 thinking/tool 组
|
||||||
if (currentGroup.length > 0) {
|
if (currentGroup.length > 0) {
|
||||||
|
// 使用第一个 action 的 id 作为 group id,保证稳定性
|
||||||
|
const firstActionId = currentGroup[0].id || currentGroup[0].blockId || `summary-${groupIndex}`;
|
||||||
groups.push({
|
groups.push({
|
||||||
type: 'summary',
|
type: 'summary',
|
||||||
id: `summary-${groupIndex}`,
|
id: `summary-${firstActionId}`,
|
||||||
actions: currentGroup
|
actions: currentGroup
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -170,20 +214,25 @@ const blockGroups = computed(() => {
|
|||||||
return groups;
|
return groups;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 是否显示等待动画
|
|
||||||
const showGenerating = computed(() => {
|
|
||||||
const hasAnyThinkingOrTool = props.actions.some(a => a.type === 'thinking' || a.type === 'tool');
|
|
||||||
const isStreaming = props.actions.some(a => a.streaming && (a.type === 'thinking' || a.type === 'tool'));
|
|
||||||
return isStreaming && !hasAnyThinkingOrTool;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取摘要组的预览文本(始终显示最新的思考/工具)
|
// 获取摘要组的预览文本(始终显示最新的思考/工具)
|
||||||
const getSummaryPreview = (actions: Action[]) => {
|
const getSummaryPreview = (actions: Action[]) => {
|
||||||
// 优先返回正在 streaming 的最后一个
|
// 优先返回正在 streaming 的最后一个
|
||||||
const streamingActions = actions.filter(a => a.streaming);
|
const streamingActions = actions.filter(a => a.streaming);
|
||||||
const currentStep = streamingActions.length > 0
|
|
||||||
? streamingActions[streamingActions.length - 1]
|
let currentStep;
|
||||||
: actions[actions.length - 1]; // 如果没有 streaming 的,返回最后一个
|
if (streamingActions.length > 0) {
|
||||||
|
// 如果有 streaming 的,返回最后一个 streaming 的
|
||||||
|
currentStep = streamingActions[streamingActions.length - 1];
|
||||||
|
} else {
|
||||||
|
// 如果没有 streaming 的(全部完成了),优先返回最后一个工具
|
||||||
|
const toolActions = actions.filter(a => a.type === 'tool');
|
||||||
|
if (toolActions.length > 0) {
|
||||||
|
currentStep = toolActions[toolActions.length - 1];
|
||||||
|
} else {
|
||||||
|
// 如果没有工具,返回最后一个思考
|
||||||
|
currentStep = actions[actions.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentStep) return '';
|
if (!currentStep) return '';
|
||||||
|
|
||||||
@ -223,7 +272,7 @@ const getSummaryPreview = (actions: Action[]) => {
|
|||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 判断摘要组是否正在运行(包括已完成但后面还没有文本输出的情况)
|
// 判断摘要组是否正在运行(显示加载动画还是对勾)
|
||||||
const isSummaryRunning = (actions: Action[], groupId: string) => {
|
const isSummaryRunning = (actions: Action[], groupId: string) => {
|
||||||
// 找到当前 group 在 blockGroups 中的索引
|
// 找到当前 group 在 blockGroups 中的索引
|
||||||
const currentIndex = blockGroups.value.findIndex(g => g.id === groupId);
|
const currentIndex = blockGroups.value.findIndex(g => g.id === groupId);
|
||||||
@ -232,11 +281,9 @@ const isSummaryRunning = (actions: Action[], groupId: string) => {
|
|||||||
// 检查后面是否有文本输出
|
// 检查后面是否有文本输出
|
||||||
const hasTextAfter = blockGroups.value.slice(currentIndex + 1).some(g => g.type === 'text');
|
const hasTextAfter = blockGroups.value.slice(currentIndex + 1).some(g => g.type === 'text');
|
||||||
|
|
||||||
// 如果后面有文本输出,说明这个 summary group 已经完成了,不再显示变色效果
|
// 只有当后面有文本输出时,才显示对勾(说明这个摘要组已经完成并开始输出了)
|
||||||
if (hasTextAfter) return false;
|
// 否则一直显示加载动画(即使工具执行完了,也要等下一个步骤或文本输出)
|
||||||
|
return !hasTextAfter;
|
||||||
// 否则,这个 summary group 应该有变色效果(无论是运行中还是已完成)
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取摘要组的所有步骤(用于展开显示)
|
// 获取摘要组的所有步骤(用于展开显示)
|
||||||
@ -253,8 +300,45 @@ const getSummarySteps = (actions: Action[]) => {
|
|||||||
const toggleExpand = (groupId: string) => {
|
const toggleExpand = (groupId: string) => {
|
||||||
if (expandedGroups.value.has(groupId)) {
|
if (expandedGroups.value.has(groupId)) {
|
||||||
expandedGroups.value.delete(groupId);
|
expandedGroups.value.delete(groupId);
|
||||||
|
|
||||||
|
// 折叠后,取消注册该组中的思考内容 ref
|
||||||
|
nextTick(() => {
|
||||||
|
const group = blockGroups.value.find(g => g.id === groupId);
|
||||||
|
if (group && group.actions) {
|
||||||
|
group.actions.forEach(action => {
|
||||||
|
if (action.type === 'thinking') {
|
||||||
|
const blockId = action.id || action.blockId;
|
||||||
|
if (blockId && typeof props.registerThinkingRef === 'function') {
|
||||||
|
props.registerThinkingRef(blockId, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
expandedGroups.value.add(groupId);
|
expandedGroups.value.add(groupId);
|
||||||
|
|
||||||
|
// 展开后,注册该组中的思考内容 ref
|
||||||
|
nextTick(() => {
|
||||||
|
const group = blockGroups.value.find(g => g.id === groupId);
|
||||||
|
if (group && group.actions) {
|
||||||
|
group.actions.forEach(action => {
|
||||||
|
if (action.type === 'thinking') {
|
||||||
|
const blockId = action.id || action.blockId;
|
||||||
|
if (blockId) {
|
||||||
|
const el = thinkingRefs.get(blockId);
|
||||||
|
if (el && typeof props.registerThinkingRef === 'function') {
|
||||||
|
props.registerThinkingRef(blockId, el);
|
||||||
|
// 如果正在 streaming,滚动到底部
|
||||||
|
if (action.streaming) {
|
||||||
|
el.scrollTop = el.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -338,8 +422,23 @@ const escapeHtml = (text: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const registerThinking = (key: string, el: Element | null) => {
|
const registerThinking = (key: string, el: Element | null) => {
|
||||||
if (typeof props.registerThinkingRef === 'function') {
|
// 本地保存一份,用于展开/折叠时管理
|
||||||
props.registerThinkingRef(key, el);
|
if (el instanceof HTMLElement) {
|
||||||
|
thinkingRefs.set(key, el);
|
||||||
|
} else {
|
||||||
|
thinkingRefs.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查当前思考内容所在的摘要是否展开
|
||||||
|
const group = blockGroups.value.find(g =>
|
||||||
|
g.type === 'summary' && g.actions && g.actions.some(a => (a.id === key || a.blockId === key))
|
||||||
|
);
|
||||||
|
|
||||||
|
// 只有展开的摘要中的思考内容才注册到父组件
|
||||||
|
if (group && expandedGroups.value.has(group.id)) {
|
||||||
|
if (typeof props.registerThinkingRef === 'function') {
|
||||||
|
props.registerThinkingRef(key, el);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -359,33 +458,6 @@ watch(() => props.actions, () => {
|
|||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 等待动画 */
|
|
||||||
.generating-placeholder {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 12px 0;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--claude-text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.generating-placeholder .letter {
|
|
||||||
display: inline-block;
|
|
||||||
animation: wave 1.6s ease-in-out infinite;
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes wave {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 0.4;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(-3px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 摘要组 */
|
/* 摘要组 */
|
||||||
.summary-group {
|
.summary-group {
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
@ -421,15 +493,20 @@ watch(() => props.actions, () => {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-icon {
|
.summary-status-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
color: var(--claude-text-tertiary);
|
display: flex;
|
||||||
transition: transform 0.2s;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-icon.expanded {
|
.check-icon {
|
||||||
transform: rotate(180deg);
|
color: var(--claude-text-tertiary);
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 运行中的发光效果 */
|
/* 运行中的发光效果 */
|
||||||
|
|||||||
57
static/src/components/chat/loaders/BouncingSquaresLoader.vue
Normal file
57
static/src/components/chat/loaders/BouncingSquaresLoader.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bouncing-squares-loader">
|
||||||
|
<div class="square"></div>
|
||||||
|
<div class="square"></div>
|
||||||
|
<div class="square"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// From Uiverse.io by mobinkakei (modified)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bouncing-squares-loader {
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square {
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--claude-text-tertiary);
|
||||||
|
left: 15%;
|
||||||
|
transform-origin: 50%;
|
||||||
|
animation: bounce-square .5s alternate infinite ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce-square {
|
||||||
|
0% {
|
||||||
|
top: 20px;
|
||||||
|
height: 2px;
|
||||||
|
transform: scaleX(1.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
height: 3px;
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
top: 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.square:nth-child(2) {
|
||||||
|
left: 45%;
|
||||||
|
animation-delay: .2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square:nth-child(3) {
|
||||||
|
left: auto;
|
||||||
|
right: 15%;
|
||||||
|
animation-delay: .3s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
71
static/src/components/chat/loaders/DualChaseLoader.vue
Normal file
71
static/src/components/chat/loaders/DualChaseLoader.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dual-chase-loader"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// From Uiverse.io by satyamchaudharydev (modified)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dual-chase-loader {
|
||||||
|
--size: 6px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dual-chase-loader::after,
|
||||||
|
.dual-chase-loader::before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
top: 50%;
|
||||||
|
animation: chase-up 2.4s cubic-bezier(0, 0, 0.24, 1.21) infinite;
|
||||||
|
left: 50%;
|
||||||
|
background: var(--claude-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dual-chase-loader::after {
|
||||||
|
top: calc(50% - var(--size));
|
||||||
|
left: calc(50% - var(--size));
|
||||||
|
animation: chase-down 2.4s cubic-bezier(0, 0, 0.24, 1.21) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes chase-down {
|
||||||
|
0%, 100% {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateX(100%) translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes chase-up {
|
||||||
|
0%, 100% {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateX(-100%) translateY(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
107
static/src/components/chat/loaders/FiveSquaresLoader.vue
Normal file
107
static/src/components/chat/loaders/FiveSquaresLoader.vue
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<div class="five-squares-loader">
|
||||||
|
<div id="sq1"></div>
|
||||||
|
<div id="sq2"></div>
|
||||||
|
<div id="sq3"></div>
|
||||||
|
<div id="sq4"></div>
|
||||||
|
<div id="sq5"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// From Uiverse.io by Nawsome (modified)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.five-squares-loader {
|
||||||
|
--square: 3px;
|
||||||
|
--offset: 7.5px;
|
||||||
|
width: calc(3 * var(--offset) + var(--square));
|
||||||
|
height: calc(2 * var(--offset) + var(--square));
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.five-squares-loader div {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--claude-text-tertiary);
|
||||||
|
width: var(--square);
|
||||||
|
height: var(--square);
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.five-squares-loader #sq1 {
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
animation: square1 2.4s 0.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.five-squares-loader #sq2 {
|
||||||
|
left: 0;
|
||||||
|
top: var(--offset);
|
||||||
|
animation: square2 2.4s 0.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.five-squares-loader #sq3 {
|
||||||
|
left: var(--offset);
|
||||||
|
top: var(--offset);
|
||||||
|
animation: square3 2.4s 0.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.five-squares-loader #sq4 {
|
||||||
|
left: calc(2 * var(--offset));
|
||||||
|
top: var(--offset);
|
||||||
|
animation: square4 2.4s 0.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.five-squares-loader #sq5 {
|
||||||
|
left: calc(3 * var(--offset));
|
||||||
|
top: var(--offset);
|
||||||
|
animation: square5 2.4s 0.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes square1 {
|
||||||
|
0% { left: 0; top: 0; }
|
||||||
|
8.33% { left: 0; top: var(--offset); }
|
||||||
|
100% { left: 0; top: var(--offset); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes square2 {
|
||||||
|
0% { left: 0; top: var(--offset); }
|
||||||
|
8.33% { left: 0; top: calc(2 * var(--offset)); }
|
||||||
|
16.67% { left: var(--offset); top: calc(2 * var(--offset)); }
|
||||||
|
25% { left: var(--offset); top: var(--offset); }
|
||||||
|
83.33% { left: var(--offset); top: var(--offset); }
|
||||||
|
91.67% { left: var(--offset); top: 0; }
|
||||||
|
100% { left: 0; top: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes square3 {
|
||||||
|
0%, 100% { left: var(--offset); top: var(--offset); }
|
||||||
|
16.67% { left: var(--offset); top: var(--offset); }
|
||||||
|
25% { left: var(--offset); top: 0; }
|
||||||
|
33.33% { left: calc(2 * var(--offset)); top: 0; }
|
||||||
|
41.67% { left: calc(2 * var(--offset)); top: var(--offset); }
|
||||||
|
66.67% { left: calc(2 * var(--offset)); top: var(--offset); }
|
||||||
|
75% { left: calc(2 * var(--offset)); top: calc(2 * var(--offset)); }
|
||||||
|
83.33% { left: var(--offset); top: calc(2 * var(--offset)); }
|
||||||
|
91.67% { left: var(--offset); top: var(--offset); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes square4 {
|
||||||
|
0% { left: calc(2 * var(--offset)); top: var(--offset); }
|
||||||
|
33.33% { left: calc(2 * var(--offset)); top: var(--offset); }
|
||||||
|
41.67% { left: calc(2 * var(--offset)); top: calc(2 * var(--offset)); }
|
||||||
|
50% { left: calc(3 * var(--offset)); top: calc(2 * var(--offset)); }
|
||||||
|
58.33% { left: calc(3 * var(--offset)); top: var(--offset); }
|
||||||
|
100% { left: calc(3 * var(--offset)); top: var(--offset); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes square5 {
|
||||||
|
0% { left: calc(3 * var(--offset)); top: var(--offset); }
|
||||||
|
50% { left: calc(3 * var(--offset)); top: var(--offset); }
|
||||||
|
58.33% { left: calc(3 * var(--offset)); top: 0; }
|
||||||
|
66.67% { left: calc(2 * var(--offset)); top: 0; }
|
||||||
|
75% { left: calc(2 * var(--offset)); top: var(--offset); }
|
||||||
|
100% { left: calc(2 * var(--offset)); top: var(--offset); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
197
static/src/components/chat/loaders/NineGridLoader.vue
Normal file
197
static/src/components/chat/loaders/NineGridLoader.vue
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
<template>
|
||||||
|
<div class="nine-grid-loader">
|
||||||
|
<div class="box"></div>
|
||||||
|
<div class="box"></div>
|
||||||
|
<div class="box"></div>
|
||||||
|
<div class="box"></div>
|
||||||
|
<div class="box"></div>
|
||||||
|
<div class="box"></div>
|
||||||
|
<div class="box"></div>
|
||||||
|
<div class="box"></div>
|
||||||
|
<div class="box"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// From Uiverse.io by Nawsome (modified)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.nine-grid-loader {
|
||||||
|
position: relative;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--claude-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box:nth-child(3n) {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box:nth-child(1):before,
|
||||||
|
.box:nth-child(4):before {
|
||||||
|
margin-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box:nth-child(3):before {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box:nth-child(1) { animation: moveBox-1 4s infinite; }
|
||||||
|
.box:nth-child(2) { animation: moveBox-2 4s infinite; }
|
||||||
|
.box:nth-child(3) { animation: moveBox-3 4s infinite; }
|
||||||
|
.box:nth-child(4) { animation: moveBox-4 4s infinite; }
|
||||||
|
.box:nth-child(5) { animation: moveBox-5 4s infinite; }
|
||||||
|
.box:nth-child(6) { animation: moveBox-6 4s infinite; }
|
||||||
|
.box:nth-child(7) { animation: moveBox-7 4s infinite; }
|
||||||
|
.box:nth-child(8) { animation: moveBox-8 4s infinite; }
|
||||||
|
.box:nth-child(9) { animation: moveBox-9 4s infinite; }
|
||||||
|
|
||||||
|
@keyframes moveBox-1 {
|
||||||
|
9% { transform: translate(-7px, 0); }
|
||||||
|
18% { transform: translate(0, 0); }
|
||||||
|
27% { transform: translate(0, 0); }
|
||||||
|
36% { transform: translate(7px, 0); }
|
||||||
|
45% { transform: translate(7px, 7px); }
|
||||||
|
54% { transform: translate(7px, 7px); }
|
||||||
|
63% { transform: translate(7px, 7px); }
|
||||||
|
72% { transform: translate(7px, 0); }
|
||||||
|
81% { transform: translate(0, 0); }
|
||||||
|
90% { transform: translate(-7px, 0); }
|
||||||
|
100% { transform: translate(0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes moveBox-2 {
|
||||||
|
9% { transform: translate(0, 0); }
|
||||||
|
18% { transform: translate(7px, 0); }
|
||||||
|
27% { transform: translate(0, 0); }
|
||||||
|
36% { transform: translate(7px, 0); }
|
||||||
|
45% { transform: translate(7px, 7px); }
|
||||||
|
54% { transform: translate(7px, 7px); }
|
||||||
|
63% { transform: translate(7px, 7px); }
|
||||||
|
72% { transform: translate(7px, 7px); }
|
||||||
|
81% { transform: translate(0, 7px); }
|
||||||
|
90% { transform: translate(0, 7px); }
|
||||||
|
100% { transform: translate(0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes moveBox-3 {
|
||||||
|
9% { transform: translate(-7px, 0); }
|
||||||
|
18% { transform: translate(-7px, 0); }
|
||||||
|
27% { transform: translate(0, 0); }
|
||||||
|
36% { transform: translate(-7px, 0); }
|
||||||
|
45% { transform: translate(-7px, 0); }
|
||||||
|
54% { transform: translate(-7px, 0); }
|
||||||
|
63% { transform: translate(-7px, 0); }
|
||||||
|
72% { transform: translate(-7px, 0); }
|
||||||
|
81% { transform: translate(-7px, -7px); }
|
||||||
|
90% { transform: translate(0, -7px); }
|
||||||
|
100% { transform: translate(0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes moveBox-4 {
|
||||||
|
9% { transform: translate(-7px, 0); }
|
||||||
|
18% { transform: translate(-7px, 0); }
|
||||||
|
27% { transform: translate(-7px, -7px); }
|
||||||
|
36% { transform: translate(0, -7px); }
|
||||||
|
45% { transform: translate(0, 0); }
|
||||||
|
54% { transform: translate(0, -7px); }
|
||||||
|
63% { transform: translate(0, -7px); }
|
||||||
|
72% { transform: translate(0, -7px); }
|
||||||
|
81% { transform: translate(-7px, -7px); }
|
||||||
|
90% { transform: translate(-7px, 0); }
|
||||||
|
100% { transform: translate(0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes moveBox-5 {
|
||||||
|
9% { transform: translate(0, 0); }
|
||||||
|
18% { transform: translate(0, 0); }
|
||||||
|
27% { transform: translate(0, 0); }
|
||||||
|
36% { transform: translate(7px, 0); }
|
||||||
|
45% { transform: translate(7px, 0); }
|
||||||
|
54% { transform: translate(7px, 0); }
|
||||||
|
63% { transform: translate(7px, 0); }
|
||||||
|
72% { transform: translate(7px, 0); }
|
||||||
|
81% { transform: translate(7px, -7px); }
|
||||||
|
90% { transform: translate(0, -7px); }
|
||||||
|
100% { transform: translate(0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes moveBox-6 {
|
||||||
|
9% { transform: translate(0, 0); }
|
||||||
|
18% { transform: translate(-7px, 0); }
|
||||||
|
27% { transform: translate(-7px, 0); }
|
||||||
|
36% { transform: translate(0, 0); }
|
||||||
|
45% { transform: translate(0, 0); }
|
||||||
|
54% { transform: translate(0, 0); }
|
||||||
|
63% { transform: translate(0, 0); }
|
||||||
|
72% { transform: translate(0, 7px); }
|
||||||
|
81% { transform: translate(-7px, 7px); }
|
||||||
|
90% { transform: translate(-7px, 0); }
|
||||||
|
100% { transform: translate(0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes moveBox-7 {
|
||||||
|
9% { transform: translate(7px, 0); }
|
||||||
|
18% { transform: translate(7px, 0); }
|
||||||
|
27% { transform: translate(7px, 0); }
|
||||||
|
36% { transform: translate(0, 0); }
|
||||||
|
45% { transform: translate(0, -7px); }
|
||||||
|
54% { transform: translate(7px, -7px); }
|
||||||
|
63% { transform: translate(0, -7px); }
|
||||||
|
72% { transform: translate(0, -7px); }
|
||||||
|
81% { transform: translate(0, 0); }
|
||||||
|
90% { transform: translate(7px, 0); }
|
||||||
|
100% { transform: translate(0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes moveBox-8 {
|
||||||
|
9% { transform: translate(0, 0); }
|
||||||
|
18% { transform: translate(-7px, 0); }
|
||||||
|
27% { transform: translate(-7px, -7px); }
|
||||||
|
36% { transform: translate(0, -7px); }
|
||||||
|
45% { transform: translate(0, -7px); }
|
||||||
|
54% { transform: translate(0, -7px); }
|
||||||
|
63% { transform: translate(0, -7px); }
|
||||||
|
72% { transform: translate(0, -7px); }
|
||||||
|
81% { transform: translate(7px, -7px); }
|
||||||
|
90% { transform: translate(7px, 0); }
|
||||||
|
100% { transform: translate(0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes moveBox-9 {
|
||||||
|
9% { transform: translate(-7px, 0); }
|
||||||
|
18% { transform: translate(-7px, 0); }
|
||||||
|
27% { transform: translate(0, 0); }
|
||||||
|
36% { transform: translate(-7px, 0); }
|
||||||
|
45% { transform: translate(0, 0); }
|
||||||
|
54% { transform: translate(0, 0); }
|
||||||
|
63% { transform: translate(-7px, 0); }
|
||||||
|
72% { transform: translate(-7px, 0); }
|
||||||
|
81% { transform: translate(-14px, 0); }
|
||||||
|
90% { transform: translate(-7px, 0); }
|
||||||
|
100% { transform: translate(0, 0); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
84
static/src/components/chat/loaders/RippleLoader.vue
Normal file
84
static/src/components/chat/loaders/RippleLoader.vue
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ripple-loader">
|
||||||
|
<div class="cell d-0"></div>
|
||||||
|
<div class="cell d-1"></div>
|
||||||
|
<div class="cell d-2"></div>
|
||||||
|
<div class="cell d-1"></div>
|
||||||
|
<div class="cell d-2"></div>
|
||||||
|
<div class="cell d-2"></div>
|
||||||
|
<div class="cell d-3"></div>
|
||||||
|
<div class="cell d-3"></div>
|
||||||
|
<div class="cell d-4"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// From Uiverse.io by alexruix (modified)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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 {
|
||||||
|
flex: 0 0 var(--cell-size);
|
||||||
|
margin: var(--cell-spacing);
|
||||||
|
background-color: transparent;
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: 1.5s ripple ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.d-1 {
|
||||||
|
animation-delay: 100ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.d-2 {
|
||||||
|
animation-delay: 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.d-3 {
|
||||||
|
animation-delay: 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.d-4 {
|
||||||
|
animation-delay: 400ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell:nth-child(1),
|
||||||
|
.cell:nth-child(2),
|
||||||
|
.cell:nth-child(3),
|
||||||
|
.cell:nth-child(4),
|
||||||
|
.cell:nth-child(5),
|
||||||
|
.cell:nth-child(6),
|
||||||
|
.cell:nth-child(7),
|
||||||
|
.cell:nth-child(8),
|
||||||
|
.cell:nth-child(9) {
|
||||||
|
--cell-color: var(--claude-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ripple {
|
||||||
|
0% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
30% {
|
||||||
|
background-color: var(--cell-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
83
static/src/components/chat/loaders/RotatingSquaresLoader.vue
Normal file
83
static/src/components/chat/loaders/RotatingSquaresLoader.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rotating-squares-loader">
|
||||||
|
<div class="square"></div>
|
||||||
|
<div class="square"></div>
|
||||||
|
<div class="square"></div>
|
||||||
|
<div class="square"></div>
|
||||||
|
<div class="square"></div>
|
||||||
|
<div class="square"></div>
|
||||||
|
<div class="square"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// From Uiverse.io by ZacharyCrespin (modified)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@keyframes square-animation {
|
||||||
|
0% { left: 0; top: 0; }
|
||||||
|
10.5% { left: 0; top: 0; }
|
||||||
|
12.5% { left: 8px; top: 0; }
|
||||||
|
23% { left: 8px; top: 0; }
|
||||||
|
25% { left: 16px; top: 0; }
|
||||||
|
35.5% { left: 16px; top: 0; }
|
||||||
|
37.5% { left: 16px; top: 8px; }
|
||||||
|
48% { left: 16px; top: 8px; }
|
||||||
|
50% { left: 8px; top: 8px; }
|
||||||
|
60.5% { left: 8px; top: 8px; }
|
||||||
|
62.5% { left: 8px; top: 16px; }
|
||||||
|
73% { left: 8px; top: 16px; }
|
||||||
|
75% { left: 0; top: 16px; }
|
||||||
|
85.5% { left: 0; top: 16px; }
|
||||||
|
87.5% { left: 0; top: 8px; }
|
||||||
|
98% { left: 0; top: 8px; }
|
||||||
|
100% { left: 0; top: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotating-squares-loader {
|
||||||
|
position: relative;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.square {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
margin: 0.5px;
|
||||||
|
background: var(--claude-text-tertiary);
|
||||||
|
animation: square-animation 10s ease-in-out infinite both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square:nth-of-type(1) {
|
||||||
|
animation-delay: -1.43s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square:nth-of-type(2) {
|
||||||
|
animation-delay: -2.86s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square:nth-of-type(3) {
|
||||||
|
animation-delay: -4.29s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square:nth-of-type(4) {
|
||||||
|
animation-delay: -5.71s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square:nth-of-type(5) {
|
||||||
|
animation-delay: -7.14s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square:nth-of-type(6) {
|
||||||
|
animation-delay: -8.57s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square:nth-of-type(7) {
|
||||||
|
animation-delay: -10s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
static/src/components/chat/loaders/SquareGridLoader.vue
Normal file
95
static/src/components/chat/loaders/SquareGridLoader.vue
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<div class="square-grid-loader">
|
||||||
|
<div class="square" id="sq1"></div>
|
||||||
|
<div class="square" id="sq2"></div>
|
||||||
|
<div class="square" id="sq3"></div>
|
||||||
|
<div class="square" id="sq4"></div>
|
||||||
|
<div class="square" id="sq5"></div>
|
||||||
|
<div class="square" id="sq6"></div>
|
||||||
|
<div class="square" id="sq7"></div>
|
||||||
|
<div class="square" id="sq8"></div>
|
||||||
|
<div class="square" id="sq9"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// From Uiverse.io by JkHuger
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@keyframes loader_5191 {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.square-grid-loader {
|
||||||
|
position: relative;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square {
|
||||||
|
background: var(--claude-text-tertiary);
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -1.5px;
|
||||||
|
margin-left: -1.5px;
|
||||||
|
border-radius: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sq1 {
|
||||||
|
margin-top: -7.5px;
|
||||||
|
margin-left: -7.5px;
|
||||||
|
animation: loader_5191 675ms ease-in-out 0s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sq2 {
|
||||||
|
margin-top: -7.5px;
|
||||||
|
animation: loader_5191 675ms ease-in-out 75ms infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sq3 {
|
||||||
|
margin-top: -7.5px;
|
||||||
|
margin-left: 4.5px;
|
||||||
|
animation: loader_5191 675ms ease-in-out 150ms infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sq4 {
|
||||||
|
margin-left: -7.5px;
|
||||||
|
animation: loader_5191 675ms ease-in-out 225ms infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sq5 {
|
||||||
|
animation: loader_5191 675ms ease-in-out 300ms infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sq6 {
|
||||||
|
margin-left: 4.5px;
|
||||||
|
animation: loader_5191 675ms ease-in-out 375ms infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sq7 {
|
||||||
|
margin-top: 4.5px;
|
||||||
|
margin-left: -7.5px;
|
||||||
|
animation: loader_5191 675ms ease-in-out 450ms infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sq8 {
|
||||||
|
margin-top: 4.5px;
|
||||||
|
animation: loader_5191 675ms ease-in-out 525ms infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sq9 {
|
||||||
|
margin-top: 4.5px;
|
||||||
|
margin-left: 4.5px;
|
||||||
|
animation: loader_5191 675ms ease-in-out 600ms infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
25
static/src/components/chat/loaders/index.ts
Normal file
25
static/src/components/chat/loaders/index.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Component } from 'vue';
|
||||||
|
import SquareGridLoader from './SquareGridLoader.vue';
|
||||||
|
import RotatingSquaresLoader from './RotatingSquaresLoader.vue';
|
||||||
|
import DualChaseLoader from './DualChaseLoader.vue';
|
||||||
|
import BouncingSquaresLoader from './BouncingSquaresLoader.vue';
|
||||||
|
import FiveSquaresLoader from './FiveSquaresLoader.vue';
|
||||||
|
import NineGridLoader from './NineGridLoader.vue';
|
||||||
|
import RippleLoader from './RippleLoader.vue';
|
||||||
|
|
||||||
|
// 加载动画池
|
||||||
|
export const loaderPool: Component[] = [
|
||||||
|
SquareGridLoader,
|
||||||
|
RotatingSquaresLoader,
|
||||||
|
DualChaseLoader,
|
||||||
|
BouncingSquaresLoader,
|
||||||
|
FiveSquaresLoader,
|
||||||
|
NineGridLoader,
|
||||||
|
RippleLoader
|
||||||
|
];
|
||||||
|
|
||||||
|
// 随机获取一个加载动画
|
||||||
|
export function getRandomLoader(): Component {
|
||||||
|
const randomIndex = Math.floor(Math.random() * loaderPool.length);
|
||||||
|
return loaderPool[randomIndex];
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user