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'"
|
v-show="chatDisplayMode === 'chat'"
|
||||||
ref="messagesArea"
|
ref="messagesArea"
|
||||||
:messages="messages"
|
:messages="messages"
|
||||||
:avatar-status="avatarStatus"
|
|
||||||
:icon-style="iconStyle"
|
:icon-style="iconStyle"
|
||||||
:expanded-blocks="expandedBlocks"
|
:expanded-blocks="expandedBlocks"
|
||||||
:render-markdown="renderMarkdown"
|
:render-markdown="renderMarkdown"
|
||||||
@ -319,6 +318,7 @@
|
|||||||
:goal-mode-armed="goalModeArmed"
|
:goal-mode-armed="goalModeArmed"
|
||||||
:goal-running="goalRunning"
|
:goal-running="goalRunning"
|
||||||
:goal-progress="goalProgress"
|
:goal-progress="goalProgress"
|
||||||
|
:avatar-status="showStatusAvatar && messages.length > 0 ? avatarStatus : null"
|
||||||
@restore-user-question="restoreUserQuestionDialog"
|
@restore-user-question="restoreUserQuestionDialog"
|
||||||
@update:input-message="inputSetMessage"
|
@update:input-message="inputSetMessage"
|
||||||
@input-change="handleInputChange"
|
@input-change="handleInputChange"
|
||||||
|
|||||||
@ -357,21 +357,21 @@ export const computed = {
|
|||||||
}
|
}
|
||||||
if (runningTools.length > 0) {
|
if (runningTools.length > 0) {
|
||||||
const keys = runningTools.map((a) => toolFaceKey(a?.tool?.name));
|
const keys = runningTools.map((a) => toolFaceKey(a?.tool?.name));
|
||||||
const toolTexts = runningTools.map((a) => {
|
const getFinalIntentText = (tool: any) => {
|
||||||
const tool = a?.tool;
|
|
||||||
if (!tool) return '';
|
if (!tool) return '';
|
||||||
const status = getToolStatusText(tool, { intentEnabled });
|
const full = tool.intent_full || '';
|
||||||
const desc = getToolDescription(tool);
|
const rendered = tool.intent_rendered || '';
|
||||||
return desc ? `${status} · ${desc}` : status;
|
// 只在 intent 打字效果完成后才显示完整文案,避免头像上出现逐字动画
|
||||||
|
return full && rendered === full ? full : '';
|
||||||
|
};
|
||||||
|
const toolTexts = runningTools.map((a) => {
|
||||||
|
return intentEnabled ? getFinalIntentText(a?.tool) : '';
|
||||||
});
|
});
|
||||||
let text = toolTexts[0] || '';
|
let text = toolTexts[0] || '';
|
||||||
if (runningTools.length > 1) {
|
if (runningTools.length > 1) {
|
||||||
text = toolTexts[0] || `并行调用 ${runningTools.length} 个工具...`;
|
text = toolTexts[0] || '';
|
||||||
} else if (!text) {
|
} else if (!text) {
|
||||||
const first = runningTools[0]?.tool;
|
text = intentEnabled ? getFinalIntentText(runningTools[0]?.tool) : '';
|
||||||
const status = getToolStatusText(first, { intentEnabled });
|
|
||||||
const desc = getToolDescription(first);
|
|
||||||
text = desc ? `${status} · ${desc}` : status;
|
|
||||||
}
|
}
|
||||||
return { mode: 'tool', toolKeys: keys, toolTexts, text, tracking: false };
|
return { mode: 'tool', toolKeys: keys, toolTexts, text, tracking: false };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,13 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@click="handleAvatarClick"
|
@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 圆角正六边形外框 -->
|
<!-- flat-top 圆角正六边形外框 -->
|
||||||
<path
|
<path
|
||||||
class="sa-frame"
|
class="sa-frame"
|
||||||
@ -23,8 +30,18 @@
|
|||||||
<g class="sa-face" ref="faceRef">
|
<g class="sa-face" ref="faceRef">
|
||||||
<!-- 眼睛组:idle 眼睛 与 work 提示符 互相 morph -->
|
<!-- 眼睛组:idle 眼睛 与 work 提示符 互相 morph -->
|
||||||
<g class="sa-fc sa-shared" :class="{ hidden: !sharedVisible }">
|
<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
|
||||||
<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" />
|
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>
|
</g>
|
||||||
|
|
||||||
<!-- 思考:三个大点 -->
|
<!-- 思考:三个大点 -->
|
||||||
@ -73,12 +90,14 @@ const props = withDefaults(
|
|||||||
toolKeys?: string[];
|
toolKeys?: string[];
|
||||||
tracking?: boolean;
|
tracking?: boolean;
|
||||||
size?: number;
|
size?: number;
|
||||||
|
hexBackground?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
mode: 'idle',
|
mode: 'idle',
|
||||||
toolKeys: () => [],
|
toolKeys: () => [],
|
||||||
tracking: false,
|
tracking: false,
|
||||||
size: 30
|
size: 30,
|
||||||
|
hexBackground: false
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const emit = defineEmits<{
|
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)`;
|
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) {
|
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)`;
|
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) {
|
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)`;
|
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() {
|
function applyNervousTransform() {
|
||||||
@ -361,7 +388,7 @@ function triggerNervous() {
|
|||||||
nervousTimer = window.setTimeout(() => {
|
nervousTimer = window.setTimeout(() => {
|
||||||
if (internalMode.value === 'nervous') {
|
if (internalMode.value === 'nervous') {
|
||||||
internalMode.value = null;
|
internalMode.value = null;
|
||||||
applyState();
|
// 由 watcher 统一调用 applyState,避免与显式调用叠加导致动画被重置
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
@ -469,7 +496,8 @@ function applyState() {
|
|||||||
watch(() => [props.mode, props.toolKeys, props.tracking, internalMode.value], () => applyState(), { deep: true });
|
watch(() => [props.mode, props.toolKeys, props.tracking, internalMode.value], () => applyState(), { deep: true });
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
applyIdleTransform();
|
// 初始化时直接定位,避免眼睛从 SVG 原点飞入
|
||||||
|
applyIdleTransform(false);
|
||||||
document.addEventListener('mousemove', onMouseMove);
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
applyState();
|
applyState();
|
||||||
});
|
});
|
||||||
@ -494,6 +522,11 @@ onBeforeUnmount(() => {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sa-bg {
|
||||||
|
fill: var(--chat-surface-color);
|
||||||
|
stroke: none;
|
||||||
|
}
|
||||||
|
|
||||||
.sa-frame {
|
.sa-frame {
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: currentColor;
|
stroke: currentColor;
|
||||||
|
|||||||
@ -112,6 +112,27 @@
|
|||||||
assistantWorkLabel(index)
|
assistantWorkLabel(index)
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</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'">
|
<template v-if="blockDisplayMode === 'minimal'">
|
||||||
<MinimalBlocks
|
<MinimalBlocks
|
||||||
v-if="hasRenderableAssistantActions(msg.actions || [])"
|
v-if="hasRenderableAssistantActions(msg.actions || [])"
|
||||||
@ -619,42 +640,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
<div class="messages-bottom-spacer" aria-hidden="true"></div>
|
<div class="messages-bottom-spacer" aria-hidden="true"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -668,19 +653,11 @@ import ToolAction from '@/components/chat/actions/ToolAction.vue';
|
|||||||
import StackedBlocks from './StackedBlocks.vue';
|
import StackedBlocks from './StackedBlocks.vue';
|
||||||
import MinimalBlocks from './MinimalBlocks.vue';
|
import MinimalBlocks from './MinimalBlocks.vue';
|
||||||
import MarkdownRenderer from './MarkdownRenderer.vue';
|
import MarkdownRenderer from './MarkdownRenderer.vue';
|
||||||
import StatusAvatar from '@/components/avatar/StatusAvatar.vue';
|
|
||||||
import { usePersonalizationStore } from '@/stores/personalization';
|
import { usePersonalizationStore } from '@/stores/personalization';
|
||||||
import { getMessageVisibility, messageStartsWork } from '@/utils/messageVisibility';
|
import { getMessageVisibility, messageStartsWork } from '@/utils/messageVisibility';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
messages: Array<any>;
|
messages: Array<any>;
|
||||||
avatarStatus?: {
|
|
||||||
mode?: string;
|
|
||||||
toolKeys?: string[];
|
|
||||||
toolTexts?: string[];
|
|
||||||
text?: string;
|
|
||||||
tracking?: boolean;
|
|
||||||
};
|
|
||||||
iconStyle: (key: string, size?: string) => Record<string, string>;
|
iconStyle: (key: string, size?: string) => Record<string, string>;
|
||||||
expandedBlocks: Set<string>;
|
expandedBlocks: Set<string>;
|
||||||
streamingMessage: boolean;
|
streamingMessage: boolean;
|
||||||
@ -725,7 +702,6 @@ const stackedBlocksEnabled = computed(() => {
|
|||||||
// 堆叠模式和极简模式都使用堆叠布局
|
// 堆叠模式和极简模式都使用堆叠布局
|
||||||
return blockDisplayMode.value === 'stacked' || blockDisplayMode.value === 'minimal';
|
return blockDisplayMode.value === 'stacked' || blockDisplayMode.value === 'minimal';
|
||||||
});
|
});
|
||||||
const showStatusAvatar = computed(() => personalization.form.show_status_avatar !== false);
|
|
||||||
const aiAssistantName = computed(() => {
|
const aiAssistantName = computed(() => {
|
||||||
if (!personalization.form.use_custom_names) {
|
if (!personalization.form.use_custom_names) {
|
||||||
return 'AI Assistant';
|
return 'AI Assistant';
|
||||||
@ -748,6 +724,7 @@ interface UserBubbleFoldState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const USER_BUBBLE_FOLD_LINES = 10;
|
const USER_BUBBLE_FOLD_LINES = 10;
|
||||||
|
const DEFAULT_GENERATING_TEXT = '生成中…';
|
||||||
const userBubbleRefs = new Map<string, HTMLElement>();
|
const userBubbleRefs = new Map<string, HTMLElement>();
|
||||||
const userBubbleFoldStates = ref<Record<string, UserBubbleFoldState>>({});
|
const userBubbleFoldStates = ref<Record<string, UserBubbleFoldState>>({});
|
||||||
const copiedBubbleKeys = ref<Set<string>>(new Set());
|
const copiedBubbleKeys = ref<Set<string>>(new Set());
|
||||||
@ -1034,80 +1011,6 @@ const filteredMessages = computed(() => {
|
|||||||
const getFilteredMessagesSafe = () =>
|
const getFilteredMessagesSafe = () =>
|
||||||
Array.isArray(filteredMessages.value) ? filteredMessages.value : [];
|
Array.isArray(filteredMessages.value) ? filteredMessages.value : [];
|
||||||
const latestMessageIndex = computed(() => getFilteredMessagesSafe().length - 1);
|
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());
|
const nowMs = ref(Date.now());
|
||||||
let timerHandle: number | null = null;
|
let timerHandle: number | null = null;
|
||||||
const debugLoggedSystemActionKeys = new Set<string>();
|
const debugLoggedSystemActionKeys = new Set<string>();
|
||||||
@ -2008,7 +1911,6 @@ onBeforeUnmount(() => {
|
|||||||
clearInterval(timerHandle);
|
clearInterval(timerHandle);
|
||||||
timerHandle = null;
|
timerHandle = null;
|
||||||
}
|
}
|
||||||
clearStatusAvatarPokeText();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isStackable = (action: any) =>
|
const isStackable = (action: any) =>
|
||||||
@ -2083,6 +1985,14 @@ const splitActionGroups = (actions: any[] = [], messageIndex = 0) => {
|
|||||||
return result;
|
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({
|
defineExpose({
|
||||||
rootEl,
|
rootEl,
|
||||||
getThinkingRef,
|
getThinkingRef,
|
||||||
|
|||||||
@ -83,6 +83,41 @@
|
|||||||
@select="selectFileAtItemByIndex"
|
@select="selectFileAtItemByIndex"
|
||||||
@hover="fileAtActiveIndex = $event"
|
@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">
|
<transition name="floating-status-motion">
|
||||||
<div v-if="floatingStatusVisible" class="floating-project-status" @click.stop>
|
<div v-if="floatingStatusVisible" class="floating-project-status" @click.stop>
|
||||||
<span v-if="projectGitSummaryForRender" class="floating-project-status__left">
|
<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 QuickMenu from '@/components/input/QuickMenu.vue';
|
||||||
import FileAtMenu, { type FileAtItem } from '@/components/input/FileAtMenu.vue';
|
import FileAtMenu, { type FileAtItem } from '@/components/input/FileAtMenu.vue';
|
||||||
import RollingNumber from '@/components/input/RollingNumber.vue';
|
import RollingNumber from '@/components/input/RollingNumber.vue';
|
||||||
|
import StatusAvatar from '@/components/avatar/StatusAvatar.vue';
|
||||||
import { useInputStore } from '@/stores/input';
|
import { useInputStore } from '@/stores/input';
|
||||||
import { usePersonalizationStore } from '@/stores/personalization';
|
import { usePersonalizationStore } from '@/stores/personalization';
|
||||||
|
|
||||||
@ -579,6 +615,13 @@ const props = defineProps<{
|
|||||||
hasPendingRuntimeGuidance?: boolean;
|
hasPendingRuntimeGuidance?: boolean;
|
||||||
terminalCount?: number;
|
terminalCount?: number;
|
||||||
hostMode?: boolean;
|
hostMode?: boolean;
|
||||||
|
avatarStatus?: {
|
||||||
|
mode: string;
|
||||||
|
toolKeys?: string[];
|
||||||
|
toolTexts?: string[];
|
||||||
|
text: string;
|
||||||
|
tracking?: boolean;
|
||||||
|
} | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const inputStore = useInputStore();
|
const inputStore = useInputStore();
|
||||||
@ -638,6 +681,31 @@ const fileAtMenuStyle = ref<Record<string, string>>({});
|
|||||||
const fileAtPickerActive = ref(false);
|
const fileAtPickerActive = ref(false);
|
||||||
let fileAtSearchTimer: number | null = null;
|
let fileAtSearchTimer: number | null = null;
|
||||||
|
|
||||||
|
const avatarPokeText = ref('');
|
||||||
|
let avatarPokeTimer: number | null = null;
|
||||||
|
const STATUS_AVATAR_POKE_TEXTS = [
|
||||||
|
'不要!',
|
||||||
|
'停下来!',
|
||||||
|
'别戳啦!',
|
||||||
|
'哎呀!',
|
||||||
|
'你戳到我了!',
|
||||||
|
'轻一点嘛!',
|
||||||
|
'我不是按钮!',
|
||||||
|
'不许乱点!',
|
||||||
|
'呜……别戳我了……',
|
||||||
|
'六边形也是会痛的!',
|
||||||
|
'再戳我要变形了!',
|
||||||
|
'我会紧张的……',
|
||||||
|
'正在假装镇定……',
|
||||||
|
'Token 掉了一地!',
|
||||||
|
'上下文被你戳乱了!',
|
||||||
|
'防戳模式启动失败',
|
||||||
|
'系统提示:AI 被戳了一下',
|
||||||
|
'警告:检测到手欠行为',
|
||||||
|
'我记住你了!',
|
||||||
|
'戳回去!'
|
||||||
|
];
|
||||||
|
|
||||||
const slashHighlightStyle = computed(() => {
|
const slashHighlightStyle = computed(() => {
|
||||||
if (slashManualScrolled.value) return { display: 'none' };
|
if (slashManualScrolled.value) return { display: 'none' };
|
||||||
const isFirst = skillSlashActiveIndex.value === 0 && skillSlashList.value?.scrollTop === 0;
|
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 projectGitMenuOpen = ref<null | 'project' | 'branch'>(null);
|
||||||
|
|
||||||
const closeProjectGitMenu = () => {
|
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) => {
|
const updateFileAtMenuPosition = (tokenEnd: number) => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const instance = editor.value;
|
const instance = editor.value;
|
||||||
@ -2312,6 +2415,7 @@ const hasRuntimeLayoutExpansion = computed(() => {
|
|||||||
return (
|
return (
|
||||||
hasQueue ||
|
hasQueue ||
|
||||||
floatingStatusVisible.value ||
|
floatingStatusVisible.value ||
|
||||||
|
showComposerAvatar.value ||
|
||||||
skillSlashOpen.value ||
|
skillSlashOpen.value ||
|
||||||
fileAtOpen.value ||
|
fileAtOpen.value ||
|
||||||
hasImages ||
|
hasImages ||
|
||||||
@ -2380,13 +2484,14 @@ const collectComposerVisualHeight = () => {
|
|||||||
// 收起态横幅是脱离流、浮在角上的小圆点,不应计入为聊天区预留的高度,
|
// 收起态横幅是脱离流、浮在角上的小圆点,不应计入为聊天区预留的高度,
|
||||||
// 否则会把消息区可滚动范围顶高。故排除 .goal-mode-banner--collapsed。
|
// 否则会把消息区可滚动范围顶高。故排除 .goal-mode-banner--collapsed。
|
||||||
const nodes = root.querySelectorAll(
|
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) => {
|
nodes.forEach((node) => {
|
||||||
if (!(node instanceof HTMLElement)) return;
|
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('floating-project-status') && !floatingStatusVisible.value) return;
|
||||||
|
if (node.classList.contains('composer-avatar') && !showComposerAvatar.value) return;
|
||||||
top = Math.min(top, node.offsetTop);
|
top = Math.min(top, node.offsetTop);
|
||||||
bottom = Math.max(bottom, node.offsetTop + node.offsetHeight);
|
bottom = Math.max(bottom, node.offsetTop + node.offsetHeight);
|
||||||
});
|
});
|
||||||
@ -2418,7 +2523,7 @@ const emitComposerHeight = () => {
|
|||||||
}
|
}
|
||||||
const baseline = baselineComposerVisualHeight.value || visualHeight;
|
const baseline = baselineComposerVisualHeight.value || visualHeight;
|
||||||
const growth = Math.max(0, visualHeight - baseline);
|
const growth = Math.max(0, visualHeight - baseline);
|
||||||
const baseReservedHeight = 93;
|
const baseReservedHeight = 123;
|
||||||
const reservedHeight = Math.ceil(baseReservedHeight + growth);
|
const reservedHeight = Math.ceil(baseReservedHeight + growth);
|
||||||
emit('composer-height-change', {
|
emit('composer-height-change', {
|
||||||
reservedHeight,
|
reservedHeight,
|
||||||
@ -2755,6 +2860,21 @@ watch(floatingStatusVisible, async () => {
|
|||||||
}, 280);
|
}, 280);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(showComposerAvatar, async () => {
|
||||||
|
await nextTick();
|
||||||
|
emitComposerHeight();
|
||||||
|
window.setTimeout(() => {
|
||||||
|
emitComposerHeight();
|
||||||
|
}, 280);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.avatarStatus?.mode,
|
||||||
|
() => {
|
||||||
|
if (props.avatarStatus?.mode !== 'idle') clearAvatarPokeText();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [props.selectedImages?.length || 0, props.selectedVideos?.length || 0],
|
() => [props.selectedImages?.length || 0, props.selectedVideos?.length || 0],
|
||||||
async () => {
|
async () => {
|
||||||
@ -2831,6 +2951,7 @@ onBeforeUnmount(() => {
|
|||||||
composerResizeObserver.disconnect();
|
composerResizeObserver.disconnect();
|
||||||
composerResizeObserver = null;
|
composerResizeObserver = null;
|
||||||
}
|
}
|
||||||
|
clearAvatarPokeText();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -2855,6 +2976,61 @@ onBeforeUnmount(() => {
|
|||||||
pointer-events: auto;
|
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__left,
|
||||||
.floating-project-status__right {
|
.floating-project-status__right {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@ -126,76 +126,6 @@
|
|||||||
margin-bottom: 0;
|
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 {
|
.messages-bottom-spacer {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: var(--composer-growth-height, 0px);
|
height: var(--composer-growth-height, 0px);
|
||||||
@ -611,6 +541,38 @@
|
|||||||
color: var(--claude-text-secondary);
|
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,
|
.user-message .message-text,
|
||||||
.assistant-message .message-text {
|
.assistant-message .message-text {
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user