feat: 优化 Agent Studio 试运行交互
- 将内置工具配置拆分为独立页签 - 支持试运行面板平滑居中展开和快捷收起 - 运行和审批期间禁用会话清理
This commit is contained in:
@@ -1,24 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import baseFormSource from './components/AgentBaseForm.vue?raw';
|
||||
import builtinToolsFormSource from './components/AgentBuiltinToolsForm.vue?raw';
|
||||
import inspectorSource from './components/AgentInspectorPanel.vue?raw';
|
||||
import tryoutPanelSource from './components/AgentTryoutPanel.vue?raw';
|
||||
|
||||
describe('agent Studio responsive layout contract', () => {
|
||||
it('keeps the inspector usable at the 768px breakpoint', () => {
|
||||
expect(inspectorSource).toContain('@media (max-width: 900px)');
|
||||
expect(inspectorSource).toMatch(
|
||||
/@media \(max-width: 900px\)[\s\S]*?left: var\(--space-4\);[\s\S]*?width: auto;/,
|
||||
/@media \(max-width: 900px\)[\s\S]*?inset:[\s\S]*?var\(--space-4\)[\s\S]*?96px;/,
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the inspector and builtin tool controls usable at 375px', () => {
|
||||
expect(inspectorSource).toMatch(
|
||||
/@media \(max-width: 480px\)[\s\S]*?right: var\(--space-2\);[\s\S]*?left: var\(--space-2\);/,
|
||||
/@media \(max-width: 480px\)[\s\S]*?inset:[\s\S]*?var\(--space-2\)[\s\S]*?96px;/,
|
||||
);
|
||||
expect(baseFormSource).toMatch(
|
||||
expect(builtinToolsFormSource).toMatch(
|
||||
/@media \(max-width: 480px\)[\s\S]*?--agent-tool-approval-column: 72px;/,
|
||||
);
|
||||
expect(baseFormSource).toContain('white-space: normal;');
|
||||
expect(baseFormSource).toContain('-webkit-line-clamp: 2;');
|
||||
expect(builtinToolsFormSource).toContain('white-space: normal;');
|
||||
expect(builtinToolsFormSource).toContain('-webkit-line-clamp: 2;');
|
||||
});
|
||||
|
||||
it('supports a centered animated tryout surface without remounting it', () => {
|
||||
expect(inspectorSource).toContain('label="内置工具"');
|
||||
expect(inspectorSource).toContain(':expanded="tryoutExpanded"');
|
||||
expect(inspectorSource).toContain("'is-tryout-expanded'");
|
||||
expect(inspectorSource).toContain('--motion-duration-medium');
|
||||
expect(inspectorSource).toContain(
|
||||
'@media (prefers-reduced-motion: reduce)',
|
||||
);
|
||||
expect(tryoutPanelSource).toContain(':aria-pressed="Boolean(expanded)"');
|
||||
});
|
||||
|
||||
it('keeps clear disabled throughout active output and async cleanup', () => {
|
||||
expect(tryoutPanelSource).toContain(
|
||||
':disabled="loading || approvalLoading || clearing"',
|
||||
);
|
||||
expect(tryoutPanelSource).toContain(
|
||||
'if (loading.value || approvalLoading.value || clearing.value)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-mutating-props */
|
||||
import type { AgentBuiltinToolKey } from '../builtin-tools';
|
||||
import type { AgentInfo, AgentOption, AgentToolBinding } from '../types';
|
||||
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessageBox,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
@@ -21,7 +19,6 @@ import { resolveAgentCompressionTokenThreshold } from '../compression-threshold'
|
||||
|
||||
const props = defineProps<{
|
||||
agent: AgentInfo;
|
||||
canDisableShellApproval: boolean;
|
||||
categories: AgentOption[];
|
||||
models: AgentOption[];
|
||||
toolBindings: AgentToolBinding[];
|
||||
@@ -35,92 +32,6 @@ const visibilityScopeOptions = [
|
||||
{ label: '公开', value: 'PUBLIC' },
|
||||
];
|
||||
|
||||
const builtinToolOptions: Array<{
|
||||
description: string;
|
||||
key: AgentBuiltinToolKey;
|
||||
label: string;
|
||||
}> = [
|
||||
{ description: '读取工作区内的 UTF-8 文本', key: 'read', label: '读取文件' },
|
||||
{ description: '在工作区内写入或插入文本', key: 'write', label: '写入文件' },
|
||||
{
|
||||
description: '以补丁方式安全修改工作区文件',
|
||||
key: 'patch',
|
||||
label: '补丁修改',
|
||||
},
|
||||
{ description: '执行平台白名单中的受控命令', key: 'shell', label: 'Shell' },
|
||||
{
|
||||
description: '将工作区文件发布为可下载产物',
|
||||
key: 'artifactPublish',
|
||||
label: '发布产物',
|
||||
},
|
||||
];
|
||||
|
||||
function builtinTool(key: AgentBuiltinToolKey) {
|
||||
return props.agent.executionConfigJson!.builtinTools![key];
|
||||
}
|
||||
|
||||
function handleBuiltinToolEnabledChange(key: AgentBuiltinToolKey) {
|
||||
if (key === 'shell' && !props.canDisableShellApproval) {
|
||||
builtinTool('shell').approvalRequired = true;
|
||||
delete props.agent.executionConfigJson!.builtinTools!
|
||||
.shellApprovalRiskConfirmed;
|
||||
}
|
||||
emit('change');
|
||||
}
|
||||
|
||||
async function confirmUnsafeShellExecution() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'关闭后,命令和脚本将以后端服务权限在宿主环境运行,当前没有沙箱隔离。确认继续?',
|
||||
'关闭 Shell 调用确认',
|
||||
{
|
||||
cancelButtonText: '保持开启',
|
||||
confirmButtonText: '确认关闭',
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
props.agent.executionConfigJson!.builtinTools!.shellApprovalRiskConfirmed = true;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmBuiltinToolEnabledChange(key: AgentBuiltinToolKey) {
|
||||
if (
|
||||
key !== 'shell' ||
|
||||
builtinTool('shell').enabled ||
|
||||
builtinTool('shell').approvalRequired
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (!props.canDisableShellApproval) {
|
||||
builtinTool('shell').approvalRequired = true;
|
||||
return true;
|
||||
}
|
||||
return confirmUnsafeShellExecution();
|
||||
}
|
||||
|
||||
async function confirmShellApprovalChange() {
|
||||
if (!props.canDisableShellApproval) {
|
||||
return false;
|
||||
}
|
||||
if (!builtinTool('shell').approvalRequired) {
|
||||
delete props.agent.executionConfigJson!.builtinTools!
|
||||
.shellApprovalRiskConfirmed;
|
||||
return true;
|
||||
}
|
||||
return confirmUnsafeShellExecution();
|
||||
}
|
||||
|
||||
function handleApprovalChange(key: AgentBuiltinToolKey, value: unknown) {
|
||||
if (key === 'shell' && value === true) {
|
||||
delete props.agent.executionConfigJson!.builtinTools!
|
||||
.shellApprovalRiskConfirmed;
|
||||
}
|
||||
emit('change');
|
||||
}
|
||||
|
||||
function handleModelChange(modelId: AgentInfo['modelId']) {
|
||||
const selectedModel = props.models.find(
|
||||
(model) => model.value === String(modelId ?? ''),
|
||||
@@ -268,66 +179,6 @@ function handleModelChange(modelId: AgentInfo['modelId']) {
|
||||
@input="emit('change')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<section
|
||||
class="agent-form__builtin-tools"
|
||||
aria-labelledby="builtin-tools-title"
|
||||
>
|
||||
<div class="agent-form__section-head">
|
||||
<div>
|
||||
<div id="builtin-tools-title" class="agent-form__section-title">
|
||||
内置工具
|
||||
</div>
|
||||
<div class="agent-form__section-description">
|
||||
为当前智能体配置工作区能力
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-form__tool-columns" aria-hidden="true">
|
||||
<span>启用</span>
|
||||
<span>调用前确认</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-form__tool-list">
|
||||
<div
|
||||
v-for="item in builtinToolOptions"
|
||||
:key="item.key"
|
||||
class="agent-form__tool-row"
|
||||
>
|
||||
<div class="agent-form__tool-copy">
|
||||
<div class="agent-form__tool-name">{{ item.label }}</div>
|
||||
<div class="agent-form__tool-description">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
</div>
|
||||
<ElSwitch
|
||||
v-model="builtinTool(item.key).enabled"
|
||||
:aria-label="`启用${item.label}`"
|
||||
:before-change="() => confirmBuiltinToolEnabledChange(item.key)"
|
||||
@change="handleBuiltinToolEnabledChange(item.key)"
|
||||
/>
|
||||
<ElTooltip
|
||||
:disabled="item.key !== 'shell' || canDisableShellApproval"
|
||||
content="Shell 调用确认仅平台超级管理员可以关闭"
|
||||
effect="light"
|
||||
placement="top"
|
||||
>
|
||||
<span class="agent-form__tool-approval">
|
||||
<ElSwitch
|
||||
v-model="builtinTool(item.key).approvalRequired"
|
||||
:aria-label="`${item.label}调用前确认`"
|
||||
:before-change="
|
||||
item.key === 'shell' ? confirmShellApprovalChange : undefined
|
||||
"
|
||||
:disabled="
|
||||
!builtinTool(item.key).enabled ||
|
||||
(item.key === 'shell' && !canDisableShellApproval)
|
||||
"
|
||||
@change="handleApprovalChange(item.key, $event)"
|
||||
/>
|
||||
</span>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="agent-form__grid">
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
@@ -396,89 +247,6 @@ function handleModelChange(modelId: AgentInfo['modelId']) {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agent-form__builtin-tools {
|
||||
--agent-tool-approval-column: 96px;
|
||||
--agent-tool-enabled-column: 48px;
|
||||
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.agent-form__section-head {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(0, 1fr)
|
||||
calc(var(--agent-tool-enabled-column) + var(--agent-tool-approval-column));
|
||||
gap: var(--space-3);
|
||||
align-items: end;
|
||||
padding-bottom: var(--space-2);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.agent-form__section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.agent-form__section-description,
|
||||
.agent-form__tool-description,
|
||||
.agent-form__tool-columns {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.agent-form__tool-columns {
|
||||
display: grid;
|
||||
grid-template-columns: var(--agent-tool-enabled-column) var(
|
||||
--agent-tool-approval-column
|
||||
);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.agent-form__tool-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.agent-form__tool-row {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(0, 1fr) var(--agent-tool-enabled-column)
|
||||
var(--agent-tool-approval-column);
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
min-height: 56px;
|
||||
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
|
||||
.agent-form__tool-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-form__tool-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.agent-form__tool-description {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agent-form__tool-row > :deep(.el-switch),
|
||||
.agent-form__tool-approval {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.agent-form__tool-approval {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.agent-form__label {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
@@ -510,26 +278,5 @@ function handleModelChange(modelId: AgentInfo['modelId']) {
|
||||
.agent-form {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.agent-form__builtin-tools {
|
||||
--agent-tool-approval-column: 72px;
|
||||
}
|
||||
|
||||
.agent-form__section-head,
|
||||
.agent-form__tool-row {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.agent-form__tool-row {
|
||||
min-height: calc(var(--space-8) * 2);
|
||||
}
|
||||
|
||||
.agent-form__tool-description {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
<script setup lang="ts">
|
||||
import type { AgentBuiltinToolKey } from '../builtin-tools';
|
||||
import type { AgentInfo } from '../types';
|
||||
|
||||
import { ElForm, ElMessageBox, ElSwitch, ElTooltip } from 'element-plus';
|
||||
|
||||
const props = defineProps<{
|
||||
agent: AgentInfo;
|
||||
canDisableShellApproval: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ change: [] }>();
|
||||
|
||||
const builtinToolOptions: Array<{
|
||||
description: string;
|
||||
key: AgentBuiltinToolKey;
|
||||
label: string;
|
||||
}> = [
|
||||
{ description: '读取工作区内的 UTF-8 文本', key: 'read', label: '读取文件' },
|
||||
{ description: '在工作区内写入或插入文本', key: 'write', label: '写入文件' },
|
||||
{
|
||||
description: '以补丁方式安全修改工作区文件',
|
||||
key: 'patch',
|
||||
label: '补丁修改',
|
||||
},
|
||||
{ description: '执行平台白名单中的受控命令', key: 'shell', label: 'Shell' },
|
||||
{
|
||||
description: '将工作区文件发布为可下载产物',
|
||||
key: 'artifactPublish',
|
||||
label: '发布产物',
|
||||
},
|
||||
];
|
||||
|
||||
function builtinTool(key: AgentBuiltinToolKey) {
|
||||
return props.agent.executionConfigJson!.builtinTools![key];
|
||||
}
|
||||
|
||||
function handleBuiltinToolEnabledChange(key: AgentBuiltinToolKey) {
|
||||
if (key === 'shell' && !props.canDisableShellApproval) {
|
||||
builtinTool('shell').approvalRequired = true;
|
||||
delete props.agent.executionConfigJson!.builtinTools!
|
||||
.shellApprovalRiskConfirmed;
|
||||
}
|
||||
emit('change');
|
||||
}
|
||||
|
||||
async function confirmUnsafeShellExecution() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'关闭后,命令和脚本将以后端服务权限在宿主环境运行,当前没有沙箱隔离。确认继续?',
|
||||
'关闭 Shell 调用确认',
|
||||
{
|
||||
cancelButtonText: '保持开启',
|
||||
confirmButtonText: '确认关闭',
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
props.agent.executionConfigJson!.builtinTools!.shellApprovalRiskConfirmed = true;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmBuiltinToolEnabledChange(key: AgentBuiltinToolKey) {
|
||||
if (
|
||||
key !== 'shell' ||
|
||||
builtinTool('shell').enabled ||
|
||||
builtinTool('shell').approvalRequired
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (!props.canDisableShellApproval) {
|
||||
builtinTool('shell').approvalRequired = true;
|
||||
return true;
|
||||
}
|
||||
return confirmUnsafeShellExecution();
|
||||
}
|
||||
|
||||
async function confirmShellApprovalChange() {
|
||||
if (!props.canDisableShellApproval) {
|
||||
return false;
|
||||
}
|
||||
if (!builtinTool('shell').approvalRequired) {
|
||||
delete props.agent.executionConfigJson!.builtinTools!
|
||||
.shellApprovalRiskConfirmed;
|
||||
return true;
|
||||
}
|
||||
return confirmUnsafeShellExecution();
|
||||
}
|
||||
|
||||
function handleApprovalChange(key: AgentBuiltinToolKey, value: unknown) {
|
||||
if (key === 'shell' && value === true) {
|
||||
delete props.agent.executionConfigJson!.builtinTools!
|
||||
.shellApprovalRiskConfirmed;
|
||||
}
|
||||
emit('change');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm class="agent-builtin-tools">
|
||||
<section aria-labelledby="builtin-tools-description">
|
||||
<div class="agent-builtin-tools__head">
|
||||
<div
|
||||
id="builtin-tools-description"
|
||||
class="agent-builtin-tools__description"
|
||||
>
|
||||
为当前智能体配置工作区能力
|
||||
</div>
|
||||
<div class="agent-builtin-tools__columns" aria-hidden="true">
|
||||
<span>启用</span>
|
||||
<span>调用前确认</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-builtin-tools__list">
|
||||
<div
|
||||
v-for="item in builtinToolOptions"
|
||||
:key="item.key"
|
||||
class="agent-builtin-tools__row"
|
||||
>
|
||||
<div class="agent-builtin-tools__copy">
|
||||
<div class="agent-builtin-tools__name">{{ item.label }}</div>
|
||||
<div class="agent-builtin-tools__description">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
</div>
|
||||
<ElSwitch
|
||||
v-model="builtinTool(item.key).enabled"
|
||||
:aria-label="`启用${item.label}`"
|
||||
:before-change="() => confirmBuiltinToolEnabledChange(item.key)"
|
||||
@change="handleBuiltinToolEnabledChange(item.key)"
|
||||
/>
|
||||
<ElTooltip
|
||||
:disabled="item.key !== 'shell' || canDisableShellApproval"
|
||||
content="Shell 调用确认仅平台超级管理员可以关闭"
|
||||
effect="light"
|
||||
placement="top"
|
||||
>
|
||||
<span class="agent-builtin-tools__approval">
|
||||
<ElSwitch
|
||||
v-model="builtinTool(item.key).approvalRequired"
|
||||
:aria-label="`${item.label}调用前确认`"
|
||||
:before-change="
|
||||
item.key === 'shell' ? confirmShellApprovalChange : undefined
|
||||
"
|
||||
:disabled="
|
||||
!builtinTool(item.key).enabled ||
|
||||
(item.key === 'shell' && !canDisableShellApproval)
|
||||
"
|
||||
@change="handleApprovalChange(item.key, $event)"
|
||||
/>
|
||||
</span>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</ElForm>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agent-builtin-tools {
|
||||
--agent-tool-approval-column: 96px;
|
||||
--agent-tool-enabled-column: 48px;
|
||||
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.agent-builtin-tools__head {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(0, 1fr)
|
||||
calc(var(--agent-tool-enabled-column) + var(--agent-tool-approval-column));
|
||||
gap: var(--space-3);
|
||||
align-items: end;
|
||||
padding-bottom: var(--space-2);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.agent-builtin-tools__description,
|
||||
.agent-builtin-tools__columns {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.agent-builtin-tools__columns {
|
||||
display: grid;
|
||||
grid-template-columns: var(--agent-tool-enabled-column) var(
|
||||
--agent-tool-approval-column
|
||||
);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.agent-builtin-tools__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.agent-builtin-tools__row {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(0, 1fr) var(--agent-tool-enabled-column)
|
||||
var(--agent-tool-approval-column);
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
min-height: 56px;
|
||||
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
|
||||
.agent-builtin-tools__copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-builtin-tools__name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.agent-builtin-tools__copy .agent-builtin-tools__description {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agent-builtin-tools__row > :deep(.el-switch),
|
||||
.agent-builtin-tools__approval {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.agent-builtin-tools__approval {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.agent-builtin-tools {
|
||||
--agent-tool-approval-column: 72px;
|
||||
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.agent-builtin-tools__head,
|
||||
.agent-builtin-tools__row {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.agent-builtin-tools__row {
|
||||
min-height: calc(var(--space-8) * 2);
|
||||
}
|
||||
|
||||
.agent-builtin-tools__copy .agent-builtin-tools__description {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -44,11 +44,13 @@ const emit = defineEmits<{
|
||||
selectIssue: [nodeId: string];
|
||||
}>();
|
||||
|
||||
const loadAgentBuiltinToolsForm = () => import('./AgentBuiltinToolsForm.vue');
|
||||
const loadAgentInteractionForm = () => import('./AgentInteractionForm.vue');
|
||||
const loadAgentKnowledgeForm = () => import('./AgentKnowledgeForm.vue');
|
||||
const loadAgentSkillInspector = () => import('./AgentSkillInspector.vue');
|
||||
const loadAgentToolForm = () => import('./AgentToolForm.vue');
|
||||
const loadAgentTryoutPanel = () => import('./AgentTryoutPanel.vue');
|
||||
const AgentBuiltinToolsForm = defineAsyncComponent(loadAgentBuiltinToolsForm);
|
||||
const AgentInteractionForm = defineAsyncComponent(loadAgentInteractionForm);
|
||||
const AgentKnowledgeForm = defineAsyncComponent(loadAgentKnowledgeForm);
|
||||
const AgentSkillInspector = defineAsyncComponent(loadAgentSkillInspector);
|
||||
@@ -65,8 +67,9 @@ const selectedKnowledge = computed(() => {
|
||||
return props.state.knowledgeBindings.find((item) => item.localId === localId);
|
||||
});
|
||||
|
||||
const activeBaseTab = ref<'basic' | 'interaction'>('basic');
|
||||
const activeBaseTab = ref<'basic' | 'builtinTools' | 'interaction'>('basic');
|
||||
const interactionForm = ref<AgentInteractionFormExpose>();
|
||||
const tryoutExpanded = ref(false);
|
||||
const selectedModel = computed(() =>
|
||||
props.models.find((item) => item.value === String(props.state.agent.modelId)),
|
||||
);
|
||||
@@ -96,6 +99,19 @@ function handleMoveSkill(skillId: number | string | undefined, offset: -1 | 1) {
|
||||
emit('moveSkill', skillId, offset);
|
||||
}
|
||||
|
||||
function handleCloseTryout() {
|
||||
tryoutExpanded.value = false;
|
||||
emit('closeTryout');
|
||||
}
|
||||
|
||||
function handleWindowKeydown(event: KeyboardEvent) {
|
||||
if (event.key !== 'Escape' || !tryoutExpanded.value) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
tryoutExpanded.value = false;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.issues,
|
||||
(issues) => {
|
||||
@@ -105,6 +121,16 @@ watch(
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.state.panelMode,
|
||||
(panelMode) => {
|
||||
if (panelMode !== 'tryout') {
|
||||
tryoutExpanded.value = false;
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
|
||||
const selectedTool = computed(() => {
|
||||
if (!props.state.selectedNodeId.startsWith('tool:')) return;
|
||||
const localId = props.state.selectedNodeId.slice('tool:'.length);
|
||||
@@ -141,8 +167,10 @@ let prefetchHandle: number | undefined;
|
||||
let prefetchTimer: number | undefined;
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', handleWindowKeydown);
|
||||
const prefetchHiddenPanels = () => {
|
||||
void Promise.allSettled([
|
||||
loadAgentBuiltinToolsForm(),
|
||||
loadAgentInteractionForm(),
|
||||
loadAgentKnowledgeForm(),
|
||||
loadAgentSkillInspector(),
|
||||
@@ -160,6 +188,7 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handleWindowKeydown);
|
||||
if (prefetchHandle !== undefined) {
|
||||
window.cancelIdleCallback(prefetchHandle);
|
||||
}
|
||||
@@ -170,7 +199,13 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="agent-inspector">
|
||||
<aside
|
||||
class="agent-inspector"
|
||||
:class="{
|
||||
'is-tryout': state.panelMode === 'tryout',
|
||||
'is-tryout-expanded': state.panelMode === 'tryout' && tryoutExpanded,
|
||||
}"
|
||||
>
|
||||
<template v-if="state.panelMode === 'tryout'">
|
||||
<AgentTryoutPanel
|
||||
:agent="state.agent"
|
||||
@@ -178,7 +213,9 @@ onBeforeUnmount(() => {
|
||||
:skill-bindings="state.skillBindings"
|
||||
:tool-bindings="state.toolBindings"
|
||||
:knowledge-bindings="state.knowledgeBindings"
|
||||
@close="emit('closeTryout')"
|
||||
:expanded="tryoutExpanded"
|
||||
@close="handleCloseTryout"
|
||||
@update:expanded="tryoutExpanded = $event"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -221,7 +258,6 @@ onBeforeUnmount(() => {
|
||||
<ElTabPane label="基础设置" name="basic">
|
||||
<AgentBaseForm
|
||||
:agent="state.agent"
|
||||
:can-disable-shell-approval="canDisableShellApproval"
|
||||
:categories="categories"
|
||||
:models="models"
|
||||
:tool-bindings="state.toolBindings"
|
||||
@@ -235,6 +271,13 @@ onBeforeUnmount(() => {
|
||||
@change="emit('change')"
|
||||
/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="内置工具" name="builtinTools">
|
||||
<AgentBuiltinToolsForm
|
||||
:agent="state.agent"
|
||||
:can-disable-shell-approval="canDisableShellApproval"
|
||||
@change="emit('change')"
|
||||
/>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</template>
|
||||
<AgentKnowledgeForm
|
||||
@@ -267,14 +310,16 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped>
|
||||
.agent-inspector {
|
||||
--agent-inspector-expanded-width: 960px;
|
||||
--agent-inspector-width: min(420px, calc(100vw - 320px));
|
||||
|
||||
position: absolute;
|
||||
top: var(--space-6);
|
||||
right: var(--space-6);
|
||||
bottom: 96px;
|
||||
inset: var(--space-6) var(--space-6) 96px
|
||||
calc(100% - var(--space-6) - var(--agent-inspector-width));
|
||||
z-index: 20;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(420px, calc(100vw - 320px));
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--el-bg-color);
|
||||
@@ -285,6 +330,27 @@ onBeforeUnmount(() => {
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.agent-inspector.is-tryout {
|
||||
transition:
|
||||
top var(--motion-duration-medium) var(--motion-ease-standard),
|
||||
right var(--motion-duration-medium) var(--motion-ease-standard),
|
||||
bottom var(--motion-duration-medium) var(--motion-ease-standard),
|
||||
left var(--motion-duration-medium) var(--motion-ease-standard),
|
||||
border-radius var(--motion-duration-medium) var(--motion-ease-standard),
|
||||
box-shadow var(--motion-duration-medium) var(--motion-ease-standard);
|
||||
}
|
||||
|
||||
.agent-inspector.is-tryout-expanded {
|
||||
inset: var(--space-6)
|
||||
max(
|
||||
var(--space-6),
|
||||
calc((100% - var(--agent-inspector-expanded-width)) / 2)
|
||||
);
|
||||
z-index: 40;
|
||||
border-radius: var(--radius-panel);
|
||||
box-shadow: var(--shadow-float);
|
||||
}
|
||||
|
||||
.agent-inspector__header {
|
||||
padding: var(--space-4);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
@@ -346,17 +412,23 @@ onBeforeUnmount(() => {
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.agent-inspector {
|
||||
top: calc(var(--space-8) + var(--space-8) + var(--space-2));
|
||||
right: var(--space-4);
|
||||
left: var(--space-4);
|
||||
width: auto;
|
||||
inset: calc(var(--space-8) + var(--space-8) + var(--space-2)) var(--space-4)
|
||||
96px;
|
||||
}
|
||||
|
||||
.agent-inspector.is-tryout-expanded {
|
||||
inset: var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.agent-inspector {
|
||||
right: var(--space-2);
|
||||
left: var(--space-2);
|
||||
inset: calc(var(--space-8) + var(--space-8) + var(--space-2)) var(--space-2)
|
||||
96px;
|
||||
}
|
||||
|
||||
.agent-inspector.is-tryout-expanded {
|
||||
inset: var(--space-2);
|
||||
}
|
||||
|
||||
.agent-inspector__header {
|
||||
@@ -367,4 +439,10 @@ onBeforeUnmount(() => {
|
||||
padding: var(--space-3) var(--space-3) 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.agent-inspector.is-tryout {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { BrushCleaning } from '@easyflow/icons';
|
||||
import { copyTextToClipboard } from '@easyflow/utils';
|
||||
|
||||
import { FullScreen } from '@element-plus/icons-vue';
|
||||
import { ElButton, ElMessage } from 'element-plus';
|
||||
|
||||
import AiChatPanel from '#/components/ai-chat/AiChatPanel.vue';
|
||||
@@ -41,13 +42,17 @@ import AgentWelcomeState from './AgentWelcomeState.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
agent: AgentInfo;
|
||||
expanded?: boolean;
|
||||
imageEnabled?: boolean;
|
||||
knowledgeBindings: AgentKnowledgeBinding[];
|
||||
skillBindings: AgentSkillBinding[];
|
||||
toolBindings: AgentToolBinding[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ close: [] }>();
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
'update:expanded': [value: boolean];
|
||||
}>();
|
||||
|
||||
const {
|
||||
loading,
|
||||
@@ -63,6 +68,7 @@ const {
|
||||
stop,
|
||||
} = useAgentTryoutStream();
|
||||
const approvalLoading = ref(false);
|
||||
const clearing = ref(false);
|
||||
const composer = useAgentComposerDraft('DRAFT');
|
||||
const loadDraftAgentArtifact = createAgentArtifactLoader(() => ({
|
||||
agentId: String(props.agent.id || ''),
|
||||
@@ -225,6 +231,10 @@ function handleSelectNextVariant(item: ChatTimelineMessageItem) {
|
||||
}
|
||||
|
||||
async function handleClearSession() {
|
||||
if (loading.value || approvalLoading.value || clearing.value) {
|
||||
return;
|
||||
}
|
||||
clearing.value = true;
|
||||
try {
|
||||
await clearDraftSession();
|
||||
await composer.clear();
|
||||
@@ -232,6 +242,8 @@ async function handleClearSession() {
|
||||
ElMessage.success('已清理会话');
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '清理会话失败');
|
||||
} finally {
|
||||
clearing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,13 +408,23 @@ async function handleReject(payload: ChatTimelineToolApprovalPayload) {
|
||||
@close="emit('close')"
|
||||
>
|
||||
<template #headerActions>
|
||||
<ElButton
|
||||
:icon="FullScreen"
|
||||
circle
|
||||
text
|
||||
:aria-label="expanded ? '收起试运行' : '展开试运行'"
|
||||
:aria-pressed="Boolean(expanded)"
|
||||
:title="expanded ? '收起试运行' : '展开试运行'"
|
||||
@click="emit('update:expanded', !expanded)"
|
||||
/>
|
||||
<ElButton
|
||||
:icon="BrushCleaning"
|
||||
circle
|
||||
text
|
||||
:disabled="approvalLoading"
|
||||
:disabled="loading || approvalLoading || clearing"
|
||||
:loading="clearing"
|
||||
aria-label="清理会话"
|
||||
title="清理会话"
|
||||
:title="loading ? '输出结束后可清理会话' : '清理会话'"
|
||||
@click="handleClearSession"
|
||||
/>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user