feat(ui): scheme B neutral grayscale for board messages + assistant bubbles
expertIdentity.ts PALETTE -> neutral grayscale; useMessageRenderer.ts removes assistant fallback for board_* events; BoardRoundCard/MessageShell apply GitHub-style gray; chatStream.ts prefers event-provided moderator avatar/color; StickyModeHeader/Scene4/LoginView/types aligned.
This commit is contained in:
parent
32746652aa
commit
8188e8861d
|
|
@ -346,6 +346,8 @@ export interface IExpertSpeechData {
|
|||
/** round_summary event payload */
|
||||
export interface IRoundSummaryData {
|
||||
moderator_name: string;
|
||||
moderator_avatar?: string;
|
||||
moderator_color?: string;
|
||||
content: string;
|
||||
round: number;
|
||||
continue: boolean;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
class="sticky-mode-header__avatar"
|
||||
:class="{ 'sticky-mode-header__avatar--lead': expert.isLead }"
|
||||
type="button"
|
||||
:style="{ borderColor: expert.color }"
|
||||
:style="{ background: expert.color, color: 'var(--text-inverse)', borderColor: expert.color }"
|
||||
:aria-label="`查看 ${expert.name} 详情`"
|
||||
:aria-expanded="openKey === expert.key"
|
||||
>
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
>
|
||||
<span
|
||||
class="expert-list__avatar"
|
||||
:style="{ borderColor: expert.color }"
|
||||
:style="{ background: expert.color, color: 'var(--text-inverse)', borderColor: expert.color }"
|
||||
>{{ expert.avatar }}</span>
|
||||
<span class="expert-list__name">{{ expert.name }}</span>
|
||||
<span v-if="expert.isLead" class="expert-list__tag">Lead</span>
|
||||
|
|
@ -153,11 +153,40 @@ const allExperts = computed<IExpertDisplay[]>(() => {
|
|||
}
|
||||
if (mode.value === 'board') {
|
||||
const list = chatStore.boardState?.experts ?? []
|
||||
// 颜色一致性:优先使用当前会话 messages 中最近的 board_speech/
|
||||
// round_summary 的 expert_color(与 MessageShell 头像同源),保证
|
||||
// 顶部卡片头像与对话头像颜色严格匹配;缺失时回退到 boardState
|
||||
// 中保存的 YAML color。
|
||||
const liveColorByName = new Map<string, string>()
|
||||
const liveAvatarByName = new Map<string, string>()
|
||||
const conv = chatStore.conversations.find(
|
||||
(c: { id: string; messages?: unknown[] }) => c.id === chatStore.currentConversationId,
|
||||
)
|
||||
if (conv?.messages) {
|
||||
// walk from latest to earliest to capture the most recent identity
|
||||
for (let i = conv.messages.length - 1; i >= 0; i--) {
|
||||
const m = conv.messages[i] as {
|
||||
message_type?: string
|
||||
expert_name?: string
|
||||
expert_color?: string
|
||||
expert_avatar?: string
|
||||
}
|
||||
if (
|
||||
(m.message_type === 'board_speech' ||
|
||||
m.message_type === 'board_summary') &&
|
||||
m.expert_name &&
|
||||
!liveColorByName.has(m.expert_name)
|
||||
) {
|
||||
if (m.expert_color) liveColorByName.set(m.expert_name, m.expert_color)
|
||||
if (m.expert_avatar) liveAvatarByName.set(m.expert_name, m.expert_avatar)
|
||||
}
|
||||
}
|
||||
}
|
||||
return list.map((e: IBoardExpert, idx: number) => ({
|
||||
key: `${e.name}-${idx}`,
|
||||
name: e.name,
|
||||
avatar: e.avatar,
|
||||
color: e.color,
|
||||
avatar: liveAvatarByName.get(e.name) ?? e.avatar,
|
||||
color: liveColorByName.get(e.name) ?? e.color,
|
||||
persona: e.persona,
|
||||
description: e.is_moderator ? '主持人' : undefined,
|
||||
isLead: false,
|
||||
|
|
|
|||
|
|
@ -14,20 +14,20 @@ export interface ExpertIdentity {
|
|||
color: string
|
||||
}
|
||||
|
||||
/** Non-blue palette. Order is stable — do not reshuffle (would break identity). */
|
||||
/** Neutral slate palette (GitHub/professional style). Order is stable — do not reshuffle. */
|
||||
const PALETTE: ReadonlyArray<string> = [
|
||||
"#d97706", // amber 600
|
||||
"#059669", // emerald 600
|
||||
"#7c3aed", // violet 600
|
||||
"#db2777", // pink 600
|
||||
"#0e7490", // cyan 700
|
||||
"#65a30d", // lime 600
|
||||
"#c2410c", // orange 700
|
||||
"#9333ea", // purple 600
|
||||
"#0891b2", // sky 600
|
||||
"#a16207", // yellow 700
|
||||
"#be185d", // rose 700
|
||||
"#15803d", // green 700
|
||||
"#4b5563", // slate 600
|
||||
"#6b7280", // gray 500
|
||||
"#9ca3af", // gray 400
|
||||
"#374151", // gray 800
|
||||
"#1f2937", // gray 900
|
||||
"#1e40af", // blue 800 (deep, non-vivid)
|
||||
"#166534", // green 800
|
||||
"#7c2d12", // orange 900
|
||||
"#581c87", // purple 900
|
||||
"#155e75", // cyan 800
|
||||
"#92400e", // amber 800
|
||||
"#78716c", // stone 500
|
||||
]
|
||||
|
||||
/** djb2-style string hash — stable across JS engines and reloads. */
|
||||
|
|
|
|||
|
|
@ -55,19 +55,19 @@ export function resolveMessageType(message: IChatMessage): MessageViewType {
|
|||
switch (message.message_type) {
|
||||
case 'plan_update':
|
||||
return 'team_plan'
|
||||
// 2026-07-01: board_* events render as plain assistant bubbles (streaming).
|
||||
// The user's first @board/@team message already shows a structured card
|
||||
// (UserBubble) with topic + expert count + expert list; rendering
|
||||
// dedicated cards (BoardBannerCard / BoardRoundCard / BoardConclusionCard)
|
||||
// for the subsequent board_started/board_speech/board_summary/
|
||||
// board_conclusion events duplicates the banner and breaks the natural
|
||||
// chat flow. Fall through to 'assistant' so the content streams
|
||||
// inline.
|
||||
// 2026-07-02: 恢复 board_* 事件走专用卡片路径,落地方案B
|
||||
// (中性灰阶头像/名字/颜色徽章/对话气泡 + 左侧3px色条)。
|
||||
// UserBubble 仅渲染用户输入的 @board 指令文本卡片,
|
||||
// BoardBannerCard 渲染后端回送的 board_started 事件 (含专家 chip + 进度条),
|
||||
// 二者职责不同,不构成重复。
|
||||
case 'board_started':
|
||||
return 'board_banner'
|
||||
case 'board_speech':
|
||||
return 'board_speech'
|
||||
case 'board_summary':
|
||||
return 'board_summary'
|
||||
case 'board_conclusion':
|
||||
return 'assistant'
|
||||
return 'board_conclusion'
|
||||
case 'debate_started':
|
||||
return 'debate_started'
|
||||
case 'debate_argument':
|
||||
|
|
@ -149,42 +149,34 @@ export function useMessageRenderer(message: IChatMessage) {
|
|||
}
|
||||
}
|
||||
|
||||
case 'board_speech':
|
||||
case 'board_speech': {
|
||||
const roleTag = message.board_role === 'moderator' ? '主持' : '专家'
|
||||
return {
|
||||
type,
|
||||
shell: {
|
||||
name: message.expert_name || '专家',
|
||||
meta: message.board_round ? `第 ${message.board_round} 轮${message.board_role === 'moderator' ? ' · 主持' : ''}` : time,
|
||||
meta: message.board_round ? `第 ${message.board_round} 轮 · ${roleTag}` : `${roleTag}`,
|
||||
avatar: message.expert_avatar,
|
||||
color: message.expert_color,
|
||||
},
|
||||
component: BoardRoundCard,
|
||||
props: {
|
||||
name: message.expert_name || '专家',
|
||||
avatar: message.expert_avatar || '',
|
||||
color: message.expert_color,
|
||||
round: message.board_round,
|
||||
role: message.board_role === 'moderator' ? 'moderator' : 'expert',
|
||||
content: message.content || '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
case 'board_summary':
|
||||
return {
|
||||
type,
|
||||
shell: {
|
||||
name: message.expert_name || '主持人',
|
||||
meta: message.board_round ? `第 ${message.board_round} 轮 · 小结` : time,
|
||||
meta: message.board_round ? `第 ${message.board_round} 轮 · 小结` : '小结',
|
||||
avatar: message.expert_avatar,
|
||||
color: message.expert_color,
|
||||
},
|
||||
component: BoardRoundCard,
|
||||
props: {
|
||||
name: message.expert_name || '主持人',
|
||||
avatar: message.expert_avatar || '',
|
||||
color: message.expert_color,
|
||||
round: message.board_round,
|
||||
role: 'summary',
|
||||
content: message.content || '',
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,7 @@
|
|||
<template>
|
||||
<div class="board-round-card" :class="[`board-round-card--${role}`]">
|
||||
<div class="board-round-card__header">
|
||||
<span
|
||||
class="board-round-card__avatar"
|
||||
:style="avatarStyle"
|
||||
>
|
||||
{{ avatar || name.charAt(0) }}
|
||||
</span>
|
||||
<span class="board-round-card__name">{{ name }}</span>
|
||||
<span v-if="round" class="board-round-card__round">第 {{ round }} 轮</span>
|
||||
<span v-if="roleTag" class="board-round-card__role">{{ roleTag }}</span>
|
||||
</div>
|
||||
<div class="board-round-card__content">
|
||||
<div class="board-round-card">
|
||||
<AssistantText :message="textMessage" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
@ -23,37 +10,16 @@ import type { IChatMessage } from '@/api/types'
|
|||
import AssistantText from './AssistantText.vue'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
avatar?: string
|
||||
color?: string
|
||||
round?: number
|
||||
role?: 'moderator' | 'expert' | 'summary'
|
||||
content: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
avatar: '',
|
||||
role: 'expert',
|
||||
})
|
||||
|
||||
const roleTag = computed(() => {
|
||||
const tags: Record<string, string> = {
|
||||
moderator: '主持',
|
||||
expert: '专家',
|
||||
summary: '小结',
|
||||
}
|
||||
return tags[props.role] || ''
|
||||
})
|
||||
|
||||
const avatarStyle = computed(() => {
|
||||
if (props.color) {
|
||||
return { background: props.color }
|
||||
}
|
||||
return { background: 'var(--accent-board)' }
|
||||
})
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// 方案B: BoardRoundCard 仅作为 Board 发言的纯内容容器,
|
||||
// 渲染 AssistantText 的流式文本。头像/名字/轮次/角色标签等元信息
|
||||
// 全部由外层 MessageShell 的 header 负责,避免重复渲染。
|
||||
const textMessage = computed<IChatMessage>(() => ({
|
||||
id: `board-${props.name}-${props.round || 0}`,
|
||||
id: `board-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
role: 'assistant',
|
||||
content: props.content,
|
||||
timestamp: new Date().toISOString(),
|
||||
|
|
@ -64,63 +30,5 @@ const textMessage = computed<IChatMessage>(() => ({
|
|||
<style scoped>
|
||||
.board-round-card {
|
||||
width: 100%;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: 3px solid var(--accent-board);
|
||||
border-radius: var(--radius-card);
|
||||
box-shadow: var(--shadow-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.board-round-card--summary {
|
||||
background: var(--accent-board-soft);
|
||||
border-left-color: var(--accent-board);
|
||||
}
|
||||
|
||||
.board-round-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.board-round-card__avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 12px;
|
||||
color: var(--text-inverse);
|
||||
}
|
||||
|
||||
.board-round-card__name {
|
||||
font-size: var(--font-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.board-round-card__round {
|
||||
font-size: var(--font-xs);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.board-round-card__role {
|
||||
margin-left: auto;
|
||||
font-size: var(--font-xs);
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--accent-board-soft);
|
||||
color: var(--accent-board);
|
||||
}
|
||||
|
||||
.board-round-card__content {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
}
|
||||
|
||||
.board-round-card__content :deep(p) {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -20,13 +20,13 @@
|
|||
</div>
|
||||
<div class="message-shell__body">
|
||||
<div class="message-shell__header">
|
||||
<!-- U4 R10: 专家身份 badge — 用 expert_color 高亮 expert_name,流式期间及结束后均保留 -->
|
||||
<!-- 方案B: 专家名字始终是纯文本 (粗体),彩色身份由头像背景承担,
|
||||
不再渲染彩色 pill 名字徽章 -->
|
||||
<span
|
||||
v-if="expertName"
|
||||
class="message-shell__expert-badge"
|
||||
:style="{ backgroundColor: expertColor || '#1890ff' }"
|
||||
>{{ expertName }}</span>
|
||||
<span v-else class="message-shell__name">{{ name }}</span>
|
||||
v-if="expertName || name"
|
||||
class="message-shell__name"
|
||||
:class="{ 'message-shell__name--expert': !!expertName }"
|
||||
>{{ expertName || name }}</span>
|
||||
<span v-if="meta" class="message-shell__meta">{{ meta }}</span>
|
||||
<span
|
||||
v-if="streaming"
|
||||
|
|
@ -152,20 +152,10 @@ withDefaults(defineProps<Props>(), {
|
|||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
/* U4 R10: 专家身份 badge — 彩色 pill,区别于普通 name 文本与 avatar */
|
||||
.message-shell__expert-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 var(--space-2);
|
||||
border-radius: var(--radius-full);
|
||||
color: var(--text-inverse, #fff);
|
||||
font-size: var(--font-xs);
|
||||
/* 方案B: 专家名字 — 粗体文本 + 略深色,与普通 name 区分 (无 pill 背景) */
|
||||
.message-shell__name--expert {
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: 1.6;
|
||||
max-width: 12em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message-shell__meta {
|
||||
|
|
|
|||
|
|
@ -14,37 +14,16 @@
|
|||
/>
|
||||
</MessageShell>
|
||||
|
||||
<MessageShell role="assistant" name="架构师老张" meta="第 1 轮" avatar="张" color="var(--accent-board)">
|
||||
<BoardRoundCard
|
||||
name="架构师老张"
|
||||
avatar="张"
|
||||
color="var(--accent-board)"
|
||||
:round="1"
|
||||
role="expert"
|
||||
:content="speech1.content"
|
||||
/>
|
||||
<MessageShell role="assistant" name="架构师老张" meta="第 1 轮 · 专家" avatar="张" color="var(--accent-board)">
|
||||
<BoardRoundCard :content="speech1.content" />
|
||||
</MessageShell>
|
||||
|
||||
<MessageShell role="assistant" name="后端负责人小李" meta="第 1 轮" avatar="李" color="var(--accent-board)">
|
||||
<BoardRoundCard
|
||||
name="后端负责人小李"
|
||||
avatar="李"
|
||||
color="var(--accent-board)"
|
||||
:round="1"
|
||||
role="expert"
|
||||
:content="speech2.content"
|
||||
/>
|
||||
<MessageShell role="assistant" name="后端负责人小李" meta="第 1 轮 · 专家" avatar="李" color="var(--accent-board)">
|
||||
<BoardRoundCard :content="speech2.content" />
|
||||
</MessageShell>
|
||||
|
||||
<MessageShell role="assistant" name="主持人" meta="第 1 轮 · 小结" avatar="主" color="var(--accent-board)">
|
||||
<BoardRoundCard
|
||||
name="主持人"
|
||||
avatar="主"
|
||||
color="var(--accent-board)"
|
||||
:round="1"
|
||||
role="summary"
|
||||
:content="summaryMessage.content"
|
||||
/>
|
||||
<BoardRoundCard :content="summaryMessage.content" />
|
||||
</MessageShell>
|
||||
|
||||
<MessageShell role="assistant" name="主持人" meta="11:05">
|
||||
|
|
|
|||
|
|
@ -1296,14 +1296,17 @@ export function dispatchWsEvent(
|
|||
(c) => c.id === conversationId,
|
||||
);
|
||||
if (!conv) break;
|
||||
// Stable identity for the moderator, just like expert_speech.
|
||||
// Stable identity for the moderator. Prefer the event's
|
||||
// moderator_avatar/moderator_color (added in 2026-07-02 so persistence
|
||||
// has the same identity) and fall back to the boardState snapshot
|
||||
// captured at board_started.
|
||||
const moderator = state.boardState.value?.experts.find(
|
||||
(e) => e.name === summaryData.moderator_name,
|
||||
);
|
||||
const identity = resolveExpertIdentity(
|
||||
summaryData.moderator_name,
|
||||
moderator?.avatar,
|
||||
moderator?.color,
|
||||
summaryData.moderator_avatar || moderator?.avatar,
|
||||
summaryData.moderator_color || moderator?.color,
|
||||
);
|
||||
const summaryMsg: IChatMessage = {
|
||||
id: generateId(),
|
||||
|
|
|
|||
|
|
@ -181,6 +181,20 @@ onMounted(async () => {
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 显式绑定到 token — AntD ConfigProvider token 在 tauri 冷启动时序
|
||||
不稳定,这里兜底强制使用项目主色(#1a1a1a 近黑),避免蓝色兜底。 */
|
||||
.login-submit.ant-btn-primary,
|
||||
.login-submit.ant-btn-primary:hover,
|
||||
.login-submit.ant-btn-primary:focus {
|
||||
background-color: var(--color-primary, #1a1a1a);
|
||||
border-color: var(--color-primary, #1a1a1a);
|
||||
color: var(--text-inverse, #ffffff);
|
||||
}
|
||||
.login-submit.ant-btn-primary:hover {
|
||||
background-color: var(--color-primary-hover, #2f2f2f);
|
||||
border-color: var(--color-primary-hover, #2f2f2f);
|
||||
}
|
||||
|
||||
.login-remember {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue