mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 04:18:17 +08:00
feat(chat): show reply model attribution in assistant message bubbles
This commit is contained in:
parent
d2886bdaa3
commit
97e2b12f71
@ -1416,6 +1416,14 @@ public class ChatController {
|
|||||||
payload.put("status", status);
|
payload.put("status", status);
|
||||||
if (savedAssistant != null && savedAssistant.getId() != null) {
|
if (savedAssistant != null && savedAssistant.getId() != null) {
|
||||||
payload.put("assistantMessageId", savedAssistant.getId());
|
payload.put("assistantMessageId", savedAssistant.getId());
|
||||||
|
// Surface runtime model attribution so the chat bubble can show
|
||||||
|
// which model produced this reply without waiting for a history reload.
|
||||||
|
if (savedAssistant.getRuntimeModel() != null && !savedAssistant.getRuntimeModel().isBlank()) {
|
||||||
|
payload.put("runtimeModel", savedAssistant.getRuntimeModel());
|
||||||
|
}
|
||||||
|
if (savedAssistant.getRuntimeProvider() != null && !savedAssistant.getRuntimeProvider().isBlank()) {
|
||||||
|
payload.put("runtimeProvider", savedAssistant.getRuntimeProvider());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (promptTokens > 0) payload.put("promptTokens", promptTokens);
|
if (promptTokens > 0) payload.put("promptTokens", promptTokens);
|
||||||
if (completionTokens > 0) payload.put("completionTokens", completionTokens);
|
if (completionTokens > 0) payload.put("completionTokens", completionTokens);
|
||||||
|
|||||||
@ -41,6 +41,12 @@ public class MessageVO {
|
|||||||
/** Completion tokens 消耗 */
|
/** Completion tokens 消耗 */
|
||||||
private Integer completionTokens;
|
private Integer completionTokens;
|
||||||
|
|
||||||
|
/** Model name actually used to produce this message (e.g. "deepseek-chat"). */
|
||||||
|
private String runtimeModel;
|
||||||
|
|
||||||
|
/** Provider id of the runtime model (e.g. "deepseek", "zhipu"). */
|
||||||
|
private String runtimeProvider;
|
||||||
|
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
private LocalDateTime updateTime;
|
private LocalDateTime updateTime;
|
||||||
@ -59,6 +65,8 @@ public class MessageVO {
|
|||||||
vo.setMetadata(parseMetadataToObject(entity.getMetadata()));
|
vo.setMetadata(parseMetadataToObject(entity.getMetadata()));
|
||||||
vo.setPromptTokens(entity.getPromptTokens());
|
vo.setPromptTokens(entity.getPromptTokens());
|
||||||
vo.setCompletionTokens(entity.getCompletionTokens());
|
vo.setCompletionTokens(entity.getCompletionTokens());
|
||||||
|
vo.setRuntimeModel(entity.getRuntimeModel());
|
||||||
|
vo.setRuntimeProvider(entity.getRuntimeProvider());
|
||||||
vo.setCreateTime(entity.getCreateTime());
|
vo.setCreateTime(entity.getCreateTime());
|
||||||
vo.setUpdateTime(entity.getUpdateTime());
|
vo.setUpdateTime(entity.getUpdateTime());
|
||||||
vo.setContentParts(contentParts);
|
vo.setContentParts(contentParts);
|
||||||
|
|||||||
@ -337,6 +337,12 @@
|
|||||||
>
|
>
|
||||||
<el-icon><RefreshRight /></el-icon>
|
<el-icon><RefreshRight /></el-icon>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Reply model attribution (assistant only) -->
|
||||||
|
<span
|
||||||
|
v-if="role === 'assistant' && replyModel"
|
||||||
|
class="action-model"
|
||||||
|
:title="replyModelTitle"
|
||||||
|
>{{ replyModel }}</span>
|
||||||
<!-- 时间戳(inline) -->
|
<!-- 时间戳(inline) -->
|
||||||
<span class="action-time">{{ formattedTime }}</span>
|
<span class="action-time">{{ formattedTime }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -710,6 +716,16 @@ const formattedTime = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Reply model attribution: shows which model produced the assistant message.
|
||||||
|
// Empty for streaming (server only emits runtimeModel after persistence) and
|
||||||
|
// historical messages prior to MessageVO carrying the field.
|
||||||
|
const replyModel = computed(() => props.message.runtimeModel || '')
|
||||||
|
const replyModelTitle = computed(() => {
|
||||||
|
const provider = props.message.runtimeProvider
|
||||||
|
const base = t('chat.replyModel', { model: replyModel.value })
|
||||||
|
return provider ? `${base} (${provider})` : base
|
||||||
|
})
|
||||||
|
|
||||||
const formatFileSize = (size: number) => {
|
const formatFileSize = (size: number) => {
|
||||||
if (size < 1024) return `${size} B`
|
if (size < 1024) return `${size} B`
|
||||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
||||||
@ -1475,6 +1491,18 @@ watch(isGenerating, (generating) => {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-model {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--mc-text-secondary, #64748b);
|
||||||
|
margin-left: 4px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--mc-fill-2, rgba(100, 116, 139, 0.08));
|
||||||
|
font-family: var(--mc-mono-font, ui-monospace, "SF Mono", Menlo, monospace);
|
||||||
|
user-select: text;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/* ==================== 主内容区域 ==================== */
|
/* ==================== 主内容区域 ==================== */
|
||||||
.msg-content {
|
.msg-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@ -487,6 +487,8 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
const msg = messages.value[msgIndex]
|
const msg = messages.value[msgIndex]
|
||||||
if (data.promptTokens !== undefined) msg.promptTokens = data.promptTokens
|
if (data.promptTokens !== undefined) msg.promptTokens = data.promptTokens
|
||||||
if (data.completionTokens !== undefined) msg.completionTokens = data.completionTokens
|
if (data.completionTokens !== undefined) msg.completionTokens = data.completionTokens
|
||||||
|
if (data.runtimeModel) msg.runtimeModel = data.runtimeModel
|
||||||
|
if (data.runtimeProvider) msg.runtimeProvider = data.runtimeProvider
|
||||||
// Replace the local temp ID with the backend-persisted ID so reconcile can match by ID
|
// Replace the local temp ID with the backend-persisted ID so reconcile can match by ID
|
||||||
if (data.assistantMessageId) {
|
if (data.assistantMessageId) {
|
||||||
msg.id = data.assistantMessageId
|
msg.id = data.assistantMessageId
|
||||||
|
|||||||
@ -148,6 +148,7 @@ export default {
|
|||||||
copy: 'Copy',
|
copy: 'Copy',
|
||||||
copied: 'Copied',
|
copied: 'Copied',
|
||||||
regenerate: 'Regenerate',
|
regenerate: 'Regenerate',
|
||||||
|
replyModel: 'Reply model: {model}',
|
||||||
ttsPlay: 'Read Aloud',
|
ttsPlay: 'Read Aloud',
|
||||||
ttsStop: 'Stop Reading',
|
ttsStop: 'Stop Reading',
|
||||||
conversations: 'Conversations',
|
conversations: 'Conversations',
|
||||||
|
|||||||
@ -148,6 +148,7 @@ export default {
|
|||||||
copy: '复制',
|
copy: '复制',
|
||||||
copied: '已复制',
|
copied: '已复制',
|
||||||
regenerate: '重新生成',
|
regenerate: '重新生成',
|
||||||
|
replyModel: '本条回复模型: {model}',
|
||||||
ttsPlay: '朗读',
|
ttsPlay: '朗读',
|
||||||
ttsStop: '停止朗读',
|
ttsStop: '停止朗读',
|
||||||
conversations: '会话列表',
|
conversations: '会话列表',
|
||||||
|
|||||||
@ -83,6 +83,9 @@ export interface Message {
|
|||||||
// Token 统计
|
// Token 统计
|
||||||
promptTokens?: number
|
promptTokens?: number
|
||||||
completionTokens?: number
|
completionTokens?: number
|
||||||
|
// Runtime model attribution (assistant messages): the model that actually produced this reply
|
||||||
|
runtimeModel?: string
|
||||||
|
runtimeProvider?: string
|
||||||
// 前端临时字段
|
// 前端临时字段
|
||||||
streaming?: boolean // 内部动画控制,UI 渲染以 status 为准
|
streaming?: boolean // 内部动画控制,UI 渲染以 status 为准
|
||||||
attachments?: ChatAttachment[]
|
attachments?: ChatAttachment[]
|
||||||
|
|||||||
@ -1774,6 +1774,8 @@ function normalizeMessage(raw: Message): Message {
|
|||||||
// 保留后端返回的 token 字段(MessageVO 新增)
|
// 保留后端返回的 token 字段(MessageVO 新增)
|
||||||
if ((raw as any).promptTokens) msg.promptTokens = (raw as any).promptTokens
|
if ((raw as any).promptTokens) msg.promptTokens = (raw as any).promptTokens
|
||||||
if ((raw as any).completionTokens) msg.completionTokens = (raw as any).completionTokens
|
if ((raw as any).completionTokens) msg.completionTokens = (raw as any).completionTokens
|
||||||
|
if ((raw as any).runtimeModel) msg.runtimeModel = (raw as any).runtimeModel
|
||||||
|
if ((raw as any).runtimeProvider) msg.runtimeProvider = (raw as any).runtimeProvider
|
||||||
|
|
||||||
if (msg.contentParts.length === 0 && msg.content) {
|
if (msg.contentParts.length === 0 && msg.content) {
|
||||||
if (msg.role === 'assistant') {
|
if (msg.role === 'assistant') {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user