test news page

This commit is contained in:
锦恢 2025-07-06 16:47:41 +08:00
parent bc38ea49bb
commit 793262f8b1
13 changed files with 210 additions and 52 deletions

View File

@ -1,14 +1,11 @@
<template> <template>
<div class="diagram-item-record" v-if="props.dataView && props.dataView.tool"> <div class="diagram-item-record" v-if="props.dataView && props.dataView.tool">
<div class="item-status" :class="props.dataView.status">{{ props.dataView.status }}</div>
<div class="item-header"> <div class="item-header">
<span class="item-title">{{ props.dataView.tool.name }}</span> <span class="item-title">{{ props.dataView.tool.name }}</span>
</div> </div>
<div class="item-desc">{{ props.dataView.tool.description }}</div> <div class="item-desc">{{ props.dataView.tool.description }}</div>
<br>
<div v-if="props.dataView.function !== undefined" class="item-result"> <div v-if="props.dataView.function !== undefined" class="item-result">
<span class="item-label">Arguments</span> <span class="item-label">Arguments</span>
@ -17,8 +14,6 @@
</div> </div>
</div> </div>
<br>
<div v-if="props.dataView.result !== undefined" class="item-result"> <div v-if="props.dataView.result !== undefined" class="item-result">
<span class="item-label">Result</span> <span class="item-label">Result</span>
<template v-if="Array.isArray(props.dataView.result)"> <template v-if="Array.isArray(props.dataView.result)">
@ -33,6 +28,49 @@
v-else-if="typeof props.dataView.result === 'string'">{{ props.dataView.result }}</pre> v-else-if="typeof props.dataView.result === 'string'">{{ props.dataView.result }}</pre>
<pre class="item-json" v-else>{{ formatJson(props.dataView.result) }}</pre> <pre class="item-json" v-else>{{ formatJson(props.dataView.result) }}</pre>
</div> </div>
<br>
<div class="item-meta">
<span v-if="props.dataView.createAt" class="item-meta-label">
Created: <b>{{ formatTime(props.dataView.createAt) }}</b>
</span>
<!-- <span v-if="props.dataView.finishAt" class="item-meta-label">
Finished: <b>{{ formatTime(props.dataView.finishAt) }}</b>
</span> -->
<!-- -->
</div>
<div class="item-timecost" v-if="props.dataView.llmTimecost !== undefined || props.dataView.toolcallTimecost !== undefined">
<template v-if="props.dataView.llmTimecost !== undefined && props.dataView.toolcallTimecost !== undefined">
<div class="timecost-bar">
<div
class="timecost-segment llm"
:style="{ width: llmPercent + '%' }"
:title="`LLM: ${props.dataView.llmTimecost}ms`"
></div>
<div
class="timecost-segment toolcall"
:style="{ width: toolcallPercent + '%' }"
:title="`ToolCall: ${props.dataView.toolcallTimecost}ms`"
></div>
</div>
<div class="timecost-labels">
<span class="llm-label">LLM: <b>{{ props.dataView.llmTimecost }}ms</b> ({{ llmPercent }}%)</span>
<span class="toolcall-label">ToolCall: <b>{{ props.dataView.toolcallTimecost }}ms</b> ({{ toolcallPercent }}%)</span>
</div>
</template>
<template v-else-if="props.dataView.llmTimecost !== undefined">
<div class="timecost-labels">
<span class="llm-label">LLM: <b>{{ props.dataView.llmTimecost }}ms</b></span>
</div>
</template>
<template v-else>
<div class="timecost-labels">
<span class="toolcall-label">ToolCall: <b>{{ props.dataView.toolcallTimecost }}ms</b></span>
</div>
</template>
</div>
</div> </div>
<div v-else class="diagram-item-record"> <div v-else class="diagram-item-record">
<div class="item-header"> <div class="item-header">
@ -47,6 +85,7 @@ import type { PropType } from 'vue';
import type { NodeDataView } from './diagram'; import type { NodeDataView } from './diagram';
import JsonRender from '@/components/json-render/index.vue'; import JsonRender from '@/components/json-render/index.vue';
import { computed } from 'vue';
const props = defineProps({ const props = defineProps({
dataView: { dataView: {
@ -62,6 +101,27 @@ function formatJson(obj: any) {
return String(obj) return String(obj)
} }
} }
//
function formatTime(val: string | number | Date) {
if (!val) return '-';
const d = new Date(val);
if (isNaN(d.getTime())) return '-';
return d.toLocaleString();
}
const llmPercent = computed(() => {
const l = props.dataView!.llmTimecost ?? 0;
const t = props.dataView!.toolcallTimecost ?? 0;
const sum = l + t;
return sum > 0 ? Math.round((l / sum) * 100) : 0;
});
const toolcallPercent = computed(() => {
const l = props.dataView!.llmTimecost ?? 0;
const t = props.dataView!.toolcallTimecost ?? 0;
const sum = l + t;
return sum > 0 ? Math.round((t / sum) * 100) : 0;
});
</script> </script>
<style scoped> <style scoped>
@ -94,32 +154,25 @@ function formatJson(obj: any) {
text-transform: capitalize; text-transform: capitalize;
} }
.item-status.running {
color: #2196f3;
}
.item-status.success {
color: #43a047;
}
.item-status.error {
color: #e53935;
}
.item-status.waiting {
color: #aaa;
}
.item-status.default {
color: #888;
}
.item-desc { .item-desc {
margin-bottom: 8px; margin-bottom: 15px;
opacity: 0.8; opacity: 0.8;
font-size: 14px; font-size: 14px;
} }
.item-meta {
margin-bottom: 8px;
display: flex;
gap: 12px;
font-size: 13px;
color: #888;
flex-wrap: wrap;
}
.item-meta-label b {
color: var(--main-color, #409EFF);
font-weight: 500;
}
.item-label { .item-label {
font-weight: 500; font-weight: 500;
margin-right: 4px; margin-right: 4px;
@ -148,6 +201,7 @@ function formatJson(obj: any) {
.item-result { .item-result {
margin-top: 6px; margin-top: 6px;
margin-bottom: 15px;
} }
.result-block { .result-block {
@ -158,7 +212,6 @@ function formatJson(obj: any) {
max-width: 100%; max-width: 100%;
} }
.result-block.error { .result-block.error {
background-color: rgba(245, 108, 108, 0.5); background-color: rgba(245, 108, 108, 0.5);
} }
@ -166,4 +219,49 @@ function formatJson(obj: any) {
.result-block.success { .result-block.success {
background-color: rgba(67, 160, 71, 0.5); background-color: rgba(67, 160, 71, 0.5);
} }
.item-timecost {
margin-bottom: 10px;
margin-top: 2px;
font-size: 14px;
color: #333;
}
.timecost-bar {
display: flex;
height: 12px;
border-radius: 6px;
overflow: hidden;
background: #f0eaff;
margin-bottom: 4px;
box-shadow: 0 1px 2px rgba(185,136,209,0.08);
}
.timecost-segment {
height: 100%;
transition: width 0.3s;
}
.llm {
background: #B988D1;
}
.toolcall {
background: #A1A7F6;
}
.timecost-labels {
display: flex;
gap: 18px;
font-size: 12px;
color: #888;
}
.llm-label b {
color: #B988D1;
}
.toolcall-label b {
color: #A1A7F6;
}
</style> </style>

View File

@ -41,6 +41,10 @@ export interface NodeDataView {
tool: ToolItem; tool: ToolItem;
status: 'default' | 'running' | 'waiting' | 'success' | 'error'; status: 'default' | 'running' | 'waiting' | 'success' | 'error';
function?: ChatCompletionChunk.Choice.Delta.ToolCall.Function; function?: ChatCompletionChunk.Choice.Delta.ToolCall.Function;
createAt?: number;
llmTimecost?: number;
toolcallTimecost?: number;
finishAt?: number;
result?: any; result?: any;
} }
@ -168,6 +172,8 @@ export async function makeNodeTest(
} }
dataView.status = 'running'; dataView.status = 'running';
const createAt = Date.now();
dataView.createAt = createAt;
context.render(); context.render();
try { try {
@ -197,20 +203,19 @@ export async function makeNodeTest(
loop.registerOnToolCall(toolCall => { loop.registerOnToolCall(toolCall => {
dataView.function = toolCall.function; dataView.function = toolCall.function;
dataView.llmTimecost = Date.now() - createAt;
if (toolCall.function?.name === dataView.tool?.name) { if (toolCall.function?.name === dataView.tool?.name) {
try { try {
const toolArgs = JSON.parse(toolCall.function?.arguments || '{}'); const toolArgs = JSON.parse(toolCall.function?.arguments || '{}');
aiMockJson = toolArgs; aiMockJson = toolArgs;
} catch (e) { } catch (e) {
// ElMessage.error('AI 生成的 JSON 解析错误');
dataView.status = 'error'; dataView.status = 'error';
dataView.result = t('ai-gen-error-json'); dataView.result = t('ai-gen-error-json');
context.render(); context.render();
loop.abort(); loop.abort();
} }
} else { } else {
// ElMessage.error('AI 调用了未知的工具');
dataView.status = 'error'; dataView.status = 'error';
dataView.result = t('ai-invoke-unknown-tool') + ' ' + toolCall.function?.name; dataView.result = t('ai-invoke-unknown-tool') + ' ' + toolCall.function?.name;
context.render(); context.render();
@ -220,6 +225,8 @@ export async function makeNodeTest(
}); });
loop.registerOnToolCalled(toolCalled => { loop.registerOnToolCalled(toolCalled => {
dataView.toolcallTimecost = Date.now() - createAt - dataView.llmTimecost!;
if (toolCalled.state === MessageState.Success) { if (toolCalled.state === MessageState.Success) {
dataView.status = 'success'; dataView.status = 'success';
dataView.result = toolCalled.content; dataView.result = toolCalled.content;
@ -240,6 +247,8 @@ export async function makeNodeTest(
await loop.start(chatStorage, usePrompt); await loop.start(chatStorage, usePrompt);
} finally { } finally {
dataView.finishAt = Date.now();
if (dataView.status === 'running') { if (dataView.status === 'running') {
dataView.status = 'success'; dataView.status = 'success';
context.render(); context.render();

View File

@ -2,7 +2,13 @@
<div style="display: flex; align-items: flex-start; gap: 32px;"> <div style="display: flex; align-items: flex-start; gap: 32px;">
<div ref="svgContainer" class="diagram-container"></div> <div ref="svgContainer" class="diagram-container"></div>
<div class="diagram-info-panel"> <div class="diagram-info-panel">
<div style="display: flex; justify-content: flex-end; align-items: center;"> <div style="display: flex; justify-content: space-between; align-items: center;">
<div v-if="infoNodeId && state.dataView.get(infoNodeId)" class="item-status" :class="state.dataView.get(infoNodeId)?.status || 'waiting'">
{{ state.dataView.get(infoNodeId)?.status || 'waiting' }}
</div>
<div v-else>
{{ "Unknown Status" }}
</div>
<el-button <el-button
circle circle
size="small" size="small"
@ -21,10 +27,9 @@
</template> </template>
<template v-else> <template v-else>
<div class="diagram-info-empty"> <div class="diagram-info-empty">
<el-icon style="font-size: 32px; color: #bbb; margin-bottom: 8px;"> <div style="color: #bbb; font-size: 15px;">
<i-ep-InfoFilled /> {{ t('diagram-node-empty') }}
</el-icon> </div>
<div style="color: #bbb; font-size: 15px;">暂无节点信息</div>
</div> </div>
</template> </template>
</div> </div>
@ -80,7 +85,11 @@ if (autoDetectDiagram) {
tool: item.tool, tool: item.tool,
function: item.function, function: item.function,
status: item.status || 'waiting', status: item.status || 'waiting',
result: item.result || null result: item.result || null,
createAt: item.createAt,
finishAt: item.finishAt,
llmTimecost: item.llmTimecost,
toolcallTimecost: item.toolcallTimecost
}); });
}); });
} else { } else {
@ -90,9 +99,6 @@ if (autoDetectDiagram) {
}; };
} }
console.log(tabStorage.autoDetectDiagram!.views);
console.log(state.dataView);
let cancelHoverHandler: NodeJS.Timeout | undefined = undefined; let cancelHoverHandler: NodeJS.Timeout | undefined = undefined;
@ -216,7 +222,7 @@ const drawDiagram = async () => {
// dataView // dataView
state.dataView.set(tool.name, { state.dataView.set(tool.name, {
tool, tool,
status: 'waiting' status: 'waiting',
}); });
} }
} }
@ -728,4 +734,25 @@ function getNodePopupStyle(node: any): any {
opacity: 0.85; opacity: 0.85;
font-size: 15px; font-size: 15px;
} }
.item-status.running {
color: var(--main-color);
}
.item-status.success {
color: #43a047;
}
.item-status.error {
color: #e53935;
}
.item-status.waiting {
color: #aaa;
}
.item-status.default {
color: #888;
}
</style> </style>

View File

@ -58,7 +58,7 @@
<el-tooltip <el-tooltip
placement="top" placement="top"
effect="light" effect="light"
content="点击连接线取消连接,点击节点以创建连接" :content="t('self-detect-caption')"
> >
<span class="iconfont icon-about"></span> <span class="iconfont icon-about"></span>
</el-tooltip> </el-tooltip>
@ -158,8 +158,14 @@ async function onTestConfirm() {
tool: view.tool, tool: view.tool,
status: view.status, status: view.status,
function: view.function, function: view.function,
result: view.result result: view.result,
createAt: view.createAt,
finishAt: view.finishAt,
llmTimecost: view.llmTimecost,
toolcallTimecost: view.toolcallTimecost,
}); });
context.render();
} }
} }
} }

