Files
EasyFlow/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-timeline/ChatToolCard.vue
陈子默 72df00f25b feat: 全新智能体功能
- 基于先进智能体框架,增加智能体编排功能
- 增加智能体聊天,并对接持久化
2026-05-25 11:42:48 +08:00

381 lines
8.3 KiB
Vue

<script setup lang="ts">
import type {ChatTimelineToolApprovalPayload, ChatTimelineToolItem,} from './types';
import {IconifyIcon} from '@easyflow/icons';
import {computed, ref} from 'vue';
const props = defineProps<{
item: ChatTimelineToolItem;
loading?: boolean;
}>();
const emit = defineEmits<{
approve: [payload: ChatTimelineToolApprovalPayload];
reject: [payload: ChatTimelineToolApprovalPayload];
}>();
const expanded = ref(false);
const isApprovalMode = computed(() => props.item.mode === 'approval');
const canApprove = computed(
() => isApprovalMode.value && props.item.status === 'pending_approval',
);
const hasDetails = computed(() => Boolean(props.item.input));
const statusText = computed(() => {
switch (props.item.status) {
case 'approving': {
return '确认中';
}
case 'error': {
return '失败';
}
case 'pending_approval': {
return '待确认';
}
case 'rejected': {
return '已拒绝';
}
case 'success': {
return '已完成';
}
default: {
return '调用中';
}
}
});
const modeText = computed(() =>
isApprovalMode.value ? '需要确认后执行' : '自动执行',
);
function formatPayload(value: unknown) {
if (value === null || value === undefined || value === '') {
return '';
}
if (typeof value === 'string') {
return value;
}
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function toggleExpanded() {
if (!hasDetails.value) {
return;
}
expanded.value = !expanded.value;
}
function getApprovalPayload() {
return props.item.approval;
}
function handleApprove() {
const payload = getApprovalPayload();
if (!payload || !canApprove.value) {
return;
}
emit('approve', payload);
}
function handleReject() {
const payload = getApprovalPayload();
if (!payload || !canApprove.value) {
return;
}
emit('reject', payload);
}
</script>
<template>
<section
class="chat-tool-card"
:class="[`is-${item.status}`, { 'is-approval': isApprovalMode }]"
>
<button
v-if="!isApprovalMode"
type="button"
class="chat-tool-card__compact"
:class="{ 'is-clickable': hasDetails }"
:disabled="!hasDetails"
@click="toggleExpanded"
>
<span class="chat-tool-card__title">
<IconifyIcon
aria-hidden="true"
class="chat-tool-card__tool-icon"
icon="mdi:hammer"
/>
<span class="chat-tool-card__name">{{ item.toolName }}</span>
</span>
<span class="chat-tool-card__meta">
<span>{{ modeText }}</span>
<span class="chat-tool-card__status">
<span class="chat-tool-card__status-dot" aria-hidden="true"></span>
<span>{{ statusText }}</span>
</span>
</span>
</button>
<template v-else>
<div class="chat-tool-card__approval-head">
<IconifyIcon
aria-hidden="true"
class="chat-tool-card__tool-icon"
icon="mdi:hammer"
/>
<div class="chat-tool-card__heading">
<div class="chat-tool-card__name">{{ item.toolName }}</div>
<div class="chat-tool-card__desc">{{ modeText }}</div>
</div>
<span class="chat-tool-card__status">
<span class="chat-tool-card__status-dot" aria-hidden="true"></span>
<span>{{ statusText }}</span>
</span>
</div>
<pre v-if="item.input" class="chat-tool-card__payload">{{
formatPayload(item.input)
}}</pre>
<div v-if="canApprove" class="chat-tool-card__actions">
<button
type="button"
class="chat-tool-card__button"
:disabled="loading"
@click="handleReject"
>
拒绝
</button>
<button
type="button"
class="chat-tool-card__button is-primary"
:disabled="loading"
@click="handleApprove"
>
批准
</button>
</div>
<div v-if="item.rejectReason" class="chat-tool-card__result-note">
{{ item.rejectReason }}
</div>
</template>
<div
v-if="!isApprovalMode && expanded && hasDetails"
class="chat-tool-card__body"
>
<pre v-if="item.input" class="chat-tool-card__payload">{{
formatPayload(item.input)
}}</pre>
</div>
</section>
</template>
<style scoped>
.chat-tool-card {
max-width: min(78%, 680px);
overflow: hidden;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
}
.chat-tool-card.is-approval {
padding: 16px;
background: var(--el-color-primary-light-9);
border-color: var(--el-color-primary-light-7);
}
.chat-tool-card__compact,
.chat-tool-card__approval-head {
display: flex;
gap: 8px;
align-items: center;
width: 100%;
min-width: 0;
}
.chat-tool-card__compact {
justify-content: space-between;
padding: 10px 12px;
color: inherit;
text-align: left;
background: transparent;
border: 0;
}
.chat-tool-card__compact.is-clickable {
cursor: pointer;
}
.chat-tool-card__compact.is-clickable:hover {
background: var(--el-fill-color-lighter);
}
.chat-tool-card__compact:focus-visible,
.chat-tool-card__button:focus-visible {
outline: 2px solid var(--el-color-primary-light-5);
outline-offset: 2px;
}
.chat-tool-card__approval-head {
align-items: flex-start;
}
.chat-tool-card__title,
.chat-tool-card__meta,
.chat-tool-card__status {
display: inline-flex;
align-items: center;
min-width: 0;
}
.chat-tool-card__title {
gap: 6px;
}
.chat-tool-card__heading {
flex: 1 1 auto;
min-width: 0;
}
.chat-tool-card__name {
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
white-space: nowrap;
}
.chat-tool-card__desc {
margin-top: 2px;
font-size: 12px;
line-height: 18px;
color: var(--el-text-color-secondary);
}
.chat-tool-card__tool-icon {
flex: 0 0 auto;
width: 14px;
height: 14px;
margin-top: 2px;
color: var(--el-text-color-secondary);
}
.chat-tool-card.is-approval .chat-tool-card__tool-icon,
.chat-tool-card.is-pending_approval .chat-tool-card__tool-icon,
.chat-tool-card.is-approving .chat-tool-card__tool-icon,
.chat-tool-card.is-running .chat-tool-card__tool-icon {
color: var(--el-color-primary);
}
.chat-tool-card__meta {
flex: 0 0 auto;
gap: 8px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.chat-tool-card__status {
gap: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.chat-tool-card__status-dot {
width: 6px;
height: 6px;
background: currentColor;
border-radius: 50%;
}
.chat-tool-card.is-approving .chat-tool-card__status,
.chat-tool-card.is-pending_approval .chat-tool-card__status,
.chat-tool-card.is-running .chat-tool-card__status {
color: var(--el-color-primary);
}
.chat-tool-card.is-success .chat-tool-card__status {
color: var(--el-color-success);
}
.chat-tool-card.is-error .chat-tool-card__status,
.chat-tool-card.is-rejected .chat-tool-card__status {
color: var(--el-color-danger);
}
.chat-tool-card__body {
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 12px 12px;
}
.chat-tool-card__payload {
max-height: 180px;
padding: 10px;
margin: 12px 0 0;
overflow: auto;
font-size: 12px;
line-height: 18px;
color: var(--el-text-color-secondary);
white-space: pre-wrap;
background: var(--el-bg-color);
border-radius: 8px;
}
.chat-tool-card__body .chat-tool-card__payload {
margin: 0;
background: var(--el-fill-color-light);
}
.chat-tool-card__actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 12px;
}
.chat-tool-card__button {
min-width: 64px;
height: 32px;
padding: 0 12px;
font-size: 13px;
color: var(--el-text-color-primary);
cursor: pointer;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color);
border-radius: 6px;
}
.chat-tool-card__button:hover {
border-color: var(--el-color-primary-light-5);
}
.chat-tool-card__button:disabled {
cursor: not-allowed;
opacity: 0.62;
}
.chat-tool-card__button.is-primary {
color: var(--el-color-white);
background: var(--el-color-primary);
border-color: var(--el-color-primary);
}
.chat-tool-card__result-note {
margin-top: 12px;
font-size: 12px;
line-height: 18px;
color: var(--el-color-danger);
}
</style>