Compare commits
5 Commits
737c4d61a2
...
dd5e94b294
| Author | SHA1 | Date | |
|---|---|---|---|
| dd5e94b294 | |||
| 2b2ff44fcd | |||
| 0a4c75ce45 | |||
| 5ac17cd279 | |||
| 6f47221ed8 |
@ -4,6 +4,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
LANG=en_US.UTF-8 \
|
LANG=en_US.UTF-8 \
|
||||||
LC_ALL=en_US.UTF-8
|
LC_ALL=en_US.UTF-8
|
||||||
|
|
||||||
|
# 1. 安装系统工具与运行时库
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
bash \
|
bash \
|
||||||
@ -18,15 +19,54 @@ RUN apt-get update && \
|
|||||||
unzip \
|
unzip \
|
||||||
locales \
|
locales \
|
||||||
tzdata \
|
tzdata \
|
||||||
iputils-ping && \
|
iputils-ping \
|
||||||
|
# Office 转换
|
||||||
|
libreoffice \
|
||||||
|
pandoc \
|
||||||
|
poppler-utils \
|
||||||
|
# 图片/视频/媒体
|
||||||
|
imagemagick \
|
||||||
|
ffmpeg \
|
||||||
|
# OCR
|
||||||
|
tesseract-ocr \
|
||||||
|
tesseract-ocr-chi-sim \
|
||||||
|
tesseract-ocr-chi-tra \
|
||||||
|
# 网页截图 / 自动化
|
||||||
|
chromium \
|
||||||
|
# 开发辅助工具
|
||||||
|
jq \
|
||||||
|
ripgrep \
|
||||||
|
fd-find \
|
||||||
|
tree \
|
||||||
|
# 字体
|
||||||
|
fonts-noto-cjk \
|
||||||
|
fonts-noto-color-emoji \
|
||||||
|
fonts-liberation && \
|
||||||
sed -i 's/# en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen && \
|
sed -i 's/# en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen && \
|
||||||
locale-gen && \
|
locale-gen && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 2. 允许 ImageMagick 处理 PDF(默认被安全策略禁止)
|
||||||
|
RUN if [ -f /etc/ImageMagick-6/policy.xml ]; then \
|
||||||
|
sed -i 's/<policy domain="coder" rights="none" pattern="PDF" \/>/<policy domain="coder" rights="read|write" pattern="PDF" \/>/g' /etc/ImageMagick-6/policy.xml; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. 安装 Node.js 20 LTS(复用 curl,已在上方安装)
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||||
|
apt-get install -y --no-install-recommends nodejs && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
node --version && \
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
# 3.5 全局安装 office skill 需要的 Node 包
|
||||||
|
RUN npm install -g docx pptxgenjs && \
|
||||||
|
npm cache clean --force
|
||||||
|
|
||||||
WORKDIR /opt/workspace
|
WORKDIR /opt/workspace
|
||||||
|
|
||||||
COPY docker/toolbox-requirements.txt /tmp/toolbox-requirements.txt
|
COPY docker/toolbox-requirements.txt /tmp/toolbox-requirements.txt
|
||||||
|
|
||||||
|
# 4. 创建 Python 虚拟环境并安装依赖
|
||||||
RUN python -m venv /opt/agent-venv && \
|
RUN python -m venv /opt/agent-venv && \
|
||||||
/opt/agent-venv/bin/pip install --no-cache-dir --upgrade pip && \
|
/opt/agent-venv/bin/pip install --no-cache-dir --upgrade pip && \
|
||||||
/opt/agent-venv/bin/pip install --no-cache-dir -r /tmp/toolbox-requirements.txt && \
|
/opt/agent-venv/bin/pip install --no-cache-dir -r /tmp/toolbox-requirements.txt && \
|
||||||
@ -34,3 +74,5 @@ RUN python -m venv /opt/agent-venv && \
|
|||||||
|
|
||||||
ENV AGENT_TOOLBOX_VENV=/opt/agent-venv
|
ENV AGENT_TOOLBOX_VENV=/opt/agent-venv
|
||||||
ENV PATH="/opt/agent-venv/bin:${PATH}"
|
ENV PATH="/opt/agent-venv/bin:${PATH}"
|
||||||
|
ENV NODE_PATH="/usr/lib/node_modules"
|
||||||
|
|
||||||
|
|||||||
@ -16,3 +16,9 @@ openpyxl==3.1.5
|
|||||||
python-pptx==1.0.2
|
python-pptx==1.0.2
|
||||||
pdfplumber==0.11.4
|
pdfplumber==0.11.4
|
||||||
PyPDF2==3.0.1
|
PyPDF2==3.0.1
|
||||||
|
# office skill 运行必需
|
||||||
|
defusedxml
|
||||||
|
markitdown[docx]
|
||||||
|
# OCR / PDF 转图片
|
||||||
|
pytesseract
|
||||||
|
pdf2image
|
||||||
|
|||||||
@ -464,6 +464,7 @@ async def _dispatch_completion_user_notice(
|
|||||||
session_data["auto_user_message_payload"] = {
|
session_data["auto_user_message_payload"] = {
|
||||||
**dict(extra_payload or {}),
|
**dict(extra_payload or {}),
|
||||||
**ui_defaults,
|
**ui_defaults,
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
}
|
}
|
||||||
# 轮询客户端通过后续任务事件流回放前置通知(在线客户端已由上面的 socketio 回显覆盖)。
|
# 轮询客户端通过后续任务事件流回放前置通知(在线客户端已由上面的 socketio 回显覆盖)。
|
||||||
if preceding_notices:
|
if preceding_notices:
|
||||||
@ -491,6 +492,7 @@ async def _dispatch_completion_user_notice(
|
|||||||
'conversation_id': conversation_id,
|
'conversation_id': conversation_id,
|
||||||
'task_id': rec.task_id,
|
'task_id': rec.task_id,
|
||||||
'message_source': message_source,
|
'message_source': message_source,
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
**ui_defaults,
|
**ui_defaults,
|
||||||
}
|
}
|
||||||
payload.update(extra_payload)
|
payload.update(extra_payload)
|
||||||
@ -508,6 +510,7 @@ async def _dispatch_completion_user_notice(
|
|||||||
'message': user_message,
|
'message': user_message,
|
||||||
'conversation_id': conversation_id,
|
'conversation_id': conversation_id,
|
||||||
'message_source': message_source,
|
'message_source': message_source,
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
}
|
}
|
||||||
payload.update(_user_message_ui_defaults(message_source, auto_user_message_event=True))
|
payload.update(_user_message_ui_defaults(message_source, auto_user_message_event=True))
|
||||||
payload.update(extra_payload)
|
payload.update(extra_payload)
|
||||||
@ -976,6 +979,7 @@ async def handle_task_with_sender(
|
|||||||
"starts_work": user_message_metadata.get("starts_work"),
|
"starts_work": user_message_metadata.get("starts_work"),
|
||||||
"metadata": user_message_metadata,
|
"metadata": user_message_metadata,
|
||||||
"conversation_id": conversation_id,
|
"conversation_id": conversation_id,
|
||||||
|
"timestamp": (saved_user_message or {}).get("timestamp") or user_work_started_at,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
1
static/icons/git-fork.svg
Normal file
1
static/icons/git-fork.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:1;"><circle cx="12" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9m6 3v3"/></svg>
|
||||||
|
After Width: | Height: | Size: 349 B |
@ -235,6 +235,7 @@
|
|||||||
:format-search-topic="formatSearchTopic"
|
:format-search-topic="formatSearchTopic"
|
||||||
:format-search-time="formatSearchTime"
|
:format-search-time="formatSearchTime"
|
||||||
:format-search-domains="formatSearchDomains"
|
:format-search-domains="formatSearchDomains"
|
||||||
|
:history-loading="historyLoading"
|
||||||
@stick-state-change="handleStickStateChange"
|
@stick-state-change="handleStickStateChange"
|
||||||
@user-scroll-intent="handleUserScrollIntent"
|
@user-scroll-intent="handleUserScrollIntent"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -199,7 +199,7 @@ export const historyMethods = {
|
|||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.scrollToBottom();
|
this.scrollHistoryToBottomInstant();
|
||||||
});
|
});
|
||||||
|
|
||||||
debugLog('历史对话内容显示完成');
|
debugLog('历史对话内容显示完成');
|
||||||
@ -278,13 +278,19 @@ export const historyMethods = {
|
|||||||
if (Array.isArray(videos) && videos.length) {
|
if (Array.isArray(videos) && videos.length) {
|
||||||
historyHasVideos = true;
|
historyHasVideos = true;
|
||||||
}
|
}
|
||||||
|
const rawCreatedAt = message.created_at ?? message.createdAt ?? message.timestamp ?? null;
|
||||||
|
const normalizedCreatedAt =
|
||||||
|
typeof rawCreatedAt === 'number' && rawCreatedAt > 0
|
||||||
|
? new Date(rawCreatedAt * 1000).toISOString()
|
||||||
|
: rawCreatedAt || null;
|
||||||
this.messages.push({
|
this.messages.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: message.content || '',
|
content: message.content || '',
|
||||||
images,
|
images,
|
||||||
videos,
|
videos,
|
||||||
media_refs: Array.isArray(mediaRefs) ? mediaRefs : [],
|
media_refs: Array.isArray(mediaRefs) ? mediaRefs : [],
|
||||||
metadata: message.metadata || {}
|
metadata: message.metadata || {},
|
||||||
|
created_at: normalizedCreatedAt
|
||||||
});
|
});
|
||||||
debugLog('添加用户消息:', message.content?.substring(0, 50) + '...');
|
debugLog('添加用户消息:', message.content?.substring(0, 50) + '...');
|
||||||
} else if (message.role === 'assistant') {
|
} else if (message.role === 'assistant') {
|
||||||
@ -526,7 +532,7 @@ export const historyMethods = {
|
|||||||
|
|
||||||
// 确保滚动到底部
|
// 确保滚动到底部
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.scrollToBottom();
|
this.scrollHistoryToBottomInstant();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const blockCount =
|
const blockCount =
|
||||||
this.$el && this.$el.querySelectorAll
|
this.$el && this.$el.querySelectorAll
|
||||||
|
|||||||
@ -75,6 +75,10 @@ export const messagingMethods = {
|
|||||||
...eventMetadata,
|
...eventMetadata,
|
||||||
media_refs: incomingMediaRefs
|
media_refs: incomingMediaRefs
|
||||||
};
|
};
|
||||||
|
const backendTimestamp = data?.timestamp ?? data?.created_at ?? data?.createdAt ?? null;
|
||||||
|
if (backendTimestamp) {
|
||||||
|
target.created_at = backendTimestamp;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.chatAddUserMessage(
|
this.chatAddUserMessage(
|
||||||
message,
|
message,
|
||||||
@ -122,6 +126,10 @@ export const messagingMethods = {
|
|||||||
media_refs: incomingMediaRefs,
|
media_refs: incomingMediaRefs,
|
||||||
message_source: source
|
message_source: source
|
||||||
};
|
};
|
||||||
|
const backendTimestamp = data?.timestamp ?? data?.created_at ?? data?.createdAt ?? null;
|
||||||
|
if (backendTimestamp) {
|
||||||
|
recentMatch.created_at = backendTimestamp;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.chatAddUserMessage(
|
this.chatAddUserMessage(
|
||||||
message,
|
message,
|
||||||
|
|||||||
@ -148,6 +148,26 @@ export const scrollMethods = {
|
|||||||
80
|
80
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
scrollHistoryToBottomInstant() {
|
||||||
|
this._escapedByUserScroll = false;
|
||||||
|
const chatArea = this.getChatAreaController();
|
||||||
|
if (chatArea && typeof chatArea.stopStickScroll === 'function') {
|
||||||
|
chatArea.stopStickScroll();
|
||||||
|
}
|
||||||
|
const area = this.getMessagesAreaElement();
|
||||||
|
if (!area) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const jump = () => {
|
||||||
|
area.scrollTop = area.scrollHeight;
|
||||||
|
};
|
||||||
|
jump();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
jump();
|
||||||
|
requestAnimationFrame(jump);
|
||||||
|
});
|
||||||
|
this.chatSetScrollState({ userScrolling: false });
|
||||||
|
},
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
uiBounceTrace(
|
uiBounceTrace(
|
||||||
'ui.scrollToBottom:called',
|
'ui.scrollToBottom:called',
|
||||||
|
|||||||
@ -234,7 +234,6 @@ export const socketMethods = {
|
|||||||
throw new Error(`状态接口请求失败: ${statusResponse.status}`);
|
throw new Error(`状态接口请求失败: ${statusResponse.status}`);
|
||||||
}
|
}
|
||||||
const statusData = await statusResponse.json();
|
const statusData = await statusResponse.json();
|
||||||
this.isConnected = true;
|
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
this.projectPath = statusData.project_path || '';
|
this.projectPath = statusData.project_path || '';
|
||||||
this.agentVersion = statusData.version || this.agentVersion;
|
this.agentVersion = statusData.version || this.agentVersion;
|
||||||
@ -296,6 +295,7 @@ export const socketMethods = {
|
|||||||
this.thinkingMode = false;
|
this.thinkingMode = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.isConnected = true;
|
||||||
|
|
||||||
const focusPromise = this.focusFetchFiles();
|
const focusPromise = this.focusFetchFiles();
|
||||||
let treePromise: Promise<any> | null = null;
|
let treePromise: Promise<any> | null = null;
|
||||||
|
|||||||
@ -20,10 +20,19 @@
|
|||||||
<span class="icon icon-sm" :style="iconStyleSafe(userHeaderIconKey(msg))" aria-hidden="true"></span>
|
<span class="icon icon-sm" :style="iconStyleSafe(userHeaderIconKey(msg))" aria-hidden="true"></span>
|
||||||
<span>{{ userHeaderLabel(msg) }}</span>
|
<span>{{ userHeaderLabel(msg) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-text user-bubble-text">
|
<div
|
||||||
|
class="message-text user-bubble-text"
|
||||||
|
:class="{
|
||||||
|
'is-collapsed': isUserBubbleCollapsed(msg, index),
|
||||||
|
'is-expandable': isUserBubbleExpandable(msg, index),
|
||||||
|
'is-history-loading': props.historyLoading || userBubbleTransitionSuppressed
|
||||||
|
}"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="msg.content"
|
v-if="msg.content"
|
||||||
class="bubble-text"
|
class="bubble-text"
|
||||||
|
:class="{ 'is-expanded': isUserBubbleExpanded(msg, index) }"
|
||||||
|
:ref="(el) => registerUserBubbleRef(msg, index, el)"
|
||||||
v-html="renderUserMessageContent(msg.content)"
|
v-html="renderUserMessageContent(msg.content)"
|
||||||
></div>
|
></div>
|
||||||
<div v-if="msg.images && msg.images.length" class="image-inline-row">
|
<div v-if="msg.images && msg.images.length" class="image-inline-row">
|
||||||
@ -52,6 +61,34 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isUserBubbleExpandable(msg, index)"
|
||||||
|
class="user-bubble-expand-row"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="user-bubble-action-btn user-bubble-expand-btn"
|
||||||
|
:class="{ expanded: isUserBubbleExpanded(msg, index) }"
|
||||||
|
:title="isUserBubbleExpanded(msg, index) ? '收起' : '展开'"
|
||||||
|
:aria-label="isUserBubbleExpanded(msg, index) ? '收起' : '展开'"
|
||||||
|
@click.stop="toggleUserBubble(msg, index)"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-bubble-actions">
|
||||||
|
<button
|
||||||
|
class="user-bubble-action-btn copy"
|
||||||
|
:class="{ copied: isUserBubbleCopied(msg, index) }"
|
||||||
|
:title="isUserBubbleCopied(msg, index) ? '已复制' : '复制'"
|
||||||
|
:aria-label="isUserBubbleCopied(msg, index) ? '已复制' : '复制'"
|
||||||
|
@click="copyUserMessage(msg, index)"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
class="user-bubble-action-btn branch"
|
||||||
|
title="分支"
|
||||||
|
aria-label="分支"
|
||||||
|
@click="branchUserMessage(msg)"
|
||||||
|
></button>
|
||||||
|
<span class="user-bubble-time">{{ formatUserMessageTime(msg) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -605,7 +642,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
|
||||||
import { useStickToBottom } from 'vue-stick-to-bottom';
|
import { useStickToBottom } from 'vue-stick-to-bottom';
|
||||||
import { useBlockExpansionAnchor } from '@/composables/useBlockExpansionAnchor';
|
import { useBlockExpansionAnchor } from '@/composables/useBlockExpansionAnchor';
|
||||||
import ToolAction from '@/components/chat/actions/ToolAction.vue';
|
import ToolAction from '@/components/chat/actions/ToolAction.vue';
|
||||||
@ -630,6 +667,7 @@ const props = defineProps<{
|
|||||||
formatSearchTopic: (filters: Record<string, any>) => string;
|
formatSearchTopic: (filters: Record<string, any>) => string;
|
||||||
formatSearchTime: (filters: Record<string, any>) => string;
|
formatSearchTime: (filters: Record<string, any>) => string;
|
||||||
formatSearchDomains: (filters: Record<string, any>) => string;
|
formatSearchDomains: (filters: Record<string, any>) => string;
|
||||||
|
historyLoading?: boolean;
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(
|
(
|
||||||
@ -666,6 +704,184 @@ const userName = computed(() => {
|
|||||||
}
|
}
|
||||||
return personalization.form.user_name || '用户';
|
return personalization.form.user_name || '用户';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 用户输入气泡折叠相关状态与逻辑
|
||||||
|
interface UserBubbleFoldState {
|
||||||
|
needsFold: boolean;
|
||||||
|
expanded: boolean;
|
||||||
|
foldHeight: number;
|
||||||
|
fullHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const USER_BUBBLE_FOLD_LINES = 10;
|
||||||
|
const userBubbleRefs = new Map<string, HTMLElement>();
|
||||||
|
const userBubbleFoldStates = ref<Record<string, UserBubbleFoldState>>({});
|
||||||
|
const copiedBubbleKeys = ref<Set<string>>(new Set());
|
||||||
|
const userBubbleTransitionSuppressed = ref(false);
|
||||||
|
let userBubbleResizeObserver: ResizeObserver | null = null;
|
||||||
|
let copiedBubbleTimeouts = new Map<string, number>();
|
||||||
|
let userBubbleTransitionSeq = 0;
|
||||||
|
|
||||||
|
const getUserBubbleKey = (msg: any, index: number) => msg?.id || `user-bubble-${index}`;
|
||||||
|
|
||||||
|
const registerUserBubbleRef = (msg: any, index: number, el: Element | null) => {
|
||||||
|
const key = getUserBubbleKey(msg, index);
|
||||||
|
if (el instanceof HTMLElement) {
|
||||||
|
userBubbleRefs.set(key, el);
|
||||||
|
} else {
|
||||||
|
userBubbleRefs.delete(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const estimateUserBubbleNeedsFold = (msg: any): boolean => {
|
||||||
|
const content = typeof msg?.content === 'string' ? msg.content : '';
|
||||||
|
if (!content) return false;
|
||||||
|
// 按常见桌面端气泡宽度和字号估算:每行约 48 个字符。
|
||||||
|
// 预留余量,避免短气泡被误判为需要折叠而出现“先折叠后展开”的跳变。
|
||||||
|
const APPROX_CHARS_PER_LINE = 48;
|
||||||
|
const foldChars = APPROX_CHARS_PER_LINE * (USER_BUBBLE_FOLD_LINES + 2);
|
||||||
|
const newlineCount = (content.match(/\n/g) || []).length;
|
||||||
|
return content.length > foldChars || newlineCount >= USER_BUBBLE_FOLD_LINES + 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
const preMeasureUserBubbles = () => {
|
||||||
|
// 在 DOM 测量前,先根据文本内容同步估算是否需要折叠,
|
||||||
|
// 避免长气泡先完整渲染再被收起。
|
||||||
|
// 只给尚未有状态记录的气泡做首次估算,避免切换对话时旧消息反复重算导致动画。
|
||||||
|
filteredMessages.value.forEach((msg, index) => {
|
||||||
|
if (!msg || msg.role !== 'user') return;
|
||||||
|
const key = getUserBubbleKey(msg, index);
|
||||||
|
if (userBubbleFoldStates.value[key]) return;
|
||||||
|
const needsFold = estimateUserBubbleNeedsFold(msg);
|
||||||
|
userBubbleFoldStates.value[key] = {
|
||||||
|
needsFold,
|
||||||
|
expanded: false,
|
||||||
|
foldHeight: 0,
|
||||||
|
fullHeight: 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const measureUserBubbles = () => {
|
||||||
|
userBubbleRefs.forEach((el, key) => {
|
||||||
|
if (!el.isConnected) {
|
||||||
|
userBubbleRefs.delete(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const computed = window.getComputedStyle(el);
|
||||||
|
const rawLineHeight = parseFloat(computed.lineHeight);
|
||||||
|
const lineHeight = Number.isFinite(rawLineHeight) ? rawLineHeight : 24;
|
||||||
|
const foldHeight = Math.round(lineHeight * USER_BUBBLE_FOLD_LINES);
|
||||||
|
const fullHeight = Math.ceil(el.scrollHeight);
|
||||||
|
const needsFold = fullHeight > foldHeight + 1;
|
||||||
|
const existing = userBubbleFoldStates.value[key];
|
||||||
|
userBubbleFoldStates.value[key] = {
|
||||||
|
needsFold,
|
||||||
|
expanded: existing?.expanded ?? false,
|
||||||
|
foldHeight,
|
||||||
|
fullHeight
|
||||||
|
};
|
||||||
|
el.style.setProperty('--bubble-fold-height', `${foldHeight}px`);
|
||||||
|
el.style.setProperty('--bubble-full-height', `${fullHeight}px`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isUserBubbleExpandable = (msg: any, index: number) => {
|
||||||
|
const key = getUserBubbleKey(msg, index);
|
||||||
|
return !!userBubbleFoldStates.value[key]?.needsFold;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isUserBubbleExpanded = (msg: any, index: number) => {
|
||||||
|
const key = getUserBubbleKey(msg, index);
|
||||||
|
return !!userBubbleFoldStates.value[key]?.expanded;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isUserBubbleCollapsed = (msg: any, index: number) => {
|
||||||
|
const key = getUserBubbleKey(msg, index);
|
||||||
|
const state = userBubbleFoldStates.value[key];
|
||||||
|
return !!state?.needsFold && !state?.expanded;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleUserBubble = (msg: any, index: number) => {
|
||||||
|
const key = getUserBubbleKey(msg, index);
|
||||||
|
const state = userBubbleFoldStates.value[key];
|
||||||
|
if (!state || !state.needsFold) return;
|
||||||
|
userBubbleFoldStates.value[key] = { ...state, expanded: !state.expanded };
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatUserMessageTime = (msg: any): string => {
|
||||||
|
const createdAt = msg?.created_at;
|
||||||
|
if (!createdAt) return '';
|
||||||
|
const date = new Date(createdAt);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = Math.max(0, now.getTime() - date.getTime());
|
||||||
|
const diffSec = Math.floor(diffMs / 1000);
|
||||||
|
const diffMin = Math.floor(diffSec / 60);
|
||||||
|
const diffHour = Math.floor(diffMin / 60);
|
||||||
|
const diffDay = Math.floor(diffHour / 24);
|
||||||
|
|
||||||
|
if (diffDay >= 1) {
|
||||||
|
const month = date.getMonth() + 1;
|
||||||
|
const day = date.getDate();
|
||||||
|
const hour = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minute = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return `${month}月${day}日 ${hour}:${minute}`;
|
||||||
|
}
|
||||||
|
if (diffHour > 0) return `${diffHour}小时前`;
|
||||||
|
if (diffMin > 0) return `${diffMin}分钟前`;
|
||||||
|
if (diffSec > 0) return `${diffSec}秒前`;
|
||||||
|
return '刚刚';
|
||||||
|
};
|
||||||
|
|
||||||
|
const isUserBubbleCopied = (msg: any, index: number) => {
|
||||||
|
const key = getUserBubbleKey(msg, index);
|
||||||
|
return copiedBubbleKeys.value.has(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyUserMessage = async (msg: any, index: number) => {
|
||||||
|
const key = getUserBubbleKey(msg, index);
|
||||||
|
const text = typeof msg?.content === 'string' ? msg.content : '';
|
||||||
|
if (!text || copiedBubbleKeys.value.has(key)) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
copiedBubbleKeys.value.add(key);
|
||||||
|
const existing = copiedBubbleTimeouts.get(key);
|
||||||
|
if (existing) window.clearTimeout(existing);
|
||||||
|
const timeout = window.setTimeout(() => {
|
||||||
|
copiedBubbleKeys.value.delete(key);
|
||||||
|
copiedBubbleTimeouts.delete(key);
|
||||||
|
}, 5000);
|
||||||
|
copiedBubbleTimeouts.set(key, timeout);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('复制用户消息失败:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const branchUserMessage = (_msg: any) => {
|
||||||
|
// TODO: 分支功能占位
|
||||||
|
void _msg;
|
||||||
|
console.log('[ChatArea] 分支功能待实现');
|
||||||
|
};
|
||||||
|
|
||||||
|
const observeUserBubbles = () => {
|
||||||
|
if (!userBubbleResizeObserver) return;
|
||||||
|
userBubbleResizeObserver.disconnect();
|
||||||
|
userBubbleRefs.forEach((el) => {
|
||||||
|
if (el.isConnected) userBubbleResizeObserver!.observe(el);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const releaseUserBubbleTransitionAfterLayout = () => {
|
||||||
|
const seq = ++userBubbleTransitionSeq;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (seq === userBubbleTransitionSeq) {
|
||||||
|
userBubbleTransitionSuppressed.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const isSystemAutoUserMessage = (message: any) => {
|
const isSystemAutoUserMessage = (message: any) => {
|
||||||
if (!message || message.role !== 'user') {
|
if (!message || message.role !== 'user') {
|
||||||
return false;
|
return false;
|
||||||
@ -1603,6 +1819,23 @@ function compactBriefLabel(msg: any): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => filteredMessages.value.map((m) => m?.id ?? m?.content?.slice(0, 80)),
|
||||||
|
() => {
|
||||||
|
userBubbleTransitionSuppressed.value = true;
|
||||||
|
// 先同步估算折叠状态,再异步精确测量,避免长气泡先展开后收起
|
||||||
|
preMeasureUserBubbles();
|
||||||
|
nextTick(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
measureUserBubbles();
|
||||||
|
observeUserBubbles();
|
||||||
|
releaseUserBubbleTransitionAfterLayout();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
attachBounceListener();
|
attachBounceListener();
|
||||||
updateChatScrollbarWidth();
|
updateChatScrollbarWidth();
|
||||||
@ -1610,6 +1843,12 @@ onMounted(() => {
|
|||||||
scrollbarResizeObserver = new ResizeObserver(updateChatScrollbarWidth);
|
scrollbarResizeObserver = new ResizeObserver(updateChatScrollbarWidth);
|
||||||
scrollbarResizeObserver.observe(scrollRef.value);
|
scrollbarResizeObserver.observe(scrollRef.value);
|
||||||
}
|
}
|
||||||
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
|
userBubbleResizeObserver = new ResizeObserver(() => {
|
||||||
|
requestAnimationFrame(measureUserBubbles);
|
||||||
|
});
|
||||||
|
nextTick(() => observeUserBubbles());
|
||||||
|
}
|
||||||
console.warn('[SCROLL_BOUNCE_TRACE]', 'mounted', {
|
console.warn('[SCROLL_BOUNCE_TRACE]', 'mounted', {
|
||||||
hasScrollRef: !!scrollRef.value
|
hasScrollRef: !!scrollRef.value
|
||||||
});
|
});
|
||||||
@ -1638,6 +1877,13 @@ onBeforeUnmount(() => {
|
|||||||
scrollbarResizeObserver.disconnect();
|
scrollbarResizeObserver.disconnect();
|
||||||
scrollbarResizeObserver = null;
|
scrollbarResizeObserver = null;
|
||||||
}
|
}
|
||||||
|
if (userBubbleResizeObserver) {
|
||||||
|
userBubbleResizeObserver.disconnect();
|
||||||
|
userBubbleResizeObserver = null;
|
||||||
|
}
|
||||||
|
userBubbleRefs.clear();
|
||||||
|
copiedBubbleTimeouts.forEach((t) => window.clearTimeout(t));
|
||||||
|
copiedBubbleTimeouts.clear();
|
||||||
if (timerHandle !== null) {
|
if (timerHandle !== null) {
|
||||||
clearInterval(timerHandle);
|
clearInterval(timerHandle);
|
||||||
timerHandle = null;
|
timerHandle = null;
|
||||||
|
|||||||
@ -966,6 +966,10 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
...eventMetadata,
|
...eventMetadata,
|
||||||
media_refs: incomingMediaRefs
|
media_refs: incomingMediaRefs
|
||||||
};
|
};
|
||||||
|
const backendTimestamp = data?.timestamp ?? data?.created_at ?? data?.createdAt ?? null;
|
||||||
|
if (backendTimestamp) {
|
||||||
|
last.created_at = backendTimestamp;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.chatAddUserMessage(message, incomingImages, incomingVideos, incomingMediaRefs, source, eventMetadata);
|
ctx.chatAddUserMessage(message, incomingImages, incomingVideos, incomingMediaRefs, source, eventMetadata);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -210,7 +210,8 @@ export const useChatStore = defineStore('chat', {
|
|||||||
images,
|
images,
|
||||||
videos,
|
videos,
|
||||||
media_refs: Array.isArray(mediaRefs) ? mediaRefs : [],
|
media_refs: Array.isArray(mediaRefs) ? mediaRefs : [],
|
||||||
metadata
|
metadata,
|
||||||
|
created_at: startedAt
|
||||||
});
|
});
|
||||||
this.currentMessageIndex = -1;
|
this.currentMessageIndex = -1;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -46,6 +46,22 @@ export const useModelStore = defineStore('model', {
|
|||||||
const exists = this.models.some((m) => m.key === key);
|
const exists = this.models.some((m) => m.key === key);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
this.currentModelKey = key;
|
this.currentModelKey = key;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 如果列表中还没有该模型(例如 fetchModels 失败),
|
||||||
|
// 临时插入一个 fallback 项,避免首屏显示“未选择模型”
|
||||||
|
if (key) {
|
||||||
|
this.models = [
|
||||||
|
...this.models,
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
label: key,
|
||||||
|
description: '',
|
||||||
|
fastOnly: false,
|
||||||
|
supportsThinking: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
this.currentModelKey = key;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async fetchModels() {
|
async fetchModels() {
|
||||||
|
|||||||
@ -98,6 +98,7 @@ const DEFAULT_DEEP_COMPRESS_TRIGGER_TOKENS = 150000;
|
|||||||
const RUN_MODE_OPTIONS: RunMode[] = ['fast', 'thinking', 'deep'];
|
const RUN_MODE_OPTIONS: RunMode[] = ['fast', 'thinking', 'deep'];
|
||||||
const EXPERIMENT_STORAGE_KEY = 'agents_personalization_experiments';
|
const EXPERIMENT_STORAGE_KEY = 'agents_personalization_experiments';
|
||||||
const THEME_STORAGE_KEY = 'agents_ui_theme';
|
const THEME_STORAGE_KEY = 'agents_ui_theme';
|
||||||
|
const STACKED_HIDE_BORDERS_STORAGE_KEY = 'agents_stacked_hide_borders';
|
||||||
|
|
||||||
const loadCachedTheme = (): PersonalForm['theme'] => {
|
const loadCachedTheme = (): PersonalForm['theme'] => {
|
||||||
if (typeof window === 'undefined' || !window.localStorage) {
|
if (typeof window === 'undefined' || !window.localStorage) {
|
||||||
@ -107,6 +108,24 @@ const loadCachedTheme = (): PersonalForm['theme'] => {
|
|||||||
return saved === 'light' || saved === 'dark' || saved === 'classic' ? saved : 'classic';
|
return saved === 'light' || saved === 'dark' || saved === 'classic' ? saved : 'classic';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadCachedStackedHideBorders = (): boolean => {
|
||||||
|
if (typeof window === 'undefined' || !window.localStorage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return window.localStorage.getItem(STACKED_HIDE_BORDERS_STORAGE_KEY) === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistStackedHideBorders = (value: boolean) => {
|
||||||
|
if (typeof window === 'undefined' || !window.localStorage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(STACKED_HIDE_BORDERS_STORAGE_KEY, value ? 'true' : 'false');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('写入堆叠块边线设置失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const defaultForm = (): PersonalForm => ({
|
const defaultForm = (): PersonalForm => ({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
communication_style: 'default',
|
communication_style: 'default',
|
||||||
@ -125,7 +144,7 @@ const defaultForm = (): PersonalForm => ({
|
|||||||
compact_message_display: 'full',
|
compact_message_display: 'full',
|
||||||
show_git_status_bar: true,
|
show_git_status_bar: true,
|
||||||
auto_open_terminal_panel: true,
|
auto_open_terminal_panel: true,
|
||||||
stacked_hide_borders: false,
|
stacked_hide_borders: loadCachedStackedHideBorders(),
|
||||||
enhanced_tool_display_categories: [],
|
enhanced_tool_display_categories: [],
|
||||||
enabled_skills: [],
|
enabled_skills: [],
|
||||||
self_identify: '',
|
self_identify: '',
|
||||||
@ -398,6 +417,7 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
this.applyTheme(this.form.theme);
|
this.applyTheme(this.form.theme);
|
||||||
}
|
}
|
||||||
persistDefaultHideWorkspace(this.form.default_hide_workspace);
|
persistDefaultHideWorkspace(this.form.default_hide_workspace);
|
||||||
|
persistStackedHideBorders(this.form.stacked_hide_borders);
|
||||||
if (this.form.default_hide_workspace) {
|
if (this.form.default_hide_workspace) {
|
||||||
useUiStore().setWorkspaceCollapsed(true);
|
useUiStore().setWorkspaceCollapsed(true);
|
||||||
}
|
}
|
||||||
@ -593,6 +613,9 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
...this.form,
|
...this.form,
|
||||||
[payload.key]: payload.value
|
[payload.key]: payload.value
|
||||||
};
|
};
|
||||||
|
if (payload.key === 'stacked_hide_borders') {
|
||||||
|
persistStackedHideBorders(!!payload.value);
|
||||||
|
}
|
||||||
this.clearFeedback();
|
this.clearFeedback();
|
||||||
this.scheduleAutoSave();
|
this.scheduleAutoSave();
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,6 +13,13 @@ body {
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font-family: "SF Pro Text", "Helvetica Neue", ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
body[data-theme='dark'] {
|
body[data-theme='dark'] {
|
||||||
background: var(--claude-bg);
|
background: var(--claude-bg);
|
||||||
color: var(--claude-text);
|
color: var(--claude-text);
|
||||||
|
|||||||
@ -576,6 +576,147 @@
|
|||||||
margin: 0 0.25em;
|
margin: 0 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 用户输入气泡:超长折叠、展开动画与悬停操作按钮 */
|
||||||
|
.user-message .message-text.user-bubble-text .bubble-text {
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
max-height: var(--bubble-full-height, 9999px);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message .message-text.user-bubble-text.is-history-loading .bubble-text {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message .message-text.user-bubble-text.is-expandable .bubble-text:not(.is-expanded) {
|
||||||
|
max-height: var(--bubble-fold-height, 240px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 展开/收起按钮独占一行,不遮挡气泡内容,始终显示 */
|
||||||
|
.user-bubble-expand-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-bottom: -10px;
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
pointer-events: auto;
|
||||||
|
align-self: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-bubble-expand-row .user-bubble-expand-btn {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
pointer-events: auto;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--claude-text-tertiary);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: color 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-bubble-expand-row .user-bubble-expand-btn:hover {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--claude-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-bubble-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 6px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(4px);
|
||||||
|
pointer-events: none;
|
||||||
|
transition:
|
||||||
|
opacity 180ms ease,
|
||||||
|
transform 180ms ease;
|
||||||
|
white-space: normal;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-bubble-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--claude-text-secondary);
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 8px;
|
||||||
|
line-height: 20px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message:not(.user-message--compact):not(.user-message--brief):hover .user-bubble-actions,
|
||||||
|
.user-message:not(.user-message--compact):not(.user-message--brief):focus-within .user-bubble-actions {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-bubble-action-btn {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--claude-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition:
|
||||||
|
background-color 140ms ease,
|
||||||
|
color 140ms ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-bubble-action-btn:hover {
|
||||||
|
background-color: var(--theme-tab-active);
|
||||||
|
color: var(--claude-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-bubble-action-btn::before {
|
||||||
|
content: '';
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: block;
|
||||||
|
background: currentColor;
|
||||||
|
mask: center / contain no-repeat;
|
||||||
|
-webkit-mask: center / contain no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-bubble-action-btn.copy::before {
|
||||||
|
mask-image: url('/static/icons/copy.svg');
|
||||||
|
-webkit-mask-image: url('/static/icons/copy.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-bubble-action-btn.copy.copied::before {
|
||||||
|
mask-image: url('/static/icons/check.svg');
|
||||||
|
-webkit-mask-image: url('/static/icons/check.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-bubble-action-btn.branch::before {
|
||||||
|
mask-image: url('/static/icons/git-fork.svg');
|
||||||
|
-webkit-mask-image: url('/static/icons/git-fork.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-bubble-action-btn.user-bubble-expand-btn::before {
|
||||||
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||||||
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||||||
|
transition: transform 260ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-bubble-action-btn.user-bubble-expand-btn.expanded::before {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
.assistant-message .message-text {
|
.assistant-message .message-text {
|
||||||
background: var(--claude-highlight);
|
background: var(--claude-highlight);
|
||||||
border-left: 4px solid var(--claude-accent);
|
border-left: 4px solid var(--claude-accent);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user