View File

@ -195,5 +195,7 @@
"ai-invoke-unknown-tool": "استدعت الذكاء الاصطناعي أداة غير معروفة", "ai-invoke-unknown-tool": "استدعت الذكاء الاصطناعي أداة غير معروفة",
"click-edge-to-delete": "انقر على الحافة للحذف", "click-edge-to-delete": "انقر على الحافة للحذف",
"select-node-define-test-tomo": "اختر عقدة أخرى لتحديد طوبولوجيا الاختبار", "select-node-define-test-tomo": "اختر عقدة أخرى لتحديد طوبولوجيا الاختبار",
"tool-self-detect": "الفحص الذاتي للأداة" "tool-self-detect": "الفحص الذاتي للأداة",
"self-detect-caption": "انقر على خط الاتصال لإلغاء التوصيل، انقر على العقدة لإنشاء اتصال",
"diagram-node-empty": "لا توجد معلومات عن العقدة"
} }

View File

@ -195,5 +195,7 @@
"ai-invoke-unknown-tool": "KI hat ein unbekanntes Tool aufgerufen", "ai-invoke-unknown-tool": "KI hat ein unbekanntes Tool aufgerufen",
"click-edge-to-delete": "Klicken Sie auf die Kante, um sie zu löschen", "click-edge-to-delete": "Klicken Sie auf die Kante, um sie zu löschen",
"select-node-define-test-tomo": "Wählen Sie einen anderen Knoten aus, um die Testtopologie zu definieren", "select-node-define-test-tomo": "Wählen Sie einen anderen Knoten aus, um die Testtopologie zu definieren",
"tool-self-detect": "Werkzeug-Selbsttest" "tool-self-detect": "Werkzeug-Selbsttest",
"self-detect-caption": "Klicken Sie auf die Verbindungslinie, um die Verbindung aufzuheben, klicken Sie auf den Knoten, um eine Verbindung herzustellen",
"diagram-node-empty": "Keine Knoteninformationen"
} }

