- 提示词模块
+ {{ t('prompt-module') }}
@@ -24,6 +24,9 @@ import { defineProps } from 'vue';
import PromptTemplates from './prompt-templates.vue';
import PromptReader from './prompt-reader.vue';
import PromptLogger from './prompt-logger.vue';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
const props = defineProps({
tabId: {
diff --git a/renderer/src/components/main-panel/prompt/prompt-templates.vue b/renderer/src/components/main-panel/prompt/prompt-templates.vue
index ff14a7c..cbe0ddd 100644
--- a/renderer/src/components/main-panel/prompt/prompt-templates.vue
+++ b/renderer/src/components/main-panel/prompt/prompt-templates.vue
@@ -8,23 +8,27 @@
prompts/list
-
-
@@ -126,8 +130,8 @@ onMounted(async () => {
user-select: none;
cursor: pointer;
display: flex;
- align-items: center;
- justify-content: space-between;
+ flex-direction: column;
+ align-items: flex-start;
transition: var(--animation-3s);
}
@@ -136,24 +140,40 @@ onMounted(async () => {
transition: var(--animation-3s);
}
+.prompt-template-container>.item:active {
+ transform: scale(0.95);
+ transition: var(--animation-3s);
+}
+
.prompt-template-container>.item.active {
background-color: var(--main-light-color);
transition: var(--animation-3s);
}
-.prompt-template-container>.item>span:first-child {
- max-width: 200px;
+.prompt-title {
+ font-weight: bold;
+ max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
-.prompt-template-container>.item>span:last-child {
+.prompt-description {
opacity: 0.6;
font-size: 12.5px;
- max-width: 200px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+ max-width: 250px;
+ overflow: visible;
+ text-overflow: unset;
+ white-space: normal;
+ word-break: break-all;
+}
+
+.empty-description {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--el-text-color-placeholder, #bbb);
+ font-size: 15px;
+ min-height: 40px;
}
\ No newline at end of file
diff --git a/renderer/src/components/main-panel/resource/resouce-reader.vue b/renderer/src/components/main-panel/resource/resouce-reader.vue
index 02fe0ef..23c2677 100644
--- a/renderer/src/components/main-panel/resource/resouce-reader.vue
+++ b/renderer/src/components/main-panel/resource/resouce-reader.vue
@@ -115,7 +115,7 @@ const formRules = computed
(() => {
currentResource.value?.params.forEach(param => {
rules[param] = [
{
- message: `${param} 是必填字段`,
+ message: `${param} ` + t('is-required'),
trigger: 'blur'
}
]
diff --git a/renderer/src/components/main-panel/resource/resource-list-templates.vue b/renderer/src/components/main-panel/resource/resource-list-templates.vue
index 2c6fd72..4bc0601 100644
--- a/renderer/src/components/main-panel/resource/resource-list-templates.vue
+++ b/renderer/src/components/main-panel/resource/resource-list-templates.vue
@@ -12,19 +12,22 @@
@@ -137,41 +140,57 @@ h3.resource-template .iconfont.icon-restart:hover {
width: 175px;
}
-.resource-template-container>.item {
- margin: 3px;
- padding: 5px 10px;
- border-radius: .3em;
- user-select: none;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: space-between;
- transition: var(--animation-3s);
+.resource-template-container > .item {
+ margin: 3px;
+ padding: 5px 10px;
+ border-radius: .3em;
+ user-select: none;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ transition: var(--animation-3s);
}
-.resource-template-container>.item:hover {
- background-color: var(--main-light-color);
- transition: var(--animation-3s);
+.resource-template-container > .item:hover {
+ background-color: var(--main-light-color);
+ transition: var(--animation-3s);
}
-.resource-template-container>.item.active {
- background-color: var(--main-light-color);
- transition: var(--animation-3s);
+.resource-template-container > .item:active {
+ transform: scale(0.95);
+ transition: var(--animation-3s);
}
-.resource-template-container>.item>span:first-child {
- max-width: 200px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+.resource-template-container > .item.active {
+ background-color: var(--main-light-color);
+ transition: var(--animation-3s);
}
-.resource-template-container>.item>span:last-child {
- opacity: 0.6;
- font-size: 12.5px;
- max-width: 200px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+.resource-title {
+ font-weight: bold;
+ max-width: 250px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.resource-description {
+ opacity: 0.6;
+ font-size: 12.5px;
+ max-width: 250px;
+ overflow: visible;
+ text-overflow: unset;
+ white-space: normal;
+ word-break: break-all;
+}
+
+.empty-description {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--el-text-color-placeholder, #bbb);
+ font-size: 15px;
+ min-height: 40px;
}
\ No newline at end of file
diff --git a/renderer/src/components/main-panel/resource/resource-list.vue b/renderer/src/components/main-panel/resource/resource-list.vue
index 08c5607..9084126 100644
--- a/renderer/src/components/main-panel/resource/resource-list.vue
+++ b/renderer/src/components/main-panel/resource/resource-list.vue
@@ -12,17 +12,23 @@
@@ -139,41 +145,58 @@ h3.resource-template .iconfont.icon-restart:hover {
width: 175px;
}
-.resource-template-container>.item {
+.resource-template-container > .item {
margin: 3px;
padding: 5px 10px;
border-radius: .3em;
user-select: none;
cursor: pointer;
display: flex;
- align-items: center;
- justify-content: space-between;
+ flex-direction: column;
+ align-items: flex-start;
transition: var(--animation-3s);
}
-.resource-template-container>.item:hover {
+.resource-template-container > .item:hover {
background-color: var(--main-light-color);
transition: var(--animation-3s);
}
-.resource-template-container>.item.active {
+.resource-template-container > .item.active {
background-color: var(--main-light-color);
transition: var(--animation-3s);
}
-.resource-template-container>.item>span:first-child {
- max-width: 200px;
+.resource-template-container > .item:active {
+ transform: scale(0.95);
+ transition: var(--animation-3s);
+}
+
+.resource-title {
+ font-weight: bold;
+ max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
-.resource-template-container>.item>span:last-child {
+.resource-description {
opacity: 0.6;
font-size: 12.5px;
- max-width: 200px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+ max-width: 250px;
+ /* Remove ellipsis and allow full text wrap */
+ overflow: visible;
+ text-overflow: unset;
+ white-space: normal;
+ word-break: break-all;
+}
+
+.empty-description {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--el-text-color-placeholder, #bbb);
+ font-size: 15px;
+ min-height: 40px;
}
\ No newline at end of file
diff --git a/renderer/src/components/main-panel/tool/auto-detector/diagram-item-record.vue b/renderer/src/components/main-panel/tool/auto-detector/diagram-item-record.vue
new file mode 100644
index 0000000..ed4fd92
--- /dev/null
+++ b/renderer/src/components/main-panel/tool/auto-detector/diagram-item-record.vue
@@ -0,0 +1,160 @@
+
+
+
+
{{ props.dataView.tool.description }}
+
+
+
Function
+
{{ props.dataView.function.name }}
+
+
+
+ Arguments
+
+
+
+
+
Result
+
+
+
{{ item.text }}
+
{{ formatJson(item) }}
+
+
+
{{ props.dataView.result }}
+
{{ formatJson(props.dataView.result) }}
+
+
+
+
+
Please select a tool to view its details.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/renderer/src/components/main-panel/tool/auto-detector/diagram.ts b/renderer/src/components/main-panel/tool/auto-detector/diagram.ts
new file mode 100644
index 0000000..3e27241
--- /dev/null
+++ b/renderer/src/components/main-panel/tool/auto-detector/diagram.ts
@@ -0,0 +1,248 @@
+import type { ElkNode } from 'elkjs/lib/elk-api';
+import { MessageState, TaskLoop } from '../../chat/core/task-loop';
+import type { Reactive } from 'vue';
+import type { ChatStorage } from '../../chat/chat-box/chat';
+import { ElMessage } from 'element-plus';
+import type { ToolItem } from '@/hook/type';
+
+import I18n from '@/i18n';
+import type { ChatCompletionChunk } from 'openai/resources/index.mjs';
+
+const { t } = I18n.global;
+
+export interface Edge {
+ id: string;
+ sources: string[];
+ targets: string[];
+ sections?: any; // { startPoint: { x, y }, endPoint: { x,
+}
+
+export type Node = ElkNode & {
+ [key: string]: any;
+ width: number;
+ height: number;
+ id: string;
+};
+
+export interface DiagramState {
+ nodes: Node[];
+ edges: Edge[];
+ selectedNodeId: string | null;
+ dataView: Map;
+ [key: string]: any;
+}
+
+export interface CanConnectResult {
+ canConnect: boolean;
+ reason?: string;
+}
+
+export interface NodeDataView {
+ tool: ToolItem;
+ status: 'default' | 'running' | 'waiting' | 'success' | 'error';
+ function?: ChatCompletionChunk.Choice.Delta.ToolCall.Function;
+ result?: any;
+}
+
+export interface DiagramContext {
+ reset: () => void,
+ render: () => void,
+ state?: DiagramState,
+ setCaption: (value: string) => void
+}
+
+/**
+ * @description 判断两个节点是否可以连接
+ */
+export function invalidConnectionDetector(state: DiagramState, d: Node): CanConnectResult {
+ const from = state.selectedNodeId;
+ const to = d.id;
+
+ if (!from) {
+ return { canConnect: false, reason: t('not-select-begin-node') };
+ }
+
+ if (from === to) {
+ return { canConnect: false, reason: '' };
+ }
+
+ // 建立邻接表
+ const adjacencyList: Record> = {};
+ state.edges.forEach(edge => {
+ const src = edge.sources[0];
+ const tgt = edge.targets[0];
+ if (!adjacencyList[src]) {
+ adjacencyList[src] = new Set();
+ }
+ adjacencyList[src].add(tgt);
+ });
+
+ // DFS 检测是否存在
+ function hasPath(current: string, target: string, visited: Set): boolean {
+ if (current === target) return true;
+ visited.add(current);
+ const neighbors = adjacencyList[current] || new Set();
+ for (const neighbor of neighbors) {
+ if (!visited.has(neighbor)) {
+ if (hasPath(neighbor, target, visited)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ if (hasPath(to, from, new Set())) {
+ return { canConnect: false, reason: t('can-make-loop') };
+ }
+
+ if (hasPath(from, to, new Set())) {
+ return { canConnect: false, reason: t('this-is-repeat-connection') };
+ }
+
+ return {
+ canConnect: true
+ }
+}
+
+/**
+ * @description 拓扑排序,输出每一层可以并行调度的节点id数组
+ * @returns string[][] 每一层可以并行调度的节点id数组
+ */
+export function topoSortParallel(state: DiagramState): string[][] {
+ // 统计每个节点的入度
+ const inDegree: Record = {};
+ state.nodes.forEach(node => {
+ inDegree[node.id] = 0;
+ });
+ state.edges.forEach(edge => {
+ const tgt = edge.targets[0];
+ if (tgt in inDegree) {
+ inDegree[tgt]++;
+ }
+ });
+
+ // 初始化队列,收集所有入度为0的节点
+ const result: string[][] = [];
+ let queue: string[] = Object.keys(inDegree).filter(id => inDegree[id] === 0);
+
+ const visited = new Set();
+
+ while (queue.length > 0) {
+ // 当前层可以并行的节点
+ result.push([...queue]);
+ const nextQueue: string[] = [];
+ for (const id of queue) {
+ visited.add(id);
+ // 遍历所有以当前节点为源的边,减少目标节点的入度
+ state.edges.forEach(edge => {
+ if (edge.sources[0] === id) {
+ const tgt = edge.targets[0];
+ inDegree[tgt]--;
+ // 如果目标节点入度为0且未访问过,加入下一层
+ if (inDegree[tgt] === 0 && !visited.has(tgt)) {
+ nextQueue.push(tgt);
+ }
+ }
+ });
+ }
+ queue = nextQueue;
+ }
+
+ // 检查是否有环
+ if (visited.size !== state.nodes.length) {
+ throw new Error('图中存在环,无法进行拓扑排序');
+ }
+
+ return result;
+}
+
+export async function makeNodeTest(
+ dataView: Reactive,
+ enableXmlWrapper: boolean,
+ prompt: string | null = null,
+ context: DiagramContext
+) {
+ if (!dataView.tool.inputSchema) {
+ return;
+ }
+
+ dataView.status = 'running';
+ context.render();
+
+ try {
+ const loop = new TaskLoop({ maxEpochs: 1 });
+ const usePrompt = (prompt || 'please call the tool {tool} to make some test').replace('{tool}', dataView.tool.name);
+ const chatStorage = {
+ messages: [],
+ settings: {
+ temperature: 0.6,
+ systemPrompt: '',
+ enableTools: [{
+ name: dataView.tool.name,
+ description: dataView.tool.description,
+ inputSchema: dataView.tool.inputSchema,
+ enabled: true
+ }],
+ enableWebSearch: false,
+ contextLength: 5,
+ enableXmlWrapper,
+ parallelToolCalls: false
+ }
+ } as ChatStorage;
+
+ loop.setMaxEpochs(1);
+
+ let aiMockJson: any = undefined;
+
+ loop.registerOnToolCall(toolCall => {
+ dataView.function = toolCall.function;
+
+ if (toolCall.function?.name === dataView.tool?.name) {
+ try {
+ const toolArgs = JSON.parse(toolCall.function?.arguments || '{}');
+ aiMockJson = toolArgs;
+ } catch (e) {
+ // ElMessage.error('AI 生成的 JSON 解析错误');
+ dataView.status = 'error';
+ dataView.result = t('ai-gen-error-json');
+ context.render();
+ loop.abort();
+ }
+ } else {
+ // ElMessage.error('AI 调用了未知的工具');
+ dataView.status = 'error';
+ dataView.result = t('ai-invoke-unknown-tool') + ' ' + toolCall.function?.name;
+ context.render();
+ loop.abort();
+ }
+ return toolCall;
+ });
+
+ loop.registerOnToolCalled(toolCalled => {
+ if (toolCalled.state === MessageState.Success) {
+ dataView.status = 'success';
+ dataView.result = toolCalled.content;
+ } else {
+ dataView.status = 'error';
+ dataView.result = toolCalled.content;
+ }
+ loop.abort();
+ return toolCalled;
+ })
+
+ loop.registerOnError(error => {
+ dataView.status = 'error';
+ dataView.result = error;
+ context.render();
+ });
+
+ await loop.start(chatStorage, usePrompt);
+
+ } finally {
+ if (dataView.status === 'running') {
+ dataView.status = 'success';
+ context.render();
+ }
+ }
+};
\ No newline at end of file
diff --git a/renderer/src/components/main-panel/tool/auto-detector/diagram.vue b/renderer/src/components/main-panel/tool/auto-detector/diagram.vue
new file mode 100644
index 0000000..21d0cda
--- /dev/null
+++ b/renderer/src/components/main-panel/tool/auto-detector/diagram.vue
@@ -0,0 +1,654 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/renderer/src/components/main-panel/tool/auto-detector/index.vue b/renderer/src/components/main-panel/tool/auto-detector/index.vue
new file mode 100644
index 0000000..e06d66d
--- /dev/null
+++ b/renderer/src/components/main-panel/tool/auto-detector/index.vue
@@ -0,0 +1,173 @@
+
+
+
+
+
Tool Diagram
+
+
context.reset()">{{ t("reset") }}
+
+
+
+
+ {{ t('start-auto-detect') }}
+
+
+
+
+
+
+ XML
+
+
+ {{ t("cancel") }}
+
+ {{ t("confirm") }}
+
+
+
+
+
+
+
+
+
+
+ {{ caption }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/renderer/src/components/main-panel/tool/index.vue b/renderer/src/components/main-panel/tool/index.vue
index 4fb3390..c629c61 100644
--- a/renderer/src/components/main-panel/tool/index.vue
+++ b/renderer/src/components/main-panel/tool/index.vue
@@ -1,29 +1,41 @@
+
+
-
\ No newline at end of file
diff --git a/renderer/src/components/main-panel/tool/tool-executor.vue b/renderer/src/components/main-panel/tool/tool-executor.vue
index 53e5793..0b52e2d 100644
--- a/renderer/src/components/main-panel/tool/tool-executor.vue
+++ b/renderer/src/components/main-panel/tool/tool-executor.vue
@@ -20,9 +20,16 @@
+
+
+
@@ -33,9 +40,35 @@
{{ t('reset') }}
-
+
{{ 'mook' }}
+
+
+
+
+ {{ 'AI' }}
+
+
+
+ {{ t('edit-ai-mook-prompt') }}
+
+
+
+
+ XML
+
+
+ {{ t('cancel') }}
+
+ {{ t('confirm') }}
+
+
+
@@ -44,7 +77,7 @@
\ No newline at end of file
diff --git a/renderer/src/components/main-panel/tool/tool-list.vue b/renderer/src/components/main-panel/tool/tool-list.vue
index 9eed698..3134c6b 100644
--- a/renderer/src/components/main-panel/tool/tool-list.vue
+++ b/renderer/src/components/main-panel/tool/tool-list.vue
@@ -8,23 +8,34 @@