Files
pikasTech-HWLAB/web/hwlab-cloud-web/src/components/workbench/ConversationPanel.vue
T

118 lines
5.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from "vue";
import type { ChatMessage } from "@/types";
import LoadingState from "@/components/common/LoadingState.vue";
import StatusBadge from "@/components/common/StatusBadge.vue";
import TraceTimeline from "@/components/agent/TraceTimeline.vue";
import CodeAgentStatusSummary from "@/components/workbench/CodeAgentStatusSummary.vue";
import MessageActions from "@/components/workbench/MessageActions.vue";
import MessageMarkdown from "@/components/workbench/MessageMarkdown.vue";
import MessageTraceDebugPanel from "@/components/workbench/MessageTraceDebugPanel.vue";
import { useClipboard } from "@/composables/useClipboard";
import { useWorkbenchStore } from "@/stores/workbench";
import { traceIdentityText } from "./message-rendering";
import { traceLifecycleExpanded } from "./trace-lifecycle";
const workbench = useWorkbenchStore();
const clipboard = useClipboard();
const panelRef = ref<HTMLElement | null>(null);
const following = ref(true);
const detailMessageId = ref<string | null>(null);
const detailMessage = computed(() => workbench.messages.find((message) => message.id === detailMessageId.value) ?? null);
const scrollSignature = computed(() => workbench.messages.map((message) => [
message.id,
message.status,
message.text?.length ?? 0,
message.updatedAt ?? "",
message.runnerTrace?.eventCount ?? message.runnerTrace?.events?.length ?? 0,
message.runnerTrace?.status ?? ""
].join(":")));
watch(scrollSignature, async () => {
if (!following.value) return;
await nextTick();
scrollToBottom();
});
onMounted(async () => {
await nextTick();
scrollToBottom();
});
function openDetails(message: ChatMessage): void {
detailMessageId.value = message.id;
}
function isRunningMessage(message: ChatMessage): boolean {
return message.role === "agent" && ["pending", "running"].includes(String(message.status ?? "").toLowerCase());
}
function showStatusBadge(message: ChatMessage): boolean {
return message.role !== "agent" || !isRunningMessage(message);
}
function isAwaitingAgentBody(message: ChatMessage): boolean {
return isRunningMessage(message) && !message.runnerTrace && !String(message.text ?? "").trim();
}
function showMessageText(message: ChatMessage): boolean {
return message.role !== "agent" || Boolean(String(message.text ?? "").trim());
}
function onPanelScroll(): void {
const panel = panelRef.value;
if (!panel) return;
following.value = panel.scrollTop + panel.clientHeight >= panel.scrollHeight - 56;
}
function scrollToBottom(): void {
const panel = panelRef.value;
if (!panel) return;
panel.scrollTop = panel.scrollHeight;
}
function traceStorageKey(message: ChatMessage): string {
return `hwlab.workbench.trace-open.${message.traceId ?? message.runnerTrace?.traceId ?? message.id}`;
}
</script>
<template>
<section id="conversation-list" ref="panelRef" class="conversation-panel" :data-following="following ? 'true' : 'false'" @scroll="onPanelScroll">
<LoadingState v-if="workbench.conversationDetailLoading" class="conversation-detail-loading" label="加载中" />
<article v-for="message in workbench.conversationDetailLoading ? [] : workbench.messages" :key="message.id" class="message-card" :data-role="message.role" :data-status="message.status">
<header v-if="message.role !== 'user'">
<strong>{{ message.title }}</strong>
<div class="message-header-actions">
<span v-if="isRunningMessage(message)" class="agent-running-indicator" aria-label="运行中" role="status"><span aria-hidden="true" /></span>
<StatusBadge v-if="showStatusBadge(message)" :status="message.status" />
<button v-if="message.role === 'agent'" class="message-detail-button" type="button" aria-label="运行详情" title="运行详情" @click="openDetails(message)">!</button>
</div>
</header>
<TraceTimeline v-if="message.role === 'agent' && message.runnerTrace" :trace="message.runnerTrace" :auto-expanded="traceLifecycleExpanded(message)" :storage-key="traceStorageKey(message)" />
<LoadingState v-if="isAwaitingAgentBody(message)" class="message-loading" label="思考中..." compact />
<MessageMarkdown v-if="showMessageText(message)" class="message-text" :source="message.text" />
</article>
<div v-if="!workbench.conversationDetailLoading && workbench.messages.length === 0 && workbench.error" class="conversation-empty-hint conversation-error-hint">加载失败{{ workbench.error }}</div>
<div v-else-if="!workbench.conversationDetailLoading && workbench.messages.length === 0" class="conversation-empty-hint">发起对话或从左侧选择 session</div>
<div v-if="detailMessage" class="workbench-dialog-backdrop" role="presentation" @click.self="detailMessageId = null">
<section id="message-detail-dialog" class="workbench-dialog wide" role="dialog" aria-modal="true" aria-labelledby="message-detail-title">
<header>
<div class="dialog-title-stack">
<h2 id="message-detail-title">消息详情</h2>
<p>{{ detailMessage.traceId || detailMessage.runnerTrace?.traceId || 'trace 未观测' }}</p>
</div>
<button type="button" class="dialog-close" aria-label="关闭" @click="detailMessageId = null">x</button>
</header>
<CodeAgentStatusSummary :message="detailMessage" />
<MessageTraceDebugPanel :message="detailMessage" />
<MessageActions
:message="detailMessage"
@cancel="workbench.cancelAgentMessage"
@retry="workbench.retryAgentMessage"
@copy-trace="(target: ChatMessage) => clipboard.copy(traceIdentityText(target))"
/>
</section>
</div>
</section>
</template>