View File

@ -195,5 +195,7 @@
"ai-invoke-unknown-tool": "AI called an unknown tool", "ai-invoke-unknown-tool": "AI called an unknown tool",
"click-edge-to-delete": "Click the edge to delete", "click-edge-to-delete": "Click the edge to delete",
"select-node-define-test-tomo": "Select another node to define the test topology", "select-node-define-test-tomo": "Select another node to define the test topology",
"tool-self-detect": "Tool Self-Check" "tool-self-detect": "Tool Self-Check",
"self-detect-caption": "Click on the connection line to cancel the connection, click on the node to create a connection",
"diagram-node-empty": "No node information"
} }

View File

@ -195,5 +195,7 @@
"ai-invoke-unknown-tool": "L'IA a appelé un outil inconnu", "ai-invoke-unknown-tool": "L'IA a appelé un outil inconnu",
"click-edge-to-delete": "Cliquez sur le bord pour supprimer", "click-edge-to-delete": "Cliquez sur le bord pour supprimer",
"select-node-define-test-tomo": "Sélectionnez un autre nœud pour définir la topologie de test", "select-node-define-test-tomo": "Sélectionnez un autre nœud pour définir la topologie de test",
"tool-self-detect": "Auto-vérification de l'outil" "tool-self-detect": "Auto-vérification de l'outil",
"self-detect-caption": "Cliquez sur la ligne de connexion pour annuler la connexion, cliquez sur le nœud pour créer une connexion",
"diagram-node-empty": "Aucune information sur le nœud"
} }

