|
@@ -28,27 +28,31 @@
|
|
|
<pre v-else class="ai-tool-detail__pre ai-scroll-area"><code>{{ pretty(step.input) }}</code></pre>
|
|
<pre v-else class="ai-tool-detail__pre ai-scroll-area"><code>{{ pretty(step.input) }}</code></pre>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 输出:表格类结果已自动导出为产物文件,这里只给指引;其余结果仍结构化渲染 -->
|
|
|
|
|
|
|
+ <!-- 输出:表格类结果与「产物文件卡片」成对出现(原表格位置就是产物位置) -->
|
|
|
<div v-if="step.result" class="ai-tool-detail__block">
|
|
<div v-if="step.result" class="ai-tool-detail__block">
|
|
|
<div class="ai-tool-detail__label">
|
|
<div class="ai-tool-detail__label">
|
|
|
<span>输出</span>
|
|
<span>输出</span>
|
|
|
- <span v-if="artifact" class="ai-tool-detail__meta">
|
|
|
|
|
- 已导出为产物文件<template v-if="artifactRowsText">({{ artifactRowsText }})</template>
|
|
|
|
|
- </span>
|
|
|
|
|
- <span v-else-if="table && table.totalRows != null" class="ai-tool-detail__meta">
|
|
|
|
|
- 共 {{ table.totalRows.toLocaleString() }} 行
|
|
|
|
|
- </span>
|
|
|
|
|
- <span v-else-if="table && table.rows.length" class="ai-tool-detail__meta">{{ table.rows.length }} 条</span>
|
|
|
|
|
- <span v-if="truncated" class="ai-tool-detail__badge">结果过大已截断</span>
|
|
|
|
|
|
|
+ <span v-if="truncated" class="ai-tool-detail__badge">仅回传头部预览行</span>
|
|
|
<span class="ai-tool-detail__spacer"></span>
|
|
<span class="ai-tool-detail__spacer"></span>
|
|
|
<button v-if="table" class="ai-tool-detail__btn" type="button" @click="showRaw = !showRaw">
|
|
<button v-if="table" class="ai-tool-detail__btn" type="button" @click="showRaw = !showRaw">
|
|
|
{{ showRaw ? '表格' : '原始 JSON' }}
|
|
{{ showRaw ? '表格' : '原始 JSON' }}
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <!-- 产物文件卡片:全量 CSV,可预览前 200 行 / 下载完整文件 -->
|
|
|
<div v-if="artifact && !showRaw" class="ai-tool-detail__artifact">
|
|
<div v-if="artifact && !showRaw" class="ai-tool-detail__artifact">
|
|
|
- <Icon icon="mdi:file-delimited-outline" :size="14" />
|
|
|
|
|
- <span>结果已导出为 CSV 产物文件,在下方产物卡片或右侧产物栏中预览 / 下载</span>
|
|
|
|
|
|
|
+ <span class="ai-tool-detail__artifact-icon">
|
|
|
|
|
+ <Icon icon="mdi:file-delimited-outline" :size="15" />
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="ai-tool-detail__artifact-name" :title="artifact.fileName">{{ artifact.fileName }}</span>
|
|
|
|
|
+ <span v-if="artifactRowsText" class="ai-tool-detail__artifact-meta">{{ artifactRowsText }}</span>
|
|
|
|
|
+ <span class="ai-tool-detail__artifact-sp"></span>
|
|
|
|
|
+ <button type="button" title="预览前 200 行" @click="onPreview">
|
|
|
|
|
+ <Icon icon="mdi:eye-outline" :size="14" />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button type="button" title="下载完整 CSV" @click="onDownload">
|
|
|
|
|
+ <Icon icon="mdi:download-outline" :size="14" />
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<a-table
|
|
<a-table
|
|
@@ -93,8 +97,11 @@
|
|
|
import { Table } from 'ant-design-vue';
|
|
import { Table } from 'ant-design-vue';
|
|
|
import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue';
|
|
import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue';
|
|
|
import Icon from '@/core/components/Icon/src/Icon.vue';
|
|
import Icon from '@/core/components/Icon/src/Icon.vue';
|
|
|
|
|
+ import { useMessage } from '@/core/hooks/web/useMessage';
|
|
|
import type { ToolStep } from '../api/types';
|
|
import type { ToolStep } from '../api/types';
|
|
|
import { parseJsonPayload } from '../utils/parseMessageContent';
|
|
import { parseJsonPayload } from '../utils/parseMessageContent';
|
|
|
|
|
+ import { downloadArtifactFile } from '../api/artifactApi';
|
|
|
|
|
+ import { openArtifactPreview } from '../utils/artifacts';
|
|
|
|
|
|
|
|
const ATable = Table;
|
|
const ATable = Table;
|
|
|
|
|
|
|
@@ -103,7 +110,16 @@
|
|
|
/** 入参里的 SQL 超过这个行数默认折叠 */
|
|
/** 入参里的 SQL 超过这个行数默认折叠 */
|
|
|
const SQL_COLLAPSE_LINES = 4;
|
|
const SQL_COLLAPSE_LINES = 4;
|
|
|
|
|
|
|
|
- const props = defineProps<{ step: ToolStep }>();
|
|
|
|
|
|
|
+ const props = withDefaults(
|
|
|
|
|
+ defineProps<{
|
|
|
|
|
+ step: ToolStep;
|
|
|
|
|
+ /** 所属消息 ID(产物预览/定位用;时间线详情里可为空) */
|
|
|
|
|
+ messageId?: string;
|
|
|
|
|
+ }>(),
|
|
|
|
|
+ { messageId: '' },
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ const { createMessage } = useMessage();
|
|
|
|
|
|
|
|
const showRaw = ref(false);
|
|
const showRaw = ref(false);
|
|
|
const showInputRaw = ref(false);
|
|
const showInputRaw = ref(false);
|
|
@@ -138,6 +154,26 @@
|
|
|
typeof artifact.value?.totalRows === 'number' ? `${artifact.value.totalRows.toLocaleString()} 行` : '',
|
|
typeof artifact.value?.totalRows === 'number' ? `${artifact.value.totalRows.toLocaleString()} 行` : '',
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
|
|
+ /** 与本步骤成对的产物对象(预览/下载用) */
|
|
|
|
|
+ const sessionArtifact = computed(() => {
|
|
|
|
|
+ if (!artifact.value) return null;
|
|
|
|
|
+ return { ...artifact.value, messageId: props.messageId, toolName: props.step.toolName };
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ function onPreview() {
|
|
|
|
|
+ if (sessionArtifact.value) openArtifactPreview(sessionArtifact.value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function onDownload() {
|
|
|
|
|
+ if (!sessionArtifact.value) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await downloadArtifactFile(sessionArtifact.value);
|
|
|
|
|
+ createMessage.success(`已开始下载 ${sessionArtifact.value.fileName}`);
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ createMessage.error(err?.message || '下载失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const table = computed<DetailTable | null>(() => {
|
|
const table = computed<DetailTable | null>(() => {
|
|
|
if (!props.step.result || payload.value === undefined || artifact.value) return null;
|
|
if (!props.step.result || payload.value === undefined || artifact.value) return null;
|
|
|
return buildTable(payload.value);
|
|
return buildTable(payload.value);
|
|
@@ -316,6 +352,69 @@
|
|
|
font-size: 12px;
|
|
font-size: 12px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /* 与工具结果成对的产物文件卡片(原表格位置) */
|
|
|
|
|
+ &__artifact {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ padding: 7px 8px;
|
|
|
|
|
+ font-size: 12.5px;
|
|
|
|
|
+ color: var(--ai-text);
|
|
|
|
|
+ background: var(--ai-bg-subtle);
|
|
|
|
|
+ border: 1px solid var(--ai-border);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+
|
|
|
|
|
+ &-icon {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ flex: 0 0 auto;
|
|
|
|
|
+ width: 26px;
|
|
|
|
|
+ height: 26px;
|
|
|
|
|
+ color: var(--ai-primary);
|
|
|
|
|
+ background: var(--ai-primary-soft);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &-name {
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &-meta {
|
|
|
|
|
+ flex: 0 0 auto;
|
|
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
|
|
+ color: var(--ai-text-2);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &-sp {
|
|
|
|
|
+ flex: 1 1 auto;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ button {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ flex: 0 0 auto;
|
|
|
|
|
+ width: 24px;
|
|
|
|
|
+ height: 24px;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ color: var(--ai-text-2);
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ color: var(--ai-primary);
|
|
|
|
|
+ background: var(--ai-bg-page);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
&__empty {
|
|
&__empty {
|
|
|
font-size: 11px;
|
|
font-size: 11px;
|
|
|
color: var(--ai-text-2);
|
|
color: var(--ai-text-2);
|