You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
223 lines
4.9 KiB
223 lines
4.9 KiB
<script setup lang="ts">
|
|
import type { AgentMessage } from "~/composables/useAgentChat";
|
|
|
|
const props = defineProps<{
|
|
message: AgentMessage;
|
|
isLoading: boolean;
|
|
isLast: boolean;
|
|
loggedIn: boolean;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
approve: [toolCallId: string, approved: boolean];
|
|
feedback: [messageId: string, feedback: "like" | "dislike"];
|
|
edit: [messageId: string, content: string];
|
|
regenerate: [];
|
|
}>();
|
|
|
|
const showActions = ref(false);
|
|
const isHovering = ref(false);
|
|
|
|
const isUser = computed(() => props.message.role === "user");
|
|
const isAssistant = computed(() => props.message.role === "assistant");
|
|
|
|
const showLoading = computed(
|
|
() => isAssistant.value && props.isLoading && props.isLast && !props.message.parts?.length,
|
|
);
|
|
|
|
const showFeedback = computed(
|
|
() => isAssistant.value && !props.isLoading && !!props.message.parts?.length && props.loggedIn,
|
|
);
|
|
|
|
function handleFeedback(type: "like" | "dislike") {
|
|
emit("feedback", props.message.id, type);
|
|
}
|
|
|
|
function handleEdit() {
|
|
emit("edit", props.message.id, props.message.content);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="agent-message"
|
|
:class="{ 'is-user': isUser, 'is-assistant': isAssistant }"
|
|
@mouseenter="isHovering = true"
|
|
@mouseleave="isHovering = false"
|
|
>
|
|
<!-- User message -->
|
|
<div v-if="isUser" class="user-message">
|
|
<div class="user-bubble">
|
|
{{ message.content }}
|
|
</div>
|
|
<div class="message-actions" :class="{ visible: isHovering }">
|
|
<button class="action-btn" title="编辑" @click="handleEdit">
|
|
<Icon name="lucide:pencil" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Assistant message -->
|
|
<div v-else class="assistant-message">
|
|
<div class="assistant-avatar">
|
|
<Icon name="lucide:sparkles" />
|
|
</div>
|
|
<div class="assistant-body">
|
|
<div v-if="showLoading" class="assistant-loading">
|
|
<span class="loading-dot" />
|
|
<span class="loading-dot" />
|
|
<span class="loading-dot" />
|
|
</div>
|
|
|
|
<AgentMessageParts
|
|
v-else-if="message.parts?.length"
|
|
:parts="message.parts"
|
|
:is-loading="isLoading"
|
|
@approve="(id, approved) => emit('approve', id, approved)"
|
|
/>
|
|
|
|
<AgentMarkdown
|
|
v-else-if="message.content"
|
|
:text="message.content"
|
|
/>
|
|
|
|
<div v-if="showFeedback" class="assistant-footer" :class="{ visible: isHovering }">
|
|
<AgentFeedbackButtons
|
|
:feedback="message.feedback"
|
|
@like="handleFeedback('like')"
|
|
@dislike="handleFeedback('dislike')"
|
|
/>
|
|
<button
|
|
v-if="isLast && !isLoading"
|
|
class="action-btn"
|
|
title="重新生成"
|
|
@click="emit('regenerate')"
|
|
>
|
|
<Icon name="lucide:refresh-cw" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.agent-message {
|
|
padding: 4px 0;
|
|
}
|
|
|
|
/* User message */
|
|
.user-message {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
}
|
|
|
|
.user-bubble {
|
|
max-width: 75%;
|
|
padding: 10px 16px;
|
|
background: var(--color-surface-soft);
|
|
border: 1px solid var(--color-hairline);
|
|
border-radius: 18px 18px 4px 18px;
|
|
font-size: 15px;
|
|
line-height: 1.6;
|
|
color: var(--color-ink);
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.message-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
opacity: 0;
|
|
transition: opacity 0.15s;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.message-actions.visible {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Assistant message */
|
|
.assistant-message {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.assistant-avatar {
|
|
flex-shrink: 0;
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
background: var(--color-surface-dark);
|
|
color: var(--color-on-dark);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.assistant-body {
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding-top: 4px;
|
|
}
|
|
|
|
.assistant-loading {
|
|
display: flex;
|
|
gap: 4px;
|
|
padding: 8px 0;
|
|
}
|
|
|
|
.loading-dot {
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: 50%;
|
|
background: var(--color-muted);
|
|
animation: bounce 1.4s infinite ease-in-out both;
|
|
}
|
|
|
|
.loading-dot:nth-child(1) { animation-delay: -0.32s; }
|
|
.loading-dot:nth-child(2) { animation-delay: -0.16s; }
|
|
|
|
@keyframes bounce {
|
|
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
|
|
40% { transform: scale(1); opacity: 1; }
|
|
}
|
|
|
|
.assistant-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-top: 8px;
|
|
opacity: 0;
|
|
transition: opacity 0.15s;
|
|
}
|
|
|
|
.assistant-footer.visible {
|
|
opacity: 1;
|
|
}
|
|
|
|
.action-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 28px;
|
|
height: 28px;
|
|
border: none;
|
|
background: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
color: var(--color-muted);
|
|
font-size: 14px;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.action-btn:hover {
|
|
background: var(--color-surface-soft);
|
|
color: var(--color-body);
|
|
}
|
|
</style>
|
|
|