feat: 新增极简模式(Minimal Mode)

新增了一个全新的消息显示模式,提供更简洁的对话体验。

主要功能:
- 新增 MinimalBlocks.vue 组件,实现极简风格的消息块展示
- 在 ChatArea.vue 中集成极简模式渲染逻辑
- 在个性化设置中添加"极简模式"选项(blockDisplayMode)
- 支持三种显示模式:堆叠模式、极简模式、传统模式

技术实现:
- 使用 personalization store 管理显示模式状态
- 通过 computed 属性动态切换渲染组件
- 保持与现有堆叠模式和传统模式的兼容性

用户体验:
- 更清爽的界面布局
- 减少视觉干扰
- 适合快速浏览对话内容

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
JOJO 2026-04-04 14:47:45 +08:00
parent b474a69b3b
commit 321818f7ef
5 changed files with 684 additions and 37 deletions

View File

@ -49,7 +49,20 @@
</div>
</div>
</div>
<template v-if="stackedBlocksEnabled">
<template v-if="blockDisplayMode === 'minimal'">
<MinimalBlocks
:actions="msg.actions || []"
:icon-style="iconStyleSafe"
:get-tool-icon="getToolIcon"
:get-tool-status-text="getToolStatusText"
:get-tool-description="getToolDescription"
:format-search-topic="formatSearchTopic"
:format-search-time="formatSearchTime"
:format-search-domains="formatSearchDomains"
:render-markdown="renderMarkdown"
/>
</template>
<template v-else-if="stackedBlocksEnabled">
<template v-for="(group, groupIndex) in splitActionGroups(msg.actions || [], index)" :key="group.key">
<StackedBlocks
v-if="group.kind === 'stack'"
@ -365,6 +378,7 @@
import { computed, ref } from 'vue';
import ToolAction from '@/components/chat/actions/ToolAction.vue';
import StackedBlocks from './StackedBlocks.vue';
import MinimalBlocks from './MinimalBlocks.vue';
import { usePersonalizationStore } from '@/stores/personalization';
const props = defineProps<{
@ -385,10 +399,12 @@ const props = defineProps<{
}>();
const personalization = usePersonalizationStore();
const blockDisplayMode = computed(() => {
return personalization.experiments.blockDisplayMode || 'stacked';
});
const stackedBlocksEnabled = computed(() => {
// false
const enabled = personalization.experiments.stackedBlocksEnabled;
return enabled !== false;
// 使
return blockDisplayMode.value === 'stacked' || blockDisplayMode.value === 'minimal';
});
const aiAssistantName = computed(() => {
if (!personalization.form.use_custom_names) {

View File

@ -0,0 +1,609 @@
<template>
<div class="minimal-blocks-container">
<!-- 等待动画 -->
<div v-if="showGenerating" class="generating-placeholder">
<span v-for="(letter, letterIdx) in '思考中...'.split('')" :key="letterIdx" class="letter" :style="{ animationDelay: `${letterIdx * 0.1}s` }">
{{ letter }}
</span>
</div>
<!-- 按真实顺序显示分组 -->
<template v-for="group in blockGroups" :key="group.id">
<!-- 摘要组thinking/tool -->
<div v-if="group.type === 'summary' && group.actions" class="summary-group">
<!-- 摘要行只有文字可点击展开 -->
<div
class="summary-line-text"
:class="{ running: isSummaryRunning(group.actions, group.id) }"
@click="toggleExpand(group.id)"
>
<div class="summary-content-wrapper">
<span class="summary-preview">{{ getSummaryPreview(group.actions) }}</span>
</div>
<svg class="expand-icon" :class="{ expanded: expandedGroups.has(group.id) }" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
<!-- 步骤容器展开时显示所有步骤 -->
<div class="steps-container" :class="{ show: expandedGroups.has(group.id) }">
<div class="steps-wrapper">
<div
v-for="(step, idx) in getSummarySteps(group.actions)"
:key="step.id"
class="step-item"
>
<div class="step-timeline">
<svg class="step-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<g v-html="getStepIcon(step)"></g>
</svg>
<div class="step-line"></div>
</div>
<div class="step-content">
<div v-if="step.type === 'thinking'" class="step-body">
<div class="thinking-content" v-html="renderContent(step.content)"></div>
</div>
<div v-else-if="step.type === 'tool'" class="step-body">
<div class="step-header">{{ getToolName(step.action) }}</div>
<div class="tool-intent" v-if="getToolIntent(step.action)">{{ getToolIntent(step.action) }}</div>
<div class="tool-result" v-if="step.action.tool?.result">
<div v-html="renderToolResult(step.action)"></div>
</div>
<div v-else class="tool-result">
<div class="result-item">执行中...</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 文本组 -->
<div v-else-if="group.type === 'text'" class="text-output" :class="{ 'streaming-text': group.streaming }">
<div class="text-content" :class="{ 'streaming-text': group.streaming }">
<div v-if="group.streaming" v-html="renderMarkdown(group.content || '', true)"></div>
<div v-else v-html="renderMarkdown(group.content || '', false)"></div>
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue';
import { usePersonalizationStore } from '@/stores/personalization';
import { renderEnhancedToolResult } from './actions/toolRenderers';
interface Action {
id?: string;
type: 'thinking' | 'tool' | 'text' | 'system';
content: string;
streaming?: boolean;
blockId?: string;
tool?: {
name?: string;
intent?: string;
arguments?: any;
result?: any;
};
}
interface Step {
id: string;
type: 'thinking' | 'tool';
content: string;
streaming: boolean;
action: Action;
}
interface BlockGroup {
type: 'summary' | 'text';
id: string;
actions?: Action[];
content?: string;
streaming?: boolean;
}
const props = defineProps<{
actions: Action[];
iconStyle?: (name: string) => any;
getToolIcon?: (tool: any) => string;
getToolStatusText?: (tool: any) => string;
getToolDescription?: (tool: any) => string;
formatSearchTopic?: (filters: Record<string, any>) => string;
formatSearchTime?: (filters: Record<string, any>) => string;
formatSearchDomains?: (filters: Record<string, any>) => string;
renderMarkdown?: (content: string, isStreaming: boolean) => string;
}>();
const personalizationStore = usePersonalizationStore();
const expandedGroups = ref(new Set<string>());
// actions thinking/tool text
const blockGroups = computed(() => {
const groups: BlockGroup[] = [];
let currentGroup: Action[] = [];
let groupIndex = 0;
props.actions.forEach((action, idx) => {
if (action.type === 'thinking' || action.type === 'tool') {
currentGroup.push(action);
} else if (action.type === 'text') {
// thinking/tool
if (currentGroup.length > 0) {
groups.push({
type: 'summary',
id: `summary-${groupIndex++}`,
actions: currentGroup
});
currentGroup = [];
}
// text
groups.push({
type: 'text',
id: `text-${idx}`,
content: action.content,
streaming: action.streaming
});
}
});
// thinking/tool
if (currentGroup.length > 0) {
groups.push({
type: 'summary',
id: `summary-${groupIndex}`,
actions: currentGroup
});
}
return groups;
});
//
const showGenerating = computed(() => {
const hasAnyThinkingOrTool = props.actions.some(a => a.type === 'thinking' || a.type === 'tool');
const isStreaming = props.actions.some(a => a.streaming && (a.type === 'thinking' || a.type === 'tool'));
return isStreaming && !hasAnyThinkingOrTool;
});
// /
const getSummaryPreview = (actions: Action[]) => {
// streaming
const streamingActions = actions.filter(a => a.streaming);
const currentStep = streamingActions.length > 0
? streamingActions[streamingActions.length - 1]
: actions[actions.length - 1]; // streaming
if (!currentStep) return '';
if (currentStep.type === 'thinking') {
// 50
const content = currentStep.content || '';
const firstLineEnd = content.indexOf('\n');
const textToShow = firstLineEnd > 0 ? content.substring(0, firstLineEnd) : content;
return textToShow.length > 50 ? textToShow.substring(0, 50) + '...' : textToShow;
} else if (currentStep.type === 'tool') {
// intent_rendered intent_full
const tool = currentStep.tool;
if (!tool) return '';
const intentEnabled = personalizationStore.form.tool_intent_enabled;
const intentText = tool.intent_rendered || tool.intent_full || '';
if (intentEnabled && intentText) {
// intentintentintent
const firstLineEnd = intentText.indexOf('\n');
const textToShow = firstLineEnd > 0 ? intentText.substring(0, firstLineEnd) : intentText;
return textToShow.length > 50 ? textToShow.substring(0, 50) + '...' : textToShow;
}
// intentintent
if (tool.status === 'preparing') {
return `准备调用 ${tool.name || '工具'}...`;
}
if (tool.status === 'running') {
return `正在调用 ${tool.name || '工具'}...`;
}
if (tool.status === 'completed') {
return '工具执行完成';
}
return currentStep.streaming ? '正在执行工具...' : '工具执行完成';
}
return '';
};
//
const isSummaryRunning = (actions: Action[], groupId: string) => {
// group blockGroups
const currentIndex = blockGroups.value.findIndex(g => g.id === groupId);
if (currentIndex === -1) return false;
//
const hasTextAfter = blockGroups.value.slice(currentIndex + 1).some(g => g.type === 'text');
// summary group
if (hasTextAfter) return false;
// summary group
return true;
};
//
const getSummarySteps = (actions: Action[]) => {
return actions.map((action, idx) => ({
id: action.id || action.blockId || `step-${idx}`,
type: action.type as 'thinking' | 'tool',
content: action.content || '',
streaming: action.streaming || false,
action
}));
};
const toggleExpand = (groupId: string) => {
if (expandedGroups.value.has(groupId)) {
expandedGroups.value.delete(groupId);
} else {
expandedGroups.value.add(groupId);
}
};
const getStepIcon = (step: Step) => {
return step.type === 'thinking' ? getIconPath('brain') : getIconPath('terminal');
};
const getToolName = (action: Action) => {
const tool = action.tool;
if (!tool) return '执行工具';
const intentEnabled = personalizationStore.form.tool_intent_enabled;
const intentText = tool.intent_rendered || tool.intent_full || '';
if (intentEnabled && intentText) {
// intentintentintent
return intentText;
}
// intentintent
if (tool.status === 'preparing') {
return `准备调用 ${tool.name || '工具'}...`;
}
if (tool.status === 'running') {
return `正在调用 ${tool.name || '工具'}...`;
}
if (tool.status === 'completed') {
return '工具执行完成';
}
return tool.name || '执行工具';
};
const getToolIntent = (action: Action) => {
return action.tool?.intent || '';
};
const renderToolResult = (action: Action) => {
if (personalizationStore.form.enhanced_tool_display) {
const rendered = renderEnhancedToolResult(
action,
props.formatSearchTopic || (() => ''),
props.formatSearchTime || (() => ''),
props.formatSearchDomains || (() => '')
);
if (rendered) return rendered;
}
const result = action.tool?.result;
if (!result) return '<div class="result-item">执行中...</div>';
if (typeof result === 'string') {
return `<div class="result-item"><pre>${escapeHtml(result)}</pre></div>`;
}
return `<div class="result-item"><pre>${escapeHtml(JSON.stringify(result, null, 2))}</pre></div>`;
};
const renderContent = (content: string) => {
if (!content) return '';
return escapeHtml(content).replace(/\n/g, '<br>');
};
const renderMarkdown = (content: string, isStreaming: boolean) => {
if (props.renderMarkdown) {
return props.renderMarkdown(content, isStreaming);
}
// renderMarkdown使HTML
return renderContent(content);
};
const escapeHtml = (text: string) => {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
};
const getIconPath = (name: string) => {
const icons: Record<string, string> = {
brain: '<path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"></path><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"></path><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"></path><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"></path><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"></path><path d="M3.477 10.896a4 4 0 0 1 .585-.396"></path><path d="M19.938 10.5a4 4 0 0 1 .585.396"></path><path d="M6 18a4 4 0 0 1-1.967-.516"></path><path d="M19.967 17.484A4 4 0 0 1 18 18"></path>',
terminal: '<polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line>',
search: '<circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.35-4.35"></path>'
};
return icons[name] || icons.brain;
};
watch(() => props.actions, () => {
//
}, { deep: true });
</script>
<style scoped>
.minimal-blocks-container {
margin: 16px 0;
}
/* 等待动画 */
.generating-placeholder {
display: flex;
gap: 4px;
padding: 12px 0;
font-size: 15px;
font-weight: 500;
color: var(--claude-text-tertiary);
}
.generating-placeholder .letter {
display: inline-block;
animation: wave 1.6s ease-in-out infinite;
opacity: 0.4;
}
@keyframes wave {
0%, 100% {
opacity: 0.4;
transform: translateY(0);
}
50% {
opacity: 1;
transform: translateY(-3px);
}
}
/* 摘要组 */
.summary-group {
margin: 16px 0;
}
/* 摘要行(只有文字) */
.summary-line-text {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0;
cursor: pointer;
transition: background-color 0.2s;
}
.summary-line-text:hover {
background-color: var(--claude-panel);
}
.summary-content-wrapper {
flex: 1;
font-size: 15px;
color: var(--claude-text-secondary);
line-height: 1.7;
padding: 0 20px 0 15px;
}
.summary-preview {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: inherit;
}
.expand-icon {
flex-shrink: 0;
margin-left: 8px;
color: var(--claude-text-tertiary);
transition: transform 0.2s;
}
.expand-icon.expanded {
transform: rotate(180deg);
}
/* 运行中的发光效果 */
.summary-line-text.running .summary-preview {
animation: textGlow 1.5s ease-in-out infinite !important;
}
@keyframes textGlow {
0%, 100% {
color: var(--claude-text-secondary) !important;
}
50% {
color: var(--claude-accent) !important;
}
}
/* 步骤容器 */
.steps-container {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1);
margin-top: 8px;
padding: 0 20px 0 15px;
}
.steps-wrapper {
overflow: hidden;
min-height: 0;
}
.steps-container.show {
grid-template-rows: 1fr;
}
/* 步骤项 */
.step-item {
display: flex;
gap: 12px;
}
.step-item:not(:last-child) {
margin-bottom: 16px;
}
.step-timeline {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
padding-top: 2px;
}
.step-icon {
color: var(--claude-text-secondary);
flex-shrink: 0;
}
.step-line {
width: 2px;
flex: 1;
background: var(--claude-border);
margin-top: 4px;
}
/* 最后一个步骤隐藏竖线 */
.step-item:last-child .step-line {
display: none;
}
.step-content {
flex: 1;
min-width: 0;
padding-top: 2px;
}
.step-header {
font-size: 12px;
font-weight: 600;
color: var(--claude-text-tertiary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.step-body {
font-size: 14px;
color: var(--claude-text-secondary);
line-height: 1.7;
}
/* 思考内容 */
.thinking-content {
color: var(--claude-text-secondary);
font-size: 14px;
line-height: 1.7;
white-space: pre-wrap;
word-wrap: break-word;
max-height: calc(1.7em * 6); /* 6行 */
overflow-y: auto;
margin: 0;
padding: 0;
/* 让第一行文字紧贴顶部 */
display: block;
}
.thinking-content::before {
content: '';
display: block;
margin-top: calc((1.7em - 1em) / -2);
}
.thinking-content::after {
content: '';
display: block;
margin-bottom: calc((1.7em - 1em) / -2);
}
/* 工具内容 */
.step-header {
font-size: 12px;
font-weight: 600;
color: var(--claude-text-tertiary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 0 0 8px 0;
padding: 0;
line-height: 1;
}
.tool-intent {
font-weight: 500;
color: var(--claude-text);
margin-bottom: 12px;
font-size: 14px;
line-height: 1.7;
/* 让第一行文字紧贴顶部 */
margin-top: calc((1.7em - 1em) / -2);
}
.tool-result {
background: var(--claude-panel);
border: 1px solid var(--claude-border);
border-radius: 8px;
padding: 12px;
font-size: 13px;
max-height: 200px; /* 约为原来卡片的三分之一 */
overflow-y: auto;
/* 隐藏滚动条 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.tool-result::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
.tool-result * {
/* 隐藏所有子元素的滚动条 */
scrollbar-width: none;
-ms-overflow-style: none;
}
.tool-result *::-webkit-scrollbar {
display: none;
}
.tool-result pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
font-size: 12px;
color: var(--claude-text-secondary);
}
.result-item {
padding: 8px 0;
border-bottom: 1px solid var(--claude-border);
}
.result-item:last-child {
border-bottom: none;
}
/* 文本输出 */
.text-output {
margin: 16px 0;
}
.text-content {
font-size: 15px;
line-height: 1.7;
color: var(--claude-text);
word-wrap: break-word;
}
</style>

View File

@ -430,27 +430,27 @@
</div>
<div class="behavior-field">
<div class="behavior-field-header">
<span class="field-title">堆叠块显示</span>
<p class="field-desc">使用新版堆叠动画展示思考/工具块超过 6 条自动收纳为"更多"默认开启</p>
<span class="field-title">堆叠块显示模式</span>
<p class="field-desc">选择思考/工具块的显示方式默认为堆叠动画</p>
</div>
<label class="toggle-row">
<input
type="checkbox"
:checked="experiments.stackedBlocksEnabled"
@change="handleStackedBlocksToggle($event)"
/>
<span class="fancy-check" aria-hidden="true">
<svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path>
</svg>
</span>
<span>在对话区使用堆叠动画可随时切换回传统列表</span>
</label>
</div>
<div class="run-mode-options">
<button
v-for="option in blockDisplayOptions"
:key="option.id"
type="button"
class="run-mode-card"
:class="{ active: experiments.blockDisplayMode === option.value }"
:aria-pressed="experiments.blockDisplayMode === option.value"
@click.prevent="handleBlockDisplayModeChange(option.value)"
>
<div class="run-mode-card-header">
<span class="run-mode-title">{{ option.label }}</span>
<span v-if="option.badge" class="run-mode-badge">{{ option.badge }}</span>
</div>
<p class="run-mode-desc">{{ option.desc }}</p>
</button>
</div>
</div>
<div class="behavior-field">
<div class="behavior-field-header">
<span class="field-title">静默禁用</span>
@ -769,7 +769,7 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { usePersonalizationStore } from '@/stores/personalization';
import { usePersonalizationStore, type BlockDisplayMode } from '@/stores/personalization';
import { useResourceStore } from '@/stores/resource';
import { useUiStore } from '@/stores/ui';
import { usePolicyStore } from '@/stores/policy';
@ -1062,9 +1062,14 @@ const handleLiquidGlassToggle = (event: Event) => {
personalization.setLiquidGlassExperimentEnabled(!!target?.checked);
};
const handleStackedBlocksToggle = (event: Event) => {
const target = event.target as HTMLInputElement | null;
personalization.setStackedBlocksEnabled(!!target?.checked);
const blockDisplayOptions = [
{ id: 'traditional', label: '传统列表', desc: '按时间顺序垂直排列,经典样式', value: 'traditional' as const },
{ id: 'stacked', label: '堆叠动画', desc: '超过 6 条自动收纳为"更多",动态展示', value: 'stacked' as const, badge: '推荐' },
{ id: 'minimal', label: '极简模式', desc: '摘要行 + 无缝展开,流式输出效果', value: 'minimal' as const, badge: '新' }
];
const handleBlockDisplayModeChange = (mode: 'traditional' | 'stacked' | 'minimal') => {
personalization.setBlockDisplayMode(mode);
};
const openAdminPanel = () => {

View File

@ -1,5 +1,6 @@
import { defineStore } from 'pinia';
export type BlockDisplayMode = 'traditional' | 'stacked' | 'minimal';
type RunMode = 'fast' | 'thinking' | 'deep';
interface PersonalForm {
@ -31,7 +32,7 @@ interface LiquidGlassPosition {
interface ExperimentState {
liquidGlassEnabled: boolean;
liquidGlassPosition: LiquidGlassPosition | null;
stackedBlocksEnabled: boolean;
blockDisplayMode: BlockDisplayMode;
}
interface PersonalizationState {
@ -84,7 +85,7 @@ const defaultForm = (): PersonalForm => ({
const defaultExperimentState = (): ExperimentState => ({
liquidGlassEnabled: false,
liquidGlassPosition: null,
stackedBlocksEnabled: true
blockDisplayMode: 'stacked'
});
const isValidPosition = (value: any): value is LiquidGlassPosition => {
@ -108,11 +109,21 @@ const loadExperimentState = (): ExperimentState => {
return defaultExperimentState();
}
const parsed = JSON.parse(raw);
// 兼容旧版 stackedBlocksEnabled
let blockDisplayMode: BlockDisplayMode = defaultExperimentState().blockDisplayMode;
if (typeof parsed?.blockDisplayMode === 'string' &&
['traditional', 'stacked', 'minimal'].includes(parsed.blockDisplayMode)) {
blockDisplayMode = parsed.blockDisplayMode;
} else if (typeof parsed?.stackedBlocksEnabled === 'boolean') {
// 兼容旧版true -> stacked, false -> traditional
blockDisplayMode = parsed.stackedBlocksEnabled ? 'stacked' : 'traditional';
}
return {
liquidGlassEnabled: Boolean(parsed?.liquidGlassEnabled),
liquidGlassPosition: isValidPosition(parsed?.liquidGlassPosition) ? parsed?.liquidGlassPosition : null,
stackedBlocksEnabled:
typeof parsed?.stackedBlocksEnabled === 'boolean' ? parsed.stackedBlocksEnabled : defaultExperimentState().stackedBlocksEnabled
blockDisplayMode
};
} catch (error) {
console.warn('无法读取实验功能设置:', error);
@ -532,16 +543,13 @@ export const usePersonalizationStore = defineStore('personalization', {
toggleLiquidGlassExperiment() {
this.setLiquidGlassExperimentEnabled(!this.experiments.liquidGlassEnabled);
},
setStackedBlocksEnabled(enabled: boolean) {
setBlockDisplayMode(mode: BlockDisplayMode) {
this.experiments = {
...this.experiments,
stackedBlocksEnabled: !!enabled
blockDisplayMode: mode
};
this.persistExperiments();
},
toggleStackedBlocks() {
this.setStackedBlocksEnabled(!this.experiments.stackedBlocksEnabled);
},
updateLiquidGlassPosition(position: LiquidGlassPosition | null) {
this.experiments = {
...this.experiments,

View File

@ -363,6 +363,12 @@
margin-bottom: 24px;
}
.user-message {
display: flex;
flex-direction: column;
align-items: flex-end; /* 靠右对齐 */
}
.user-message .message-header,
.assistant-message .message-header {
font-size: 14px;
@ -390,6 +396,9 @@
display: flex;
flex-direction: column;
gap: 10px;
max-width: 60%; /* 最大宽度 60% */
width: fit-content; /* 根据内容自适应宽度 */
align-self: flex-end; /* 确保靠右 */
}
.assistant-message .message-text {