mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +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);
|
||||
if (savedAssistant != null && savedAssistant.getId() != null) {
|
||||
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 (completionTokens > 0) payload.put("completionTokens", completionTokens);
|
||||
|
||||
@ -41,6 +41,12 @@ public class MessageVO {
|
||||
/** Completion tokens 消耗 */
|
||||
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 updateTime;
|
||||
@ -59,6 +65,8 @@ public class MessageVO {
|
||||
vo.setMetadata(parseMetadataToObject(entity.getMetadata()));
|
||||
vo.setPromptTokens(entity.getPromptTokens());
|
||||
vo.setCompletionTokens(entity.getCompletionTokens());
|
||||
vo.setRuntimeModel(entity.getRuntimeModel());
|
||||
vo.setRuntimeProvider(entity.getRuntimeProvider());
|
||||
vo.setCreateTime(entity.getCreateTime());
|
||||
vo.setUpdateTime(entity.getUpdateTime());
|
||||
vo.setContentParts(contentParts);
|
||||
|
||||
@ -337,6 +337,12 @@
|
||||
>
|
||||
<el-icon><RefreshRight /></el-icon>
|
||||
</button>
|
||||
<!-- Reply model attribution (assistant only) -->
|
||||
<span
|
||||
v-if="role === 'assistant' && replyModel"
|
||||
class="action-model"
|
||||
:title="replyModelTitle"
|
||||
>{{ replyModel }}</span>
|
||||
<!-- 时间戳(inline) -->
|
||||
<span class="action-time">{{ formattedTime }}</span>
|
||||
</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) => {
|
||||
if (size < 1024) return `${size} B`
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
||||
@ -1475,6 +1491,18 @@ watch(isGenerating, (generating) => {
|
||||
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 {
|
||||
position: relative;
|
||||
|
||||
@ -487,6 +487,8 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
||||
const msg = messages.value[msgIndex]
|
||||
if (data.promptTokens !== undefined) msg.promptTokens = data.promptTokens
|
||||
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
|
||||
if (data.assistantMessageId) {
|
||||
msg.id = data.assistantMessageId
|
||||
|
||||
@ -148,6 +148,7 @@ export default {
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
regenerate: 'Regenerate',
|
||||
replyModel: 'Reply model: {model}',
|
||||
ttsPlay: 'Read Aloud',
|
||||
ttsStop: 'Stop Reading',
|
||||
conversations: 'Conversations',
|
||||
|
||||
@ -148,6 +148,7 @@ export default {
|
||||
copy: '复制',
|
||||
copied: '已复制',
|
||||
regenerate: '重新生成',
|
||||
replyModel: '本条回复模型: {model}',
|
||||
ttsPlay: '朗读',
|
||||
ttsStop: '停止朗读',
|
||||
conversations: '会话列表',
|
||||
|
||||
@ -83,6 +83,9 @@ export interface Message {
|
||||
// Token 统计
|
||||
promptTokens?: number
|
||||
completionTokens?: number
|
||||
// Runtime model attribution (assistant messages): the model that actually produced this reply
|
||||
runtimeModel?: string
|
||||
runtimeProvider?: string
|
||||
// 前端临时字段
|
||||
streaming?: boolean // 内部动画控制,UI 渲染以 status 为准
|
||||
attachments?: ChatAttachment[]
|
||||
|
||||
@ -1774,6 +1774,8 @@ function normalizeMessage(raw: Message): Message {
|
||||
// 保留后端返回的 token 字段(MessageVO 新增)
|
||||
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).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.role === 'assistant') {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user