feat(ui): restore reply-header generating hint and refine composer avatar text

This commit is contained in:
JOJO 2026-07-06 04:47:40 +08:00
parent 01dfec4c72
commit 7863e497d8
6 changed files with 294 additions and 213 deletions

View File

@ -222,7 +222,6 @@
v-show="chatDisplayMode === 'chat'"
ref="messagesArea"
:messages="messages"
:avatar-status="avatarStatus"
:icon-style="iconStyle"
:expanded-blocks="expandedBlocks"
:render-markdown="renderMarkdown"
@ -319,6 +318,7 @@
:goal-mode-armed="goalModeArmed"
:goal-running="goalRunning"
:goal-progress="goalProgress"
:avatar-status="showStatusAvatar && messages.length > 0 ? avatarStatus : null"
@restore-user-question="restoreUserQuestionDialog"
@update:input-message="inputSetMessage"
@input-change="handleInputChange"

View File

@ -357,21 +357,21 @@ export const computed = {
}
if (runningTools.length > 0) {
const keys = runningTools.map((a) => toolFaceKey(a?.tool?.name));
const toolTexts = runningTools.map((a) => {
const tool = a?.tool;
const getFinalIntentText = (tool: any) => {
if (!tool) return '';
const status = getToolStatusText(tool, { intentEnabled });
const desc = getToolDescription(tool);
return desc ? `${status} · ${desc}` : status;
const full = tool.intent_full || '';
const rendered = tool.intent_rendered || '';
// 只在 intent 打字效果完成后才显示完整文案,避免头像上出现逐字动画
return full && rendered === full ? full : '';
};
const toolTexts = runningTools.map((a) => {
return intentEnabled ? getFinalIntentText(a?.tool) : '';
});
let text = toolTexts[0] || '';
if (runningTools.length > 1) {
text = toolTexts[0] || `并行调用 ${runningTools.length} 个工具...`;
text = toolTexts[0] || '';
} else if (!text) {
const first = runningTools[0]?.tool;
const status = getToolStatusText(first, { intentEnabled });
const desc = getToolDescription(first);
text = desc ? `${status} · ${desc}` : status;
text = intentEnabled ? getFinalIntentText(runningTools[0]?.tool) : '';
}
return { mode: 'tool', toolKeys: keys, toolTexts, text, tracking: false };
}

View File

@ -8,6 +8,13 @@
aria-hidden="true"
@click="handleAvatarClick"
>
<!-- flat-top 圆角正六边形背景与对话区背景同色 -->
<path
v-if="props.hexBackground"
class="sa-bg"
d="M 175.959,107.000 Q 180.000,100.000 175.959,93.000 L 144.041,37.718 Q 140.000,30.718 131.917,30.718 L 68.083,30.718 Q 60.000,30.718 55.959,37.718 L 24.041,93.000 Q 20.000,100.000 24.041,107.000 L 55.959,162.282 Q 60.000,169.282 68.083,169.282 L 131.917,169.282 Q 140.000,169.282 144.041,162.282 Z"
/>
<!-- flat-top 圆角正六边形外框 -->
<path
class="sa-frame"
@ -23,8 +30,18 @@
<g class="sa-face" ref="faceRef">
<!-- 眼睛组idle 眼睛 work 提示符 互相 morph -->
<g class="sa-fc sa-shared" :class="{ hidden: !sharedVisible }">
<path class="sa-eye" ref="eyeLeftRef" d="M 0,0 Q 8.5,0 17,0 Q 25.5,0 34,0 Q 25.5,0 17,0 Q 8.5,0 0,0" />
<path class="sa-eye" ref="eyeRightRef" d="M 0,0 Q 8.5,0 17,0 Q 25.5,0 34,0 Q 25.5,0 17,0 Q 8.5,0 0,0" />
<path
class="sa-eye"
ref="eyeLeftRef"
d="M 0,-11 Q 0,-5.5 0,0 Q 0,5.5 0,11 Q 0,5.5 0,0 Q 0,-5.5 0,-11"
transform="translate(84, 96)"
/>
<path
class="sa-eye"
ref="eyeRightRef"
d="M 0,-11 Q 0,-5.5 0,0 Q 0,5.5 0,11 Q 0,5.5 0,0 Q 0,-5.5 0,-11"
transform="translate(116, 96)"
/>
</g>
<!-- 思考三个大点 -->
@ -73,12 +90,14 @@ const props = withDefaults(
toolKeys?: string[];
tracking?: boolean;
size?: number;
hexBackground?: boolean;
}>(),
{
mode: 'idle',
toolKeys: () => [],
tracking: false,
size: 30
size: 30,
hexBackground: false
}
);
const emit = defineEmits<{
@ -228,14 +247,22 @@ function applyWorkTransform() {
eyeRightRef.value.style.transform = `translate(${WORK_ORIGIN.x}px, ${WORK_ORIGIN.y}px) rotate(${WORK_ROTATION_RIGHT}deg)`;
}
}
function applyIdleTransform() {
function applyIdleTransform(animate = true) {
if (eyeLeftRef.value) {
eyeLeftRef.value.style.transition = DEFAULT_EYE_TRANSITION;
if (!animate) eyeLeftRef.value.style.transition = 'none';
eyeLeftRef.value.style.transform = `translate(${EYE_LEFT_ORIGIN.x}px, ${EYE_LEFT_ORIGIN.y}px)`;
if (!animate) {
void eyeLeftRef.value.offsetWidth;
eyeLeftRef.value.style.transition = '';
}
}
if (eyeRightRef.value) {
eyeRightRef.value.style.transition = DEFAULT_EYE_TRANSITION;
if (!animate) eyeRightRef.value.style.transition = 'none';
eyeRightRef.value.style.transform = `translate(${EYE_RIGHT_ORIGIN.x}px, ${EYE_RIGHT_ORIGIN.y}px)`;
if (!animate) {
void eyeRightRef.value.offsetWidth;
eyeRightRef.value.style.transition = '';
}
}
}
function applyNervousTransform() {
@ -361,7 +388,7 @@ function triggerNervous() {
nervousTimer = window.setTimeout(() => {
if (internalMode.value === 'nervous') {
internalMode.value = null;
applyState();
// watcher applyState
}
}, 500);
}
@ -469,7 +496,8 @@ function applyState() {
watch(() => [props.mode, props.toolKeys, props.tracking, internalMode.value], () => applyState(), { deep: true });
onMounted(() => {
applyIdleTransform();
// SVG
applyIdleTransform(false);
document.addEventListener('mousemove', onMouseMove);
applyState();
});
@ -494,6 +522,11 @@ onBeforeUnmount(() => {
flex-shrink: 0;
}
.sa-bg {
fill: var(--chat-surface-color);
stroke: none;
}
.sa-frame {
fill: none;
stroke: currentColor;

View File

@ -112,6 +112,27 @@
assistantWorkLabel(index)
}}</span>
</div>
<div
v-if="msg.awaitingFirstContent"
class="action-item streaming-content immediate-show assistant-generating-block"
>
<div class="text-output">
<div
class="text-content assistant-generating-placeholder"
role="status"
aria-live="polite"
>
<span
v-for="(letter, letterIndex) in getGeneratingLetters(msg)"
:key="letterIndex"
class="assistant-generating-letter"
:style="{ animationDelay: `${letterIndex * 0.08}s` }"
>
{{ letter }}
</span>
</div>
</div>
</div>
<template v-if="blockDisplayMode === 'minimal'">
<MinimalBlocks
v-if="hasRenderableAssistantActions(msg.actions || [])"
@ -619,42 +640,6 @@
</div>
</div>
</div>
<div
v-if="showStatusAvatar && (filteredMessages || []).length > 0"
ref="statusAvatarRef"
class="conversation-status-avatar"
role="status"
aria-live="polite"
>
<StatusAvatar
:mode="avatarStatusSafe.mode"
:tool-keys="avatarStatusSafe.toolKeys"
:tracking="avatarStatusSafe.tracking"
:size="34"
@active-index="handleStatusAvatarActiveIndex"
@poke="handleStatusAvatarPoke"
/>
<Transition name="conversation-status-avatar-text-fade" mode="out-in">
<span
v-if="statusAvatarText"
:key="statusAvatarText"
class="conversation-status-avatar__text"
:class="{ 'conversation-status-avatar__text--animated': statusAvatarTextAnimated }"
>
<template v-if="statusAvatarTextAnimated">
<span
v-for="(letter, letterIndex) in statusAvatarLetters"
:key="`${letter}-${letterIndex}`"
class="conversation-status-avatar__letter"
:style="{ animationDelay: `${letterIndex * 0.08}s` }"
>
{{ letter }}
</span>
</template>
<template v-else>{{ statusAvatarText }}</template>
</span>
</Transition>
</div>
</div>
<div class="messages-bottom-spacer" aria-hidden="true"></div>
</div>
@ -668,19 +653,11 @@ import ToolAction from '@/components/chat/actions/ToolAction.vue';
import StackedBlocks from './StackedBlocks.vue';
import MinimalBlocks from './MinimalBlocks.vue';
import MarkdownRenderer from './MarkdownRenderer.vue';
import StatusAvatar from '@/components/avatar/StatusAvatar.vue';
import { usePersonalizationStore } from '@/stores/personalization';
import { getMessageVisibility, messageStartsWork } from '@/utils/messageVisibility';
const props = defineProps<{
messages: Array<any>;
avatarStatus?: {
mode?: string;
toolKeys?: string[];
toolTexts?: string[];
text?: string;
tracking?: boolean;
};
iconStyle: (key: string, size?: string) => Record<string, string>;
expandedBlocks: Set<string>;
streamingMessage: boolean;
@ -725,7 +702,6 @@ const stackedBlocksEnabled = computed(() => {
// 使
return blockDisplayMode.value === 'stacked' || blockDisplayMode.value === 'minimal';
});
const showStatusAvatar = computed(() => personalization.form.show_status_avatar !== false);
const aiAssistantName = computed(() => {
if (!personalization.form.use_custom_names) {
return 'AI Assistant';
@ -748,6 +724,7 @@ interface UserBubbleFoldState {
}
const USER_BUBBLE_FOLD_LINES = 10;
const DEFAULT_GENERATING_TEXT = '生成中…';
const userBubbleRefs = new Map<string, HTMLElement>();
const userBubbleFoldStates = ref<Record<string, UserBubbleFoldState>>({});
const copiedBubbleKeys = ref<Set<string>>(new Set());
@ -1034,80 +1011,6 @@ const filteredMessages = computed(() => {
const getFilteredMessagesSafe = () =>
Array.isArray(filteredMessages.value) ? filteredMessages.value : [];
const latestMessageIndex = computed(() => getFilteredMessagesSafe().length - 1);
const statusAvatarActiveIndex = ref(0);
const statusAvatarRef = ref<HTMLElement | null>(null);
const statusAvatarPokeText = ref('');
let statusAvatarPokeTimer: number | null = null;
const STATUS_AVATAR_POKE_TEXTS = [
'不要!',
'停下来!',
'别戳啦!',
'哎呀!',
'你戳到我了!',
'轻一点嘛!',
'我不是按钮!',
'不许乱点!',
'呜……别戳我了……',
'六边形也是会痛的!',
'再戳我要变形了!',
'我会紧张的……',
'正在假装镇定……',
'Token 掉了一地!',
'上下文被你戳乱了!',
'防戳模式启动失败',
'系统提示AI 被戳了一下',
'警告:检测到手欠行为',
'我记住你了!',
'戳回去!'
];
const avatarStatusSafe = computed(() => ({
mode: props.avatarStatus?.mode || 'idle',
toolKeys: Array.isArray(props.avatarStatus?.toolKeys) ? props.avatarStatus!.toolKeys! : [],
toolTexts: Array.isArray(props.avatarStatus?.toolTexts) ? props.avatarStatus!.toolTexts! : [],
text: typeof props.avatarStatus?.text === 'string' ? props.avatarStatus.text : '',
tracking: props.avatarStatus?.tracking !== false
}));
const statusAvatarText = computed(() => {
if (statusAvatarPokeText.value && avatarStatusSafe.value.mode === 'idle') {
return statusAvatarPokeText.value;
}
if (avatarStatusSafe.value.mode === 'tool' && avatarStatusSafe.value.toolTexts.length > 1) {
const idx = statusAvatarActiveIndex.value % avatarStatusSafe.value.toolTexts.length;
return avatarStatusSafe.value.toolTexts[idx] || avatarStatusSafe.value.text;
}
return avatarStatusSafe.value.text;
});
const statusAvatarTextAnimated = computed(() => {
return avatarStatusSafe.value.mode === 'work' && !!statusAvatarText.value;
});
const statusAvatarLetters = computed(() => Array.from(statusAvatarText.value || ''));
const clearStatusAvatarPokeText = () => {
if (statusAvatarPokeTimer !== null) {
window.clearTimeout(statusAvatarPokeTimer);
statusAvatarPokeTimer = null;
}
statusAvatarPokeText.value = '';
};
const handleStatusAvatarActiveIndex = (index: number) => {
statusAvatarActiveIndex.value = Number.isFinite(index) ? Math.max(0, index) : 0;
};
const handleStatusAvatarPoke = () => {
if (avatarStatusSafe.value.mode !== 'idle') return;
const texts = STATUS_AVATAR_POKE_TEXTS;
statusAvatarPokeText.value = texts[Math.floor(Math.random() * texts.length)] || '不要!';
if (statusAvatarPokeTimer !== null) window.clearTimeout(statusAvatarPokeTimer);
statusAvatarPokeTimer = window.setTimeout(() => {
statusAvatarPokeText.value = '';
statusAvatarPokeTimer = null;
}, 1800);
};
watch(
() => [props.avatarStatus?.mode, props.avatarStatus?.toolKeys?.join('|')],
() => {
statusAvatarActiveIndex.value = 0;
if (avatarStatusSafe.value.mode !== 'idle') clearStatusAvatarPokeText();
}
);
const nowMs = ref(Date.now());
let timerHandle: number | null = null;
const debugLoggedSystemActionKeys = new Set<string>();
@ -2008,7 +1911,6 @@ onBeforeUnmount(() => {
clearInterval(timerHandle);
timerHandle = null;
}
clearStatusAvatarPokeText();
});
const isStackable = (action: any) =>
@ -2083,6 +1985,14 @@ const splitActionGroups = (actions: any[] = [], messageIndex = 0) => {
return result;
};
function getGeneratingLetters(message: any) {
const label =
typeof message?.generatingLabel === 'string' && message.generatingLabel.trim()
? message.generatingLabel.trim()
: DEFAULT_GENERATING_TEXT;
return Array.from(label);
}
defineExpose({
rootEl,
getThinkingRef,

View File

@ -83,6 +83,41 @@
@select="selectFileAtItemByIndex"
@hover="fileAtActiveIndex = $event"
/>
<transition name="floating-status-motion">
<div v-if="showComposerAvatar" class="composer-avatar" @click.stop="handleAvatarPoke">
<StatusAvatar
:mode="props.avatarStatus.mode"
:tool-keys="props.avatarStatus.toolKeys || []"
:tracking="props.avatarStatus.tracking !== false"
:size="40"
hex-background
/>
<Transition name="composer-avatar-text-fade" mode="out-in">
<span
v-if="avatarPokeText"
:key="`poke-${avatarPokeText}`"
class="composer-avatar__text"
:class="{
'composer-avatar__text--right': !floatingStatusVisible,
'composer-avatar__text--top': floatingStatusVisible
}"
>
{{ avatarPokeText }}
</span>
<span
v-else-if="composerAvatarText"
:key="`tool-${props.avatarStatus?.mode}`"
class="composer-avatar__text"
:class="{
'composer-avatar__text--right': !floatingStatusVisible,
'composer-avatar__text--top': floatingStatusVisible
}"
>
{{ composerAvatarText }}
</span>
</Transition>
</div>
</transition>
<transition name="floating-status-motion">
<div v-if="floatingStatusVisible" class="floating-project-status" @click.stop>
<span v-if="projectGitSummaryForRender" class="floating-project-status__left">
@ -463,6 +498,7 @@ import { TextSelection } from 'prosemirror-state';
import QuickMenu from '@/components/input/QuickMenu.vue';
import FileAtMenu, { type FileAtItem } from '@/components/input/FileAtMenu.vue';
import RollingNumber from '@/components/input/RollingNumber.vue';
import StatusAvatar from '@/components/avatar/StatusAvatar.vue';
import { useInputStore } from '@/stores/input';
import { usePersonalizationStore } from '@/stores/personalization';
@ -579,6 +615,13 @@ const props = defineProps<{
hasPendingRuntimeGuidance?: boolean;
terminalCount?: number;
hostMode?: boolean;
avatarStatus?: {
mode: string;
toolKeys?: string[];
toolTexts?: string[];
text: string;
tracking?: boolean;
} | null;
}>();
const inputStore = useInputStore();
@ -638,6 +681,31 @@ const fileAtMenuStyle = ref<Record<string, string>>({});
const fileAtPickerActive = ref(false);
let fileAtSearchTimer: number | null = null;
const avatarPokeText = ref('');
let avatarPokeTimer: number | null = null;
const STATUS_AVATAR_POKE_TEXTS = [
'不要!',
'停下来!',
'别戳啦!',
'哎呀!',
'你戳到我了!',
'轻一点嘛!',
'我不是按钮!',
'不许乱点!',
'呜……别戳我了……',
'六边形也是会痛的!',
'再戳我要变形了!',
'我会紧张的……',
'正在假装镇定……',
'Token 掉了一地!',
'上下文被你戳乱了!',
'防戳模式启动失败',
'系统提示AI 被戳了一下',
'警告:检测到手欠行为',
'我记住你了!',
'戳回去!'
];
const slashHighlightStyle = computed(() => {
if (slashManualScrolled.value) return { display: 'none' };
const isFirst = skillSlashActiveIndex.value === 0 && skillSlashList.value?.scrollTop === 0;
@ -764,6 +832,22 @@ const floatingStatusVisible = computed(() => {
);
});
const showComposerAvatar = computed(() => {
if (skillSlashMenuOpen.value || fileAtOpen.value || props.quickMenuOpen) return false;
if (runtimeQueuedMessagesForRender.value.length > 0 || props.hasPendingRuntimeGuidance)
return false;
return !!props.avatarStatus;
});
const composerAvatarText = computed(() => {
if (!props.avatarStatus) return '';
// tool intent think
if (props.avatarStatus.mode === 'tool' || props.avatarStatus.mode === 'think') {
return props.avatarStatus.text || '';
}
return '';
});
const projectGitMenuOpen = ref<null | 'project' | 'branch'>(null);
const closeProjectGitMenu = () => {
@ -869,6 +953,25 @@ const closeFileAtMenu = () => {
}
};
const clearAvatarPokeText = () => {
if (avatarPokeTimer !== null) {
window.clearTimeout(avatarPokeTimer);
avatarPokeTimer = null;
}
avatarPokeText.value = '';
};
const handleAvatarPoke = () => {
if (!props.avatarStatus || props.avatarStatus.mode !== 'idle') return;
const texts = STATUS_AVATAR_POKE_TEXTS;
avatarPokeText.value = texts[Math.floor(Math.random() * texts.length)] || '不要!';
if (avatarPokeTimer !== null) window.clearTimeout(avatarPokeTimer);
avatarPokeTimer = window.setTimeout(() => {
avatarPokeText.value = '';
avatarPokeTimer = null;
}, 1800);
};
const updateFileAtMenuPosition = (tokenEnd: number) => {
nextTick(() => {
const instance = editor.value;
@ -2312,6 +2415,7 @@ const hasRuntimeLayoutExpansion = computed(() => {
return (
hasQueue ||
floatingStatusVisible.value ||
showComposerAvatar.value ||
skillSlashOpen.value ||
fileAtOpen.value ||
hasImages ||
@ -2380,13 +2484,14 @@ const collectComposerVisualHeight = () => {
//
// .goal-mode-banner--collapsed
const nodes = root.querySelectorAll(
'.runtime-queue-list:not(.runtime-queue-list--empty), .floating-project-status'
'.runtime-queue-list:not(.runtime-queue-list--empty), .floating-project-status, .composer-avatar'
);
nodes.forEach((node) => {
if (!(node instanceof HTMLElement)) return;
// floatingStatusVisible false DOM 退
// 退
// floatingStatusVisible / showComposerAvatar false DOM 退
// 退
if (node.classList.contains('floating-project-status') && !floatingStatusVisible.value) return;
if (node.classList.contains('composer-avatar') && !showComposerAvatar.value) return;
top = Math.min(top, node.offsetTop);
bottom = Math.max(bottom, node.offsetTop + node.offsetHeight);
});
@ -2418,7 +2523,7 @@ const emitComposerHeight = () => {
}
const baseline = baselineComposerVisualHeight.value || visualHeight;
const growth = Math.max(0, visualHeight - baseline);
const baseReservedHeight = 93;
const baseReservedHeight = 123;
const reservedHeight = Math.ceil(baseReservedHeight + growth);
emit('composer-height-change', {
reservedHeight,
@ -2755,6 +2860,21 @@ watch(floatingStatusVisible, async () => {
}, 280);
});
watch(showComposerAvatar, async () => {
await nextTick();
emitComposerHeight();
window.setTimeout(() => {
emitComposerHeight();
}, 280);
});
watch(
() => props.avatarStatus?.mode,
() => {
if (props.avatarStatus?.mode !== 'idle') clearAvatarPokeText();
}
);
watch(
() => [props.selectedImages?.length || 0, props.selectedVideos?.length || 0],
async () => {
@ -2831,6 +2951,7 @@ onBeforeUnmount(() => {
composerResizeObserver.disconnect();
composerResizeObserver = null;
}
clearAvatarPokeText();
});
</script>
@ -2855,6 +2976,61 @@ onBeforeUnmount(() => {
pointer-events: auto;
}
.composer-avatar {
position: absolute;
left: 0;
bottom: calc(100% + 6px);
z-index: auto;
display: flex;
align-items: center;
justify-content: flex-start;
height: 40px;
pointer-events: auto;
color: var(--claude-text);
}
.composer-avatar .status-avatar {
display: block;
flex-shrink: 0;
}
.composer-avatar__text {
position: absolute;
white-space: nowrap;
font-size: 14px;
font-weight: 500;
color: var(--claude-text-secondary);
pointer-events: none;
z-index: 1;
}
.composer-avatar__text--right {
left: calc(100% + 8px);
top: 50%;
transform: translateY(-50%);
}
.composer-avatar__text--top {
left: 0;
bottom: calc(100% + 4px);
transform: none;
}
.composer-avatar-text-fade-enter-active,
.composer-avatar-text-fade-leave-active {
transition: opacity 0.18s ease;
}
.composer-avatar-text-fade-enter-from,
.composer-avatar-text-fade-leave-to {
opacity: 0;
}
.composer-avatar-text-fade-enter-to,
.composer-avatar-text-fade-leave-from {
opacity: 1;
}
.floating-project-status__left,
.floating-project-status__right {
min-width: 0;

View File

@ -126,76 +126,6 @@
margin-bottom: 0;
}
.conversation-status-avatar {
display: flex;
align-items: center;
gap: 10px;
min-height: 40px;
margin: 12px 0 18px;
color: var(--text-secondary);
}
.conversation-status-avatar .status-avatar {
color: var(--text-primary);
}
.conversation-status-avatar__text {
min-width: 0;
max-width: min(560px, 86%);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-secondary);
font-size: 13px;
font-weight: 500;
letter-spacing: 0.01em;
}
.conversation-status-avatar-text-fade-enter-active,
.conversation-status-avatar-text-fade-leave-active {
transition: opacity 0.18s ease;
}
.conversation-status-avatar-text-fade-enter-from,
.conversation-status-avatar-text-fade-leave-to {
opacity: 0;
}
.conversation-status-avatar-text-fade-enter-to,
.conversation-status-avatar-text-fade-leave-from {
opacity: 1;
}
.conversation-status-avatar__text--animated {
display: inline-flex;
align-items: center;
gap: 0.02em;
letter-spacing: 0.05em;
}
.conversation-status-avatar__letter {
display: inline-block;
opacity: 0.42;
transform: translateY(0);
animation: conversation-status-letter-wave 1.6s ease-in-out infinite;
}
@keyframes conversation-status-letter-wave {
0%,
100% {
opacity: 0.42;
transform: translateY(0);
}
20% {
opacity: 1;
transform: translateY(-3px);
}
42% {
opacity: 0.68;
transform: translateY(0);
}
}
.messages-bottom-spacer {
width: 1px;
height: var(--composer-growth-height, 0px);
@ -611,6 +541,38 @@
color: var(--claude-text-secondary);
}
.assistant-generating-placeholder {
display: inline-flex;
align-items: center;
color: var(--claude-text-secondary);
font-size: 14px;
font-weight: 500;
letter-spacing: 0.05em;
}
.assistant-generating-letter {
display: inline-block;
opacity: 0.42;
transform: translateY(0);
animation: assistant-generating-letter-wave 1.6s ease-in-out infinite;
}
@keyframes assistant-generating-letter-wave {
0%,
100% {
opacity: 0.42;
transform: translateY(0);
}
20% {
opacity: 1;
transform: translateY(-3px);
}
42% {
opacity: 0.68;
transform: translateY(0);
}
}
.user-message .message-text,
.assistant-message .message-text {
padding: 16px 20px;