View File

@ -195,5 +195,7 @@
"ai-invoke-unknown-tool": "AIが未知のツールを呼び出しました", "ai-invoke-unknown-tool": "AIが未知のツールを呼び出しました",
"click-edge-to-delete": "クリックして削除", "click-edge-to-delete": "クリックして削除",
"select-node-define-test-tomo": "テストトポロジを定義するために別のノードを選択してください", "select-node-define-test-tomo": "テストトポロジを定義するために別のノードを選択してください",
"tool-self-detect": "ツール自己診断" "tool-self-detect": "ツール自己診断",
"self-detect-caption": "接続線をクリックして接続を解除し、ノードをクリックして接続を作成します",
"diagram-node-empty": "ノード情報なし"
} }

View File

@ -195,5 +195,7 @@
"ai-invoke-unknown-tool": "AI가 알 수 없는 도구를 호출했습니다", "ai-invoke-unknown-tool": "AI가 알 수 없는 도구를 호출했습니다",
"click-edge-to-delete": "가장자리를 클릭하여 삭제", "click-edge-to-delete": "가장자리를 클릭하여 삭제",
"select-node-define-test-tomo": "테스트 토폴로지를 정의하려면 다른 노드를 선택하세요", "select-node-define-test-tomo": "테스트 토폴로지를 정의하려면 다른 노드를 선택하세요",
"tool-self-detect": "도구 자체 점검" "tool-self-detect": "도구 자체 점검",
"self-detect-caption": "연결선을 클릭하여 연결을 취소하고, 노드를 클릭하여 연결을 생성합니다",
"diagram-node-empty": "노드 정보 없음"
} }

