| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- <template>
- <div v-if="detailData" class="agent-detail-panel">
- <div v-if="!hideHeader" class="agent-detail-header">
- <div class="agent-detail-header-left">
- <div class="agent-detail-icon"></div>
- <span class="agent-detail-title">{{ detailData.agent }}</span>
- </div>
- <div class="agent-detail-close" @click="emit('close')">
- <div class="close-icon"></div>
- </div>
- </div>
- <div class="agent-detail-body">
- <!-- 思考内容 -->
- <div v-if="detailData.thinkingContent" class="agent-detail-section">
- <div class="agent-detail-section-header" @click="thinkingCollapsed = !thinkingCollapsed">
- <span class="agent-detail-section-title">思考内容</span>
- <div :class="['section-arrow', { collapsed: thinkingCollapsed }]">
- <div class="arrow-icon"></div>
- </div>
- </div>
- <Transition name="collapse">
- <div v-show="!thinkingCollapsed" class="agent-detail-section-body thinking" v-html="renderMarkdown(detailData.thinkingContent)"></div>
- </Transition>
- </div>
- <!-- 执行步骤 -->
- <div v-if="detailData.steps.length" class="agent-detail-section">
- <div class="agent-detail-section-header" @click="stepsCollapsed = !stepsCollapsed">
- <span class="agent-detail-section-title">执行步骤</span>
- <span class="agent-detail-section-count">{{ detailData.steps.length }}</span>
- <div :class="['section-arrow', { collapsed: stepsCollapsed }]">
- <div class="arrow-icon"></div>
- </div>
- </div>
- <Transition name="collapse">
- <div v-show="!stepsCollapsed" class="agent-detail-section-body">
- <div v-for="(step, sIdx) in detailData.steps" :key="sIdx" class="detail-step">
- <!-- agent_start -->
- <div v-if="step.type === 'agent_start'" class="detail-step-item">
- <div class="step-dot agent-start"></div>
- <div class="step-text">{{ step.message }}</div>
- </div>
- <!-- tool_call -->
- <div v-else-if="step.type === 'tool_call'" class="detail-step-item">
- <div class="step-dot tool-call"></div>
- <div class="step-text">
- <span class="step-label">调用工具:</span>
- <span class="step-tool">{{ step.tool }}</span>
- <span v-if="step.message" class="step-msg">{{ step.message }}</span>
- </div>
- </div>
- <!-- executing -->
- <div v-else-if="step.type === 'executing'" class="detail-step-item">
- <div class="step-dot executing"></div>
- <div class="step-text">
- {{ step.message || '正在执行...' }}
- <span v-if="step.tool" class="step-tool">{{ step.tool }}</span>
- </div>
- </div>
- <!-- tool_result -->
- <div v-else-if="step.type === 'tool_result'" class="detail-step-item">
- <div class="step-dot tool-result"></div>
- <div class="step-text">
- <span class="step-label">结果:</span>
- <span class="step-tool">{{ step.tool }}</span>
- <span class="step-msg">{{ step.message }}</span>
- </div>
- </div>
- <!-- agent_done -->
- <div v-else-if="step.type === 'agent_done'" class="detail-step-item">
- <div class="step-dot agent-done"></div>
- <div class="step-text">{{ step.message }}</div>
- </div>
- </div>
- </div>
- </Transition>
- </div>
- <!-- 输出内容 -->
- <div v-if="detailData.content" class="agent-detail-section">
- <div class="agent-detail-section-header" @click="outputCollapsed = !outputCollapsed">
- <span class="agent-detail-section-title">输出内容</span>
- <div :class="['section-arrow', { collapsed: outputCollapsed }]">
- <div class="arrow-icon"></div>
- </div>
- </div>
- <Transition name="collapse">
- <div v-show="!outputCollapsed" class="agent-detail-section-body output" v-html="renderMarkdown(detailData.content)"></div>
- </Transition>
- </div>
- </div>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, computed } from 'vue';
- import { renderMarkdown } from './utils';
- import type { Message, ThinkingStep } from './types';
- const props = defineProps<{
- messageIndex: number;
- agentName: string;
- agentId?: string;
- messages: Message[];
- hideHeader?: boolean;
- }>();
- const emit = defineEmits<{
- (e: 'close'): void;
- }>();
- const thinkingCollapsed = ref(false);
- const stepsCollapsed = ref(false);
- const outputCollapsed = ref(false);
- const detailData = computed(() => {
- const msg = props.messages[props.messageIndex];
- if (!msg?.thinkingSteps) return null;
- const agentName = props.agentName;
- const agentId = props.agentId;
- let thinkingContent = '';
- let content = '';
- const steps: ThinkingStep[] = [];
- for (const step of msg.thinkingSteps) {
- const match = agentId ? step.agentId === agentId : (step.agent || '系统') === agentName;
- if (match) {
- if (step.thinkingContent) thinkingContent += step.thinkingContent;
- if (step.content) content += step.content;
- steps.push(step);
- }
- }
- return { agent: agentName, thinkingContent, content, steps };
- });
- </script>
- <style scoped lang="less">
- .agent-detail-panel {
- height: 100%;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- .agent-detail-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 16px;
- border-bottom: 1px solid rgba(63, 80, 106, 0.3);
- flex-shrink: 0;
- .agent-detail-header-left {
- display: flex;
- align-items: center;
- gap: 10px;
- min-width: 0;
- }
- .agent-detail-icon {
- width: 20px;
- height: 20px;
- background-image: var(--img-chat-agent-icon);
- background-repeat: no-repeat;
- background-size: 100% 100%;
- flex-shrink: 0;
- }
- .agent-detail-title {
- font-size: 15px;
- font-weight: 600;
- color: #e0e6ed;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .agent-detail-close {
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.2s;
- flex-shrink: 0;
- &:hover {
- background: rgba(10, 132, 255, 0.15);
- }
- .close-icon {
- width: 16px;
- height: 16px;
- background-image: var(--img-chat-close-icon-btn);
- background-repeat: no-repeat;
- background-size: 100% 100%;
- }
- }
- }
- .agent-detail-body {
- flex: 1;
- overflow-y: auto;
- padding: 12px 16px;
- .agent-detail-section {
- margin-bottom: 8px;
- border: 1px solid rgba(63, 80, 106, 0.25);
- border-radius: 8px;
- overflow: hidden;
- background: rgba(10, 13, 23, 0.3);
- &:last-child {
- margin-bottom: 0;
- }
- .agent-detail-section-header {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 10px 14px;
- cursor: pointer;
- user-select: none;
- transition: background 0.2s;
- &:hover {
- background: rgba(10, 132, 255, 0.06);
- }
- .agent-detail-section-title {
- font-size: 13px;
- font-weight: 600;
- color: #8b949e;
- }
- .agent-detail-section-count {
- font-size: 11px;
- color: #8b949e;
- background: rgba(10, 132, 255, 0.1);
- padding: 1px 6px;
- border-radius: 10px;
- }
- .section-arrow {
- margin-left: auto;
- display: flex;
- align-items: center;
- transition: transform 0.25s ease;
- &.collapsed {
- transform: rotate(-90deg);
- }
- .arrow-icon {
- width: 10px;
- height: 10px;
- background-image: var(--img-chat-arrow-icon);
- background-repeat: no-repeat;
- background-size: 100% 100%;
- }
- }
- }
- .agent-detail-section-body {
- padding: 0 14px 12px;
- font-size: 13px;
- line-height: 1.6;
- color: #e0e6ed;
- word-break: break-word;
- &.thinking {
- white-space: pre-wrap;
- color: #b0b8c4;
- max-height: 300px;
- overflow-y: auto;
- }
- &.output {
- max-height: 500px;
- overflow-y: auto;
- }
- :deep(h1),
- :deep(h2),
- :deep(h3),
- :deep(h4),
- :deep(h5),
- :deep(h6) {
- color: #fff;
- }
- :deep(p) {
- margin: 0;
- }
- :deep(code) {
- background: rgba(10, 132, 255, 0.1);
- padding: 1px 4px;
- border-radius: 3px;
- font-size: 12px;
- }
- :deep(pre) {
- background: rgba(10, 132, 255, 0.1);
- padding: 8px;
- border-radius: 4px;
- overflow-x: auto;
- margin: 6px 0;
- white-space: pre-wrap;
- }
- :deep(pre code) {
- background: transparent;
- padding: 0;
- }
- /* 步骤时间线 */
- .detail-step {
- position: relative;
- padding-left: 20px;
- &::before {
- content: '';
- position: absolute;
- left: 5px;
- top: 16px;
- bottom: 0;
- width: 1px;
- background: rgba(63, 80, 106, 0.3);
- }
- &:last-child::before {
- display: none;
- }
- .detail-step-item {
- display: flex;
- align-items: flex-start;
- gap: 10px;
- padding: 8px 0;
- .step-dot {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- flex-shrink: 0;
- margin-top: 4px;
- position: relative;
- z-index: 1;
- &.agent-start {
- background: #0a84ff;
- box-shadow: 0 0 6px rgba(10, 132, 255, 0.4);
- }
- &.tool-call {
- background: #79c0ff;
- }
- &.executing {
- background: #d29922;
- animation: pulse-dot 1.5s ease-in-out infinite;
- }
- &.tool-result {
- background: #3fb950;
- }
- &.agent-done {
- background: #8b949e;
- }
- }
- .step-text {
- flex: 1;
- font-size: 12px;
- line-height: 1.5;
- color: #b0b8c4;
- .step-label {
- color: #8b949e;
- }
- .step-tool {
- color: #79c0ff;
- font-family: monospace;
- font-size: 11px;
- background: rgba(10, 132, 255, 0.1);
- padding: 1px 5px;
- border-radius: 3px;
- }
- .step-msg {
- color: #b0b8c4;
- }
- }
- }
- }
- }
- }
- }
- }
- .collapse-enter-active,
- .collapse-leave-active {
- transition: all 0.25s ease;
- overflow: hidden;
- }
- .collapse-enter-from,
- .collapse-leave-to {
- opacity: 0;
- max-height: 0;
- padding-top: 0;
- padding-bottom: 0;
- margin-top: 0;
- margin-bottom: 0;
- }
- .collapse-enter-to,
- .collapse-leave-from {
- opacity: 1;
- max-height: 500px;
- }
- @keyframes pulse-dot {
- 0%,
- 100% {
- opacity: 1;
- }
- 50% {
- opacity: 0.4;
- }
- }
- </style>
|