feat(ui): restore reply-header generating hint and refine composer avatar text
This commit is contained in:
parent
01dfec4c72
commit
7863e497d8
@ -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"
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user