View File

@ -195,5 +195,7 @@
"ai-invoke-unknown-tool": "ИИ вызвал неизвестный инструмент", "ai-invoke-unknown-tool": "ИИ вызвал неизвестный инструмент",
"click-edge-to-delete": "Нажмите на край, чтобы удалить", "click-edge-to-delete": "Нажмите на край, чтобы удалить",
"select-node-define-test-tomo": "Выберите другой узел для определения тестовой топологии", "select-node-define-test-tomo": "Выберите другой узел для определения тестовой топологии",
"tool-self-detect": "Самопроверка инструмента" "tool-self-detect": "Самопроверка инструмента",
"self-detect-caption": "Нажмите на линию соединения, чтобы отменить соединение, нажмите на узел, чтобы создать соединение",
"diagram-node-empty": "Нет информации об узле"
} }

View File

@ -195,5 +195,7 @@
"ai-invoke-unknown-tool": "AI 调用了未知的工具", "ai-invoke-unknown-tool": "AI 调用了未知的工具",
"click-edge-to-delete": "点击边以删除", "click-edge-to-delete": "点击边以删除",
"select-node-define-test-tomo": "选择另一个节点以定义测试拓扑", "select-node-define-test-tomo": "选择另一个节点以定义测试拓扑",
"tool-self-detect": "工具自检" "tool-self-detect": "工具自检",
"self-detect-caption": "点击连接线取消连接,点击节点以创建连接",
"diagram-node-empty": "暂无节点信息"
} }

View File

@ -195,5 +195,7 @@
"ai-invoke-unknown-tool": "AI調用了未知的工具", "ai-invoke-unknown-tool": "AI調用了未知的工具",
"click-edge-to-delete": "點擊邊緣以刪除", "click-edge-to-delete": "點擊邊緣以刪除",
"select-node-define-test-tomo": "選擇另一個節點以定義測試拓撲", "select-node-define-test-tomo": "選擇另一個節點以定義測試拓撲",
"tool-self-detect": "工具自檢" "tool-self-detect": "工具自檢",
"self-detect-caption": "點擊連接線取消連接,點擊節點以創建連接",
"diagram-node-empty": "暫無節點資訊"
} }