| @@ -0,0 +1,9 @@ | |||
| root = true | |||
| [*] | |||
| charset = utf-8 | |||
| indent_style = space | |||
| indent_size = 2 | |||
| end_of_line = lf | |||
| insert_final_newline = true | |||
| trim_trailing_whitespace = true | |||
| @@ -0,0 +1,4 @@ | |||
| node_modules | |||
| dist | |||
| out | |||
| .gitignore | |||
| @@ -0,0 +1,17 @@ | |||
| /* eslint-env node */ | |||
| require('@rushstack/eslint-patch/modern-module-resolution') | |||
| module.exports = { | |||
| extends: [ | |||
| 'eslint:recommended', | |||
| 'plugin:vue/vue3-recommended', | |||
| '@electron-toolkit', | |||
| '@electron-toolkit/eslint-config-ts/eslint-recommended', | |||
| '@vue/eslint-config-typescript/recommended', | |||
| '@vue/eslint-config-prettier' | |||
| ], | |||
| rules: { | |||
| 'vue/require-default-prop': 'off', | |||
| 'vue/multi-word-component-names': 'off' | |||
| } | |||
| } | |||
| @@ -0,0 +1,10 @@ | |||
| node_modules | |||
| dist | |||
| out | |||
| .DS_Store | |||
| *.log* | |||
| # 项目排除路径 | |||
| /src/preload/index.d.ts | |||
| /.idea | |||
| /.vscode | |||
| @@ -0,0 +1,6 @@ | |||
| out | |||
| dist | |||
| pnpm-lock.yaml | |||
| LICENSE.md | |||
| tsconfig.json | |||
| tsconfig.*.json | |||
| @@ -0,0 +1,4 @@ | |||
| singleQuote: true | |||
| semi: false | |||
| printWidth: 100 | |||
| trailingComma: none | |||
| @@ -0,0 +1,3 @@ | |||
| provider: generic | |||
| url: https://example.com/auto-updates | |||
| updaterCacheDirName: mindpilotui-updater | |||
| @@ -0,0 +1,67 @@ | |||
| { | |||
| "appId": "com.electron.app", | |||
| "productName": "mindpilotui", | |||
| "directories": { | |||
| "buildResources": "build" | |||
| }, | |||
| "files": [ | |||
| "!**/.vscode/*", | |||
| "!src/*", | |||
| "!electron.vite.config.{js,ts,mjs,cjs}", | |||
| "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}", | |||
| "!{.env,.env.*,.npmrc,pnpm-lock.yaml}", | |||
| "!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}" | |||
| ], | |||
| "asarUnpack": [ | |||
| "resources/**" | |||
| ], | |||
| "win": { | |||
| "executableName": "mindpilotui" | |||
| }, | |||
| "nsis": { | |||
| "artifactName": "${name}-${version}-setup.${ext}", | |||
| "shortcutName": "${productName}", | |||
| "uninstallDisplayName": "${productName}", | |||
| "createDesktopShortcut": "always", | |||
| "allowToChangeInstallationDirectory": true, | |||
| "oneClick": false | |||
| }, | |||
| "mac": { | |||
| "entitlementsInherit": "build/entitlements.mac.plist", | |||
| "extendInfo": [ | |||
| { | |||
| "NSCameraUsageDescription": "Application requests access to the device's camera." | |||
| }, | |||
| { | |||
| "NSMicrophoneUsageDescription": "Application requests access to the device's microphone." | |||
| }, | |||
| { | |||
| "NSDocumentsFolderUsageDescription": "Application requests access to the user's Documents folder." | |||
| }, | |||
| { | |||
| "NSDownloadsFolderUsageDescription": "Application requests access to the user's Downloads folder." | |||
| } | |||
| ], | |||
| "notarize": false | |||
| }, | |||
| "dmg": { | |||
| "artifactName": "${name}-${version}.${ext}" | |||
| }, | |||
| "linux": { | |||
| "target": [ | |||
| "AppImage", | |||
| "snap", | |||
| "deb" | |||
| ], | |||
| "maintainer": "electronjs.org", | |||
| "category": "Utility" | |||
| }, | |||
| "appImage": { | |||
| "artifactName": "${name}-${version}.${ext}" | |||
| }, | |||
| "npmRebuild": false, | |||
| "publish": { | |||
| "provider": "generic", | |||
| "url": "https://example.com/auto-updates" | |||
| } | |||
| } | |||
| @@ -0,0 +1,28 @@ | |||
| import { resolve } from "path"; | |||
| import { defineConfig, externalizeDepsPlugin } from "electron-vite"; | |||
| import vue from "@vitejs/plugin-vue"; | |||
| import svgLoader from "vite-svg-loader"; | |||
| export default defineConfig({ | |||
| main: { | |||
| plugins: [externalizeDepsPlugin()] | |||
| }, | |||
| preload: { | |||
| plugins: [externalizeDepsPlugin()] | |||
| }, | |||
| renderer: { | |||
| resolve: { | |||
| alias: { | |||
| "@renderer": resolve("src/renderer/src") | |||
| } | |||
| }, | |||
| plugins: [ | |||
| vue(), | |||
| svgLoader({ | |||
| svgoConfig: { | |||
| multipass: true | |||
| } | |||
| }) | |||
| ] | |||
| } | |||
| }); | |||
| @@ -0,0 +1,73 @@ | |||
| { | |||
| "name": "mindpilotui", | |||
| "version": "1.0.0", | |||
| "description": "An Electron application with Vue and TypeScript", | |||
| "main": "./out/main/index.js", | |||
| "author": "example.com", | |||
| "homepage": "https://electron-vite.org", | |||
| "scripts": { | |||
| "format": "prettier --write .", | |||
| "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix", | |||
| "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", | |||
| "typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false", | |||
| "typecheck": "npm run typecheck:node && npm run typecheck:web", | |||
| "start": "electron-vite preview", | |||
| "dev": "electron-vite dev", | |||
| "build": "npm run typecheck && electron-vite build", | |||
| "postinstall": "electron-builder install-app-deps", | |||
| "build:unpack": "npm run build && electron-builder --dir", | |||
| "build:win": "npm run build && electron-builder --win --config ./electron-builder.json", | |||
| "build:mac": "npm run build && electron-builder --mac --config ./electron-builder.json", | |||
| "build:linux": "npm run build && electron-builder --linux --config ./electron-builder.json" | |||
| }, | |||
| "dependencies": { | |||
| "@electron-toolkit/preload": "^3.0.0", | |||
| "@electron-toolkit/utils": "^3.0.0", | |||
| "@element-plus/icons-vue": "^2.3.1", | |||
| "@iconify-icons/ep": "^1.2.12", | |||
| "@iconify-icons/ri": "^1.2.10", | |||
| "@iconify/utils": "^2.1.25", | |||
| "@pureadmin/utils": "^2.4.7", | |||
| "axios": "^1.7.3", | |||
| "deep-chat": "^2.0.0", | |||
| "electron-updater": "^6.1.7", | |||
| "element-plus": "^2.7.6", | |||
| "idb": "^8.0.0", | |||
| "pinia": "^2.2.2", | |||
| "uuid": "^10.0.0", | |||
| "v-contextmenu": "3.2.0", | |||
| "vite-svg-loader": "^5.1.0", | |||
| "vue-router": "4", | |||
| "vue-runtime-helpers": "^1.1.2", | |||
| "vue-tippy": "v6" | |||
| }, | |||
| "devDependencies": { | |||
| "@electron-forge/cli": "^6.2.1", | |||
| "@electron-forge/maker-deb": "^6.2.1", | |||
| "@electron-forge/maker-rpm": "^6.2.1", | |||
| "@electron-forge/maker-squirrel": "^6.2.1", | |||
| "@electron-forge/maker-zip": "^6.2.1", | |||
| "@electron-toolkit/eslint-config": "^1.0.2", | |||
| "@electron-toolkit/eslint-config-ts": "^2.0.0", | |||
| "@electron-toolkit/tsconfig": "^1.0.1", | |||
| "@iconify/vue": "^4.1.2", | |||
| "@rushstack/eslint-patch": "^1.10.3", | |||
| "@types/node": "^22.5.1", | |||
| "@types/uuid": "^10.0.0", | |||
| "@vitejs/plugin-vue": "^5.0.5", | |||
| "@vue/eslint-config-prettier": "^9.0.0", | |||
| "@vue/eslint-config-typescript": "^13.0.0", | |||
| "electron": "^31.0.2", | |||
| "electron-builder": "^24.13.3", | |||
| "electron-vite": "^2.3.0", | |||
| "eslint": "^8.57.0", | |||
| "eslint-plugin-vue": "^9.26.0", | |||
| "prettier": "^3.3.2", | |||
| "sass": "^1.77.8", | |||
| "typescript": "^5.5.2", | |||
| "vite": "^5.3.1", | |||
| "vue": "^3.4.30", | |||
| "vue-tsc": "^2.0.22" | |||
| }, | |||
| "packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72" | |||
| } | |||
| @@ -0,0 +1,77 @@ | |||
| import { app, shell, BrowserWindow, ipcMain } from 'electron' | |||
| import { join } from 'path' | |||
| import { electronApp, optimizer, is } from '@electron-toolkit/utils' | |||
| import icon from '../../resources/icon.png?asset' | |||
| function createWindow(): void { | |||
| // Create the browser window. | |||
| const mainWindow = new BrowserWindow({ | |||
| width: 1920, | |||
| height: 1080, | |||
| show: false, | |||
| autoHideMenuBar: true, | |||
| ...(process.platform === 'linux' ? { icon } : {}), | |||
| webPreferences: { | |||
| preload: join(__dirname, '../preload/index.js'), | |||
| sandbox: false, | |||
| webSecurity: false | |||
| } | |||
| }) | |||
| // mainWindow.webContents.toggleDevTools() | |||
| mainWindow.on('ready-to-show', () => { | |||
| mainWindow.show() | |||
| }) | |||
| mainWindow.webContents.setWindowOpenHandler((details) => { | |||
| shell.openExternal(details.url) | |||
| return { action: 'deny' } | |||
| }) | |||
| // HMR for renderer base on electron-vite cli. | |||
| // Load the remote URL for development or the local html file for production. | |||
| if (is.dev && process.env['ELECTRON_RENDERER_URL']) { | |||
| mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) | |||
| } else { | |||
| mainWindow.loadFile(join(__dirname, '../renderer/index.html')) | |||
| } | |||
| } | |||
| // This method will be called when Electron has finished | |||
| // initialization and is ready to create browser windows. | |||
| // Some APIs can only be used after this event occurs. | |||
| app.whenReady().then(() => { | |||
| // Set app user model id for windows | |||
| electronApp.setAppUserModelId('com.electron') | |||
| // Default open or close DevTools by F12 in development | |||
| // and ignore CommandOrControl + R in production. | |||
| // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils | |||
| app.on('browser-window-created', (_, window) => { | |||
| optimizer.watchWindowShortcuts(window) | |||
| }) | |||
| // IPC test | |||
| ipcMain.on('ping', () => console.log('pong')) | |||
| createWindow() | |||
| app.on('activate', function () { | |||
| // On macOS it's common to re-create a window in the app when the | |||
| // dock icon is clicked and there are no other windows open. | |||
| if (BrowserWindow.getAllWindows().length === 0) createWindow() | |||
| }) | |||
| }) | |||
| // Quit when all windows are closed, except on macOS. There, it's common | |||
| // for applications and their menu bar to stay active until the user quits | |||
| // explicitly with Cmd + Q. | |||
| app.on('window-all-closed', () => { | |||
| if (process.platform !== 'darwin') { | |||
| app.quit() | |||
| } | |||
| }) | |||
| // In this file you can include the rest of your app"s specific main process | |||
| // code. You can also put them in separate files and require them here. | |||
| @@ -0,0 +1,22 @@ | |||
| import { contextBridge } from 'electron' | |||
| import { electronAPI } from '@electron-toolkit/preload' | |||
| // Custom APIs for renderer | |||
| const api = {} | |||
| // Use `contextBridge` APIs to expose Electron APIs to | |||
| // renderer only if context isolation is enabled, otherwise | |||
| // just add to the DOM global. | |||
| if (process.contextIsolated) { | |||
| try { | |||
| contextBridge.exposeInMainWorld('electron', electronAPI) | |||
| contextBridge.exposeInMainWorld('api', api) | |||
| } catch (error) { | |||
| console.error(error) | |||
| } | |||
| } else { | |||
| // @ts-ignore (define in dts) | |||
| window.electron = electronAPI | |||
| // @ts-ignore (define in dts) | |||
| window.api = api | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| <!doctype html> | |||
| <html> | |||
| <head> | |||
| <meta charset="UTF-8" /> | |||
| <title>Electron</title> | |||
| <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> | |||
| </head> | |||
| <body> | |||
| <div id="app"></div> | |||
| <script type="module" src="/src/main.ts"></script> | |||
| </body> | |||
| </html> | |||
| @@ -0,0 +1,6 @@ | |||
| <script setup lang="ts"> | |||
| </script> | |||
| <template> | |||
| <RouterView /> | |||
| </template> | |||
| @@ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M12 21q-3.45 0-6.012-2.287T3.05 13H5.1q.35 2.6 2.313 4.3T12 19q2.925 0 4.963-2.037T19 12t-2.037-4.962T12 5q-1.725 0-3.225.8T6.25 8H9v2H3V4h2v2.35q1.275-1.6 3.113-2.475T12 3q1.875 0 3.513.713t2.85 1.924t1.925 2.85T21 12t-.712 3.513t-1.925 2.85t-2.85 1.925T12 21m2.8-4.8L11 12.4V7h2v4.6l3.2 3.2z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="#4d4d4d" d="M899.4 638.2h-27.198c-2.2-.6-4.2-1.6-6.4-2c-57.2-8.8-102.4-56.4-106.2-112.199c-4.401-62.4 31.199-115.2 89.199-132.4c7.6-2.2 15.6-3.8 23.399-5.8h27.2c1.8.6 3.4 1.6 5.4 1.8c52.8 8.6 93 46.6 104.4 98.6c.8 4 2 8 3 12v27.2c-.6 1.8-1.6 3.6-1.8 5.4c-8.4 52-45.4 91.599-96.801 103.6c-5 1.2-9.6 2.6-14.2 3.8zM130.603 385.8l27.202.001c2.2.6 4.2 1.6 6.4 1.8c57.6 9 102.6 56.8 106.2 113.2c4 62.2-32 114.8-90.2 131.8c-7.401 2.2-15 3.8-22.401 5.6h-27.2c-1.8-.6-3.4-1.6-5.2-2c-52-9.6-86-39.8-102.2-90.2c-2.2-6.6-3.4-13.6-5.2-20.4v-27.2c.6-1.8 1.6-3.6 1.8-5.4c8.6-52.2 45.4-91.6 96.8-103.6c4.8-1.201 9.4-2.401 13.999-3.601m370.801.001h27.2c2.2.6 4.2 1.6 6.4 2c57.4 9 103.6 58.6 106 114.6c2.8 63-35.2 116.4-93.8 131.4c-6.2 1.6-12.4 3-18.6 4.4h-27.2c-2.2-.6-4.2-1.6-6.4-2c-57.4-8.8-103.601-58.6-106.2-114.6c-3-63 35.2-116.4 93.8-131.4c6.4-1.6 12.6-3 18.8-4.4"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg width="32" height="32" viewBox="0 0 48 48"><path fill="#2F88FF" fill-rule="evenodd" stroke="#000" stroke-linejoin="round" stroke-width="4" d="M44 40.836q-7.34-8.96-13.036-10.168t-10.846-.365V41L4 23.545 20.118 7v10.167q9.523.075 16.192 6.833 6.668 6.758 7.69 16.836Z" clip-rule="evenodd"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2.88 18.054a35.9 35.9 0 0 1 8.531-16.32.8.8 0 0 1 1.178 0q.25.27.413.455a35.9 35.9 0 0 1 8.118 15.865c-2.141.451-4.34.747-6.584.874l-2.089 4.178a.5.5 0 0 1-.894 0l-2.089-4.178a44 44 0 0 1-6.584-.874m6.698-1.123 1.157.066L12 19.527l1.265-2.53 1.157-.066a42 42 0 0 0 4.227-.454A33.9 33.9 0 0 0 12 4.09a33.9 33.9 0 0 0-6.649 12.387q2.093.334 4.227.454M12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg width="1em" height="1em" fill="none" class="t-icon t-icon-calendar" viewBox="0 0 16 16"><path fill="currentColor" d="M10 3H6V1.5H5V3H3a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-2V1.5h-1zM5 5h1V4h4v1h1V4h2v2H3V4h2zM3 7h10v6H3z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.38 2.019a7.5 7.5 0 1 0 10.6 10.6C21.662 17.854 17.316 22 12.001 22 6.477 22 2 17.523 2 12c0-5.315 4.146-9.661 9.38-9.981"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12M11 1h2v3h-2zm0 19h2v3h-2zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414zm2.121-14.85 1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414zM23 11v2h-3v-2zM4 11v2H1v-2z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="true" class="iconify iconify--ant-design" viewBox="0 0 1024 1024"><path fill="currentColor" d="M864 170h-60c-4.4 0-8 3.6-8 8v518H310v-73c0-6.7-7.8-10.5-13-6.3l-141.9 112a8 8 0 0 0 0 12.6l141.9 112c5.3 4.2 13 .4 13-6.3v-75h498c35.3 0 64-28.7 64-64V178c0-4.4-3.6-8-8-8"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 4H1V3h2V1h1v2.5zM13 3V1h-1v2.5l.5.5H15V3zm-1 9.5V15h1v-2h2v-1h-2.5zM1 12v1h2v2h1v-2.5l-.5-.5zm11-1.5-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5zM10 7H6v2h4z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3 12h10V4H3zm2-6h6v4H5zM2 6H1V2.5l.5-.5H5v1H2zm13-3.5V6h-1V3h-3V2h3.5zM14 10h1v3.5l-.5.5H11v-1h3zM2 13h3v1H1.5l-.5-.5V10h1z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="globalization" viewBox="0 0 512 512"><path fill="currentColor" d="m478.33 433.6-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4zM334.83 362 368 281.65 401.17 362zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73 39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93.92 1.19 1.83 2.35 2.74 3.51-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59 22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 1024 1024"><path fill="#FF5D50" d="M428.698 107.315c-6.503 72.192-36.352 207.258-160.256 337.408 3.686-48.025-7.117-83.763-19.047-107.673-6.605-13.159-26.06-10.599-28.877 3.84-5.734 29.44-20.582 75.059-57.6 137.779-71.628 121.395-62.566 459.878 340.736 459.878S934.093 585.728 876.8 442.522c-37.376-93.44-93.952-152.525-128.82-182.324-11.417-9.779-29.132-1.945-29.593 13.056-.921 30.464-7.321 73.37-33.075 102.144-.666-52.787-38.144-208.384-202.445-296.857-23.296-12.544-51.763 2.457-54.17 28.774z"/><path fill="#FFDF99" d="M702.26 678.4c-4.2-45.056-60.673-166.554-212.634-246.426-10.599-5.58-23.092 3.124-21.504 15.002 6.246 46.848 12.953 140.493-24.064 184.73 4.044-40.397-18.125-73.83-36.66-94.31-8.396-9.217-23.552-4.66-25.497 7.68-3.533 22.322-12.851 56.268-36.557 97.945-42.086 74.035-86.989 188.672 124.57 294.656 10.956.563 22.17.87 33.74.87a618 618 0 0 0 32.717-.87C694.631 878.182 709.837 759.706 702.26 678.4"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="true" class="iconify iconify--mdi" viewBox="0 0 24 24"><path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1zm10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg width="1em" height="1em" fill="none" class="t-icon t-icon-laptop" viewBox="0 0 16 16"><path fill="currentColor" d="M2.5 12a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h11a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1zm0-1h11V4h-11zM15 13H1v1h14z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg width="1em" height="1em" fill="none" class="t-icon t-icon-service" viewBox="0 0 16 16"><path fill="currentColor" d="M2.52 6.37a5.5 5.5 0 0 1 10.98.13v4c0 .05 0 .1-.02.15A4.5 4.5 0 0 1 9 14.7H8v-1h1a3.5 3.5 0 0 0 3.4-2.7h-1.9a.5.5 0 0 1-.5-.5v-4c0-.28.22-.5.5-.5h1.93a4.5 4.5 0 0 0-8.86 0H5.5c.28 0 .5.22.5.5v4a.5.5 0 0 1-.5.5H3a.5.5 0 0 1-.5-.5v-4c0-.04 0-.09.02-.13M12.5 7H11v3h1.5zm-9 0v3H5V7z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg width="1em" height="1em" fill="none" class="t-icon t-icon-shop" viewBox="0 0 16 16"><path fill="currentColor" d="M8 1a2.5 2.5 0 0 0-2.5 2.5V5h-2a.5.5 0 0 0-.5.5v9c0 .28.22.5.5.5h9a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-2V3.5A2.5 2.5 0 0 0 8 1m1.5 5v2h1V6H12v8H4V6h1.5v2h1V6zm0-1h-3V3.5a1.5 1.5 0 1 1 3 0z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="icon" viewBox="0 0 1024 1024"><path d="M554 849.574c0 23.365-18.635 42.307-42 42.307s-42-18.941-42-42.307V662.719c0-23.365 18.635-42.307 42-42.307v-7.051c23.365 0 42 25.993 42 49.358z"/><path d="M893 888.5c0 17.397-14.103 31.5-31.5 31.5h-700c-17.397 0-31.5-14.103-31.5-31.5s14.103-31.5 31.5-31.5h700c17.397 0 31.5 14.103 31.5 31.5m33-714.074C926 135.484 894.686 105 855.744 105H168.256C129.314 105 98 135.484 98 174.426V533h828zM98 630.988C98 669.931 129.314 702 168.256 702h687.488C894.686 702 926 669.931 926 630.988V596H98z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg width="1em" height="1em" fill="none" class="t-icon t-icon-user-avatar" viewBox="0 0 16 16"><path fill="currentColor" d="M8 10.5c1.24 0 2.42.31 3.5.88v1.12h1v-1.14a.94.94 0 0 0-.49-.84 8.48 8.48 0 0 0-8.02 0 .94.94 0 0 0-.49.84v1.14h1v-1.12A7.5 7.5 0 0 1 8 10.5M10.5 6a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0m-1 0a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0"/><path fill="currentColor" d="M2.5 1.5a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-11a1 1 0 0 0-1-1zm11 1v11h-11v-11z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="0.5em" height="1em" viewBox="0 0 12 24"><path fill="#706161" fill-rule="evenodd" d="M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5L10 4.563L2.682 12z"/></svg> | |||
| @@ -0,0 +1,4 @@ | |||
| <!-- sample rectangle --> | |||
| <svg width="32" height="32" xmlns="http://www.w3.org/2000/svg"> | |||
| <g clip-path="url(#icon-pure-logo_a)" fill="#1665FF"><path d="M15 32a.377.377 0 0 0 .377-.375.377.377 0 0 0-.378-.375.377.377 0 0 0-.378.375c0 .207.17.375.378.375Zm3.907-20.375a2.26 2.26 0 0 0 2.27-2.25 2.26 2.26 0 0 0-2.27-2.25 2.26 2.26 0 0 0-2.269 2.25 2.26 2.26 0 0 0 2.27 2.25Zm3.908 6.625a2.26 2.26 0 0 0 2.27-2.25 2.26 2.26 0 0 0-2.27-2.25A2.26 2.26 0 0 0 20.546 16a2.26 2.26 0 0 0 2.27 2.25Zm-15.63 0A2.26 2.26 0 0 0 9.454 16a2.26 2.26 0 0 0-2.27-2.25A2.26 2.26 0 0 0 4.917 16a2.26 2.26 0 0 0 2.269 2.25Zm17.647-6.501a1.38 1.38 0 0 0 1.386-1.375A1.38 1.38 0 0 0 24.832 9a1.38 1.38 0 0 0-1.386 1.374c0 .76.621 1.375 1.386 1.375ZM15 6.25a1.38 1.38 0 0 0 1.385-1.375c0-.76-.62-1.375-1.386-1.375a1.38 1.38 0 0 0-1.386 1.375c0 .759.62 1.374 1.386 1.374Zm-9.833 5.499a1.38 1.38 0 0 0 1.386-1.375A1.38 1.38 0 0 0 5.167 9a1.38 1.38 0 0 0-1.386 1.374c0 .76.621 1.375 1.386 1.375Zm0 11.251a1.38 1.38 0 0 0 1.386-1.374c0-.76-.62-1.375-1.386-1.375a1.38 1.38 0 0 0-1.386 1.375A1.38 1.38 0 0 0 5.167 23ZM15 28.625a1.38 1.38 0 0 0 1.385-1.374c0-.76-.62-1.375-1.386-1.375a1.38 1.38 0 0 0-1.386 1.375c0 .759.62 1.374 1.386 1.374ZM24.832 23a1.38 1.38 0 0 0 1.386-1.374c0-.76-.62-1.375-1.386-1.375a1.38 1.38 0 0 0-1.386 1.375A1.38 1.38 0 0 0 24.832 23ZM22.059 4.751a.88.88 0 0 0 .883-.875.88.88 0 0 0-.883-.876.88.88 0 0 0-.883.876.88.88 0 0 0 .883.875Zm-14.118 0a.88.88 0 0 0 .883-.875A.88.88 0 0 0 7.94 3a.88.88 0 0 0-.883.876.88.88 0 0 0 .883.875ZM.883 16.876a.88.88 0 0 0 .883-.875.88.88 0 0 0-.883-.876.88.88 0 0 0-.883.876.88.88 0 0 0 .883.875ZM7.941 29a.88.88 0 0 0 .883-.875.88.88 0 0 0-.883-.876.88.88 0 0 0-.883.876.88.88 0 0 0 .883.875Zm14.118 0a.88.88 0 0 0 .883-.875.88.88 0 0 0-.883-.876.88.88 0 0 0-.883.876.88.88 0 0 0 .883.875Zm7.058-12.124a.88.88 0 0 0 .883-.875.88.88 0 0 0-.883-.876.88.88 0 0 0-.883.876.88.88 0 0 0 .883.875Zm-.503-8.251a.377.377 0 0 0 .378-.375.377.377 0 0 0-.378-.375.377.377 0 0 0-.378.375c0 .207.17.375.378.375ZM15 .75a.377.377 0 0 0 .377-.375A.377.377 0 0 0 15 0a.377.377 0 0 0-.378.375c0 .207.17.375.378.375ZM1.386 8.625a.377.377 0 0 0 .378-.375.377.377 0 0 0-.378-.375.377.377 0 0 0-.378.375c0 .207.17.375.378.375Zm0 15.625a.377.377 0 0 0 .378-.374.377.377 0 0 0-.378-.375.377.377 0 0 0-.378.375c0 .207.17.375.378.375Zm27.228-.125a.377.377 0 0 0 .378-.375.377.377 0 0 0-.378-.375.377.377 0 0 0-.378.375c0 .207.17.375.378.375Z"></path><path d="M19.538 20.5c-1.007-.374-2.142.126-2.646 1-.505.876-1.513 1.375-2.647 1-.63-.126-1.008-.625-1.261-1 0-.125-.127-.25-.127-.5 0-1 .756-1.75 1.64-1.875h.25c1.765.125 3.277-1.375 3.404-3.126.127-1.75-1.386-3.25-3.152-3.375h-.378c-1.008 0-1.64-.75-1.64-1.75 0-.249 0-.5.127-.624v-.125c0-.126.127-.126.127-.25.378-1.25-.252-2.5-1.512-2.874-1.135-.375-2.52.25-2.9 1.5-.377 1.125.252 2.375 1.387 2.874.168.084.336.126.505.126h.126c.883.125 1.64.875 1.64 1.75 0 .374-.127.624-.252.875-.252.5-.505 1-.505 1.625 0 .626.127 1.375.505 1.875.126.25.251.625.251.876 0 .875-.63 1.625-1.512 1.75h-.378c-1.261.249-2.018 1.5-1.764 2.75.253 1.25 1.512 2 2.646 1.75.63-.126 1.135-.501 1.386-1 .505-.876 1.64-1.375 2.647-1 .505.126 1.008.5 1.262 1 .251.375.756.75 1.26 1 1.262.374 2.396-.25 2.9-1.5.504-1.126-.127-2.376-1.387-2.751h-.002Z"></path></g> | |||
| </svg> | |||
| @@ -0,0 +1,191 @@ | |||
| <template> | |||
| <div :class="cardClass"> | |||
| <div class="list-card-item_detail bg-bg_color"> | |||
| <div class="list-card-item_header"> | |||
| <div class="list-card-item_detail--left"> | |||
| <div :class="cardLogoClass"> | |||
| <Icon icon="fluent-emoji:open-book"></Icon> | |||
| </div> | |||
| <p class="list-card-item_detail--name text-text_color_primary"> | |||
| {{ product.name }} | |||
| </p> | |||
| </div> | |||
| <div class="list-card-item_detail--operation"> | |||
| <el-dropdown trigger="click" :disabled="!product.isSetup"> | |||
| <IconifyIconOffline :icon="More2Fill" class="text-[24px]" /> | |||
| <template #dropdown> | |||
| <el-dropdown-menu :disabled="!product.isSetup"> | |||
| <el-dropdown-item @click="handleClickManage(product)"> 管理 </el-dropdown-item> | |||
| <el-dropdown-item @click="handleClickDelete(product)"> 删除 </el-dropdown-item> | |||
| </el-dropdown-menu> | |||
| </template> | |||
| </el-dropdown> | |||
| </div> | |||
| </div> | |||
| <p class="list-card-item_detail--desc text-text_color_regular"> | |||
| {{ product.description }} | |||
| </p> | |||
| <el-row class="mt-4"> | |||
| <el-col :span="12"> | |||
| <el-statistic title="文件数量" :value="formattedFileCount" /> | |||
| </el-col> | |||
| <el-col :span="12"> | |||
| <el-statistic title="最后更新时间" :value="formattedLastUpdatedAt" /> | |||
| </el-col> | |||
| </el-row> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script setup lang="ts"> | |||
| import { computed, PropType } from 'vue' | |||
| import { Icon } from '@iconify/vue' | |||
| import More2Fill from '@iconify-icons/ri/more-2-fill' | |||
| import IconifyIconOffline from './ReIcon/src/iconifyIconOffline' | |||
| defineOptions({ | |||
| name: 'ReCard' | |||
| }) | |||
| interface CardProductType { | |||
| type: number | |||
| isSetup: boolean | |||
| description: string | |||
| name: string | |||
| fileCount: number | |||
| lastUpdatedAt: Date | null | |||
| } | |||
| const props = defineProps({ | |||
| product: { | |||
| type: Object as PropType<CardProductType>, | |||
| required: true | |||
| } | |||
| }) | |||
| const emit = defineEmits(['manage-product', 'delete-item']) | |||
| const handleClickManage = (product: CardProductType) => { | |||
| emit('manage-product', product) | |||
| } | |||
| const handleClickDelete = (product: CardProductType) => { | |||
| emit('delete-item', product) | |||
| } | |||
| const cardClass = computed(() => [ | |||
| 'list-card-item', | |||
| { 'list-card-item__disabled': !props.product.isSetup } | |||
| ]) | |||
| const cardLogoClass = computed(() => [ | |||
| 'list-card-item_detail--logo', | |||
| { 'list-card-item_detail--logo__disabled': !props.product.isSetup } | |||
| ]) | |||
| const formattedFileCount = computed(() => props.product.fileCount ?? 0) | |||
| const formattedLastUpdatedAt = computed(() => { | |||
| const date = new Date(props.product.lastUpdatedAt) | |||
| return date.toLocaleString('zh-CN', { | |||
| year: 'numeric', | |||
| month: '2-digit', | |||
| day: '2-digit', | |||
| hour: '2-digit', | |||
| minute: '2-digit', | |||
| hour12: false | |||
| }) | |||
| }) | |||
| </script> | |||
| <style scoped lang="scss"> | |||
| .list-card-item { | |||
| display: flex; | |||
| flex-direction: column; | |||
| margin-bottom: 12px; | |||
| overflow: hidden; | |||
| cursor: pointer; | |||
| border-radius: 3px; | |||
| &_detail { | |||
| flex: 1; | |||
| min-height: 140px; | |||
| padding: 24px 32px; | |||
| &--left { | |||
| display: flex; | |||
| align-items: center; | |||
| flex: 1; | |||
| min-width: 0; | |||
| overflow: hidden; | |||
| } | |||
| &--logo { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| width: 46px; | |||
| height: 46px; | |||
| font-size: 26px; | |||
| color: #0052d9; | |||
| background: #e0ebff; | |||
| border-radius: 50%; | |||
| margin-right: 16px; | |||
| flex-shrink: 0; | |||
| &__disabled { | |||
| color: #a1c4ff; | |||
| } | |||
| } | |||
| &--operation { | |||
| display: flex; | |||
| align-items: center; | |||
| &--tag { | |||
| border: 0; | |||
| margin-right: 8px; | |||
| } | |||
| } | |||
| &--name { | |||
| margin: 0; | |||
| font-size: 16px; | |||
| font-weight: 400; | |||
| white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| } | |||
| &--desc { | |||
| display: -webkit-box; | |||
| height: 40px; | |||
| margin-top: 14px; | |||
| margin-bottom: 0; | |||
| overflow: hidden; | |||
| font-size: 14px; | |||
| line-height: 20px; | |||
| text-overflow: ellipsis; | |||
| -webkit-line-clamp: 2; | |||
| -webkit-box-orient: vertical; | |||
| } | |||
| } | |||
| &_header { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| } | |||
| &__disabled { | |||
| .list-card-item_detail--name, | |||
| .list-card-item_detail--desc { | |||
| color: var(--el-text-color-disabled); | |||
| } | |||
| .list-card-item_detail--operation--tag { | |||
| color: #bababa; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,15 @@ | |||
| import iconifyIconOffline from "./src/iconifyIconOffline"; | |||
| import iconifyIconOnline from "./src/iconifyIconOnline"; | |||
| import iconSelect from "./src/Select.vue"; | |||
| import fontIcon from "./src/iconfont"; | |||
| /** 本地图标组件 */ | |||
| const IconifyIconOffline = iconifyIconOffline; | |||
| /** 在线图标组件 */ | |||
| const IconifyIconOnline = iconifyIconOnline; | |||
| /** `IconSelect`图标选择器组件 */ | |||
| const IconSelect = iconSelect; | |||
| /** `iconfont`组件 */ | |||
| const FontIcon = fontIcon; | |||
| export { IconifyIconOffline, IconifyIconOnline, IconSelect, FontIcon }; | |||
| @@ -0,0 +1,270 @@ | |||
| <script setup lang="ts"> | |||
| import { IconJson } from "../data"; | |||
| import { cloneDeep, isAllEmpty } from "@pureadmin/utils"; | |||
| import { ref, computed, CSSProperties, watch } from "vue"; | |||
| import Search from "@iconify-icons/ri/search-eye-line"; | |||
| import IconifyIconOffline from "./iconifyIconOffline"; | |||
| import IconifyIconOnline from "./iconifyIconOnline"; | |||
| type ParameterCSSProperties = (item?: string) => CSSProperties | undefined; | |||
| defineOptions({ | |||
| name: "IconSelect" | |||
| }); | |||
| const inputValue = defineModel({ type: String }); | |||
| const iconList = ref(IconJson); | |||
| const icon = ref(); | |||
| const currentActiveType = ref("ep:"); | |||
| // 深拷贝图标数据,前端做搜索 | |||
| const copyIconList = cloneDeep(iconList.value); | |||
| const totalPage = ref(0); | |||
| // 每页显示35个图标 | |||
| const pageSize = ref(35); | |||
| const currentPage = ref(1); | |||
| // 搜索条件 | |||
| const filterValue = ref(""); | |||
| const tabsList = [ | |||
| { | |||
| label: "Element Plus", | |||
| name: "ep:" | |||
| }, | |||
| { | |||
| label: "Remix Icon", | |||
| name: "ri:" | |||
| }, | |||
| { | |||
| label: "Font Awesome 5 Solid", | |||
| name: "fa-solid:" | |||
| } | |||
| ]; | |||
| const pageList = computed(() => | |||
| copyIconList[currentActiveType.value] | |||
| .filter(i => i.includes(filterValue.value)) | |||
| .slice( | |||
| (currentPage.value - 1) * pageSize.value, | |||
| currentPage.value * pageSize.value | |||
| ) | |||
| ); | |||
| const iconItemStyle = computed((): ParameterCSSProperties => { | |||
| return item => { | |||
| if (inputValue.value === currentActiveType.value + item) { | |||
| return { | |||
| borderColor: "var(--el-color-primary)", | |||
| color: "var(--el-color-primary)" | |||
| }; | |||
| } | |||
| }; | |||
| }); | |||
| function setVal() { | |||
| currentActiveType.value = inputValue.value.substring( | |||
| 0, | |||
| inputValue.value.indexOf(":") + 1 | |||
| ); | |||
| icon.value = inputValue.value.substring(inputValue.value.indexOf(":") + 1); | |||
| } | |||
| function onBeforeEnter() { | |||
| if (isAllEmpty(icon.value)) return; | |||
| setVal(); | |||
| // 寻找当前图标在第几页 | |||
| const curIconIndex = copyIconList[currentActiveType.value].findIndex( | |||
| i => i === icon.value | |||
| ); | |||
| currentPage.value = Math.ceil((curIconIndex + 1) / pageSize.value); | |||
| } | |||
| function onAfterLeave() { | |||
| filterValue.value = ""; | |||
| } | |||
| function handleClick({ props }) { | |||
| currentPage.value = 1; | |||
| currentActiveType.value = props.name; | |||
| } | |||
| function onChangeIcon(item) { | |||
| icon.value = item; | |||
| inputValue.value = currentActiveType.value + item; | |||
| } | |||
| function onCurrentChange(page) { | |||
| currentPage.value = page; | |||
| } | |||
| function onClear() { | |||
| icon.value = ""; | |||
| inputValue.value = ""; | |||
| } | |||
| watch( | |||
| () => pageList.value, | |||
| () => | |||
| (totalPage.value = copyIconList[currentActiveType.value].filter(i => | |||
| i.includes(filterValue.value) | |||
| ).length), | |||
| { immediate: true } | |||
| ); | |||
| watch( | |||
| () => inputValue.value, | |||
| val => val && setVal(), | |||
| { immediate: true } | |||
| ); | |||
| watch( | |||
| () => filterValue.value, | |||
| () => (currentPage.value = 1) | |||
| ); | |||
| </script> | |||
| <template> | |||
| <div class="selector"> | |||
| <el-input v-model="inputValue" disabled> | |||
| <template #append> | |||
| <el-popover | |||
| :width="350" | |||
| trigger="click" | |||
| popper-class="pure-popper" | |||
| :popper-options="{ | |||
| placement: 'auto' | |||
| }" | |||
| @before-enter="onBeforeEnter" | |||
| @after-leave="onAfterLeave" | |||
| > | |||
| <template #reference> | |||
| <div | |||
| class="w-[40px] h-[32px] cursor-pointer flex justify-center items-center" | |||
| > | |||
| <IconifyIconOffline v-if="!icon" :icon="Search" /> | |||
| <IconifyIconOnline v-else :icon="inputValue" /> | |||
| </div> | |||
| </template> | |||
| <el-input | |||
| v-model="filterValue" | |||
| class="px-2 pt-2" | |||
| placeholder="搜索图标" | |||
| clearable | |||
| /> | |||
| <el-tabs v-model="currentActiveType" @tab-click="handleClick"> | |||
| <el-tab-pane | |||
| v-for="(pane, index) in tabsList" | |||
| :key="index" | |||
| :label="pane.label" | |||
| :name="pane.name" | |||
| > | |||
| <el-scrollbar height="220px"> | |||
| <ul class="flex flex-wrap px-2 ml-2"> | |||
| <li | |||
| v-for="(item, key) in pageList" | |||
| :key="key" | |||
| :title="item" | |||
| class="icon-item p-2 cursor-pointer mr-2 mt-1 flex justify-center items-center border border-[#e5e7eb]" | |||
| :style="iconItemStyle(item)" | |||
| @click="onChangeIcon(item)" | |||
| > | |||
| <IconifyIconOnline | |||
| :icon="currentActiveType + item" | |||
| width="20px" | |||
| height="20px" | |||
| /> | |||
| </li> | |||
| </ul> | |||
| <el-empty | |||
| v-show="pageList.length === 0" | |||
| :description="`${filterValue} 图标不存在`" | |||
| :image-size="60" | |||
| /> | |||
| </el-scrollbar> | |||
| </el-tab-pane> | |||
| </el-tabs> | |||
| <div | |||
| class="w-full h-9 flex items-center overflow-auto border-t border-[#e5e7eb]" | |||
| > | |||
| <el-pagination | |||
| class="flex-auto ml-2" | |||
| :total="totalPage" | |||
| :current-page="currentPage" | |||
| :page-size="pageSize" | |||
| :pager-count="5" | |||
| layout="pager" | |||
| background | |||
| small | |||
| @current-change="onCurrentChange" | |||
| /> | |||
| <el-button | |||
| class="justify-end mr-2 ml-2" | |||
| type="danger" | |||
| size="small" | |||
| text | |||
| bg | |||
| @click="onClear" | |||
| > | |||
| 清空 | |||
| </el-button> | |||
| </div> | |||
| </el-popover> | |||
| </template> | |||
| </el-input> | |||
| </div> | |||
| </template> | |||
| <style lang="scss" scoped> | |||
| .icon-item { | |||
| &:hover { | |||
| color: var(--el-color-primary); | |||
| border-color: var(--el-color-primary); | |||
| transition: all 0.4s; | |||
| transform: scaleX(1.05); | |||
| } | |||
| } | |||
| :deep(.el-tabs__nav-next) { | |||
| font-size: 15px; | |||
| line-height: 32px; | |||
| box-shadow: -5px 0 5px -6px #ccc; | |||
| } | |||
| :deep(.el-tabs__nav-prev) { | |||
| font-size: 15px; | |||
| line-height: 32px; | |||
| box-shadow: 5px 0 5px -6px #ccc; | |||
| } | |||
| :deep(.el-input-group__append) { | |||
| padding: 0; | |||
| } | |||
| :deep(.el-tabs__item) { | |||
| height: 30px; | |||
| font-size: 12px; | |||
| font-weight: normal; | |||
| line-height: 30px; | |||
| } | |||
| :deep(.el-tabs__header), | |||
| :deep(.el-tabs__nav-wrap) { | |||
| position: static; | |||
| margin: 0; | |||
| box-shadow: 0 2px 5px rgb(0 0 0 / 6%); | |||
| } | |||
| :deep(.el-tabs__nav-wrap::after) { | |||
| height: 0; | |||
| } | |||
| :deep(.el-tabs__nav-wrap) { | |||
| padding: 0 24px; | |||
| } | |||
| :deep(.el-tabs__content) { | |||
| margin-top: 4px; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,63 @@ | |||
| import type { iconType } from "./types"; | |||
| import { h, defineComponent, type Component } from "vue"; | |||
| import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index"; | |||
| /** | |||
| * 支持 `iconfont`、自定义 `svg` 以及 `iconify` 中所有的图标 | |||
| * @see 点击查看文档图标篇 {@link https://pure-admin.github.io/pure-admin-doc/pages/icon/} | |||
| * @param icon 必传 图标 | |||
| * @param attrs 可选 iconType 属性 | |||
| * @returns Component | |||
| */ | |||
| export function useRenderIcon(icon: any, attrs?: iconType): Component { // eslint-disable-line @typescript-eslint/no-explicit-any | |||
| // iconfont | |||
| const ifReg = /^IF-/; | |||
| // typeof icon === "function" 属于SVG | |||
| if (ifReg.test(icon)) { | |||
| // iconfont | |||
| const name = icon.split(ifReg)[1]; | |||
| const iconName = name.slice( | |||
| 0, | |||
| name.indexOf(" ") == -1 ? name.length : name.indexOf(" ") | |||
| ); | |||
| const iconType = name.slice(name.indexOf(" ") + 1, name.length); | |||
| return defineComponent({ | |||
| name: "FontIcon", | |||
| render() { | |||
| return h(FontIcon, { | |||
| icon: iconName, | |||
| iconType, | |||
| ...attrs | |||
| }); | |||
| } | |||
| }); | |||
| } else if (typeof icon === "function" || typeof icon?.render === "function") { | |||
| // svg | |||
| return attrs ? h(icon, { ...attrs }) : icon; | |||
| } else if (typeof icon === "object") { | |||
| return defineComponent({ | |||
| name: "OfflineIcon", | |||
| render() { | |||
| return h(IconifyIconOffline, { | |||
| icon: icon, | |||
| ...attrs | |||
| }); | |||
| } | |||
| }); | |||
| } else { | |||
| // 通过是否存在 : 符号来判断是在线还是本地图标,存在即是在线图标,反之 | |||
| return defineComponent({ | |||
| name: "Icon", | |||
| render() { | |||
| const IconifyIcon = | |||
| icon && icon.includes(":") ? IconifyIconOnline : IconifyIconOffline; | |||
| // eslint-disable-next-line @typescript-eslint/ban-ts-comment | |||
| // @ts-ignore | |||
| return h(IconifyIcon, { | |||
| icon: icon, | |||
| ...attrs | |||
| }); | |||
| } | |||
| }); | |||
| } | |||
| } | |||
| @@ -0,0 +1,48 @@ | |||
| import { h, defineComponent } from "vue"; | |||
| // 封装iconfont组件,默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 (https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code) | |||
| export default defineComponent({ | |||
| name: "FontIcon", | |||
| props: { | |||
| icon: { | |||
| type: String, | |||
| default: "" | |||
| } | |||
| }, | |||
| render() { | |||
| const attrs = this.$attrs; | |||
| if (Object.keys(attrs).includes("uni") || attrs?.iconType === "uni") { | |||
| return h( | |||
| "i", | |||
| { | |||
| class: "iconfont", | |||
| ...attrs | |||
| }, | |||
| this.icon | |||
| ); | |||
| } else if ( | |||
| Object.keys(attrs).includes("svg") || | |||
| attrs?.iconType === "svg" | |||
| ) { | |||
| return h( | |||
| "svg", | |||
| { | |||
| class: "icon-svg", | |||
| "aria-hidden": true | |||
| }, | |||
| { | |||
| default: () => [ | |||
| h("use", { | |||
| "xlink:href": `#${this.icon}` | |||
| }) | |||
| ] | |||
| } | |||
| ); | |||
| } else { | |||
| return h("i", { | |||
| class: `iconfont ${this.icon}`, | |||
| ...attrs | |||
| }); | |||
| } | |||
| } | |||
| }); | |||
| @@ -0,0 +1,33 @@ | |||
| import { h, defineComponent, PropType } from 'vue' | |||
| import { Icon as IconifyIcon, addIcon } from '@iconify/vue/dist/offline' | |||
| // Iconify Icon在Vue里本地使用(用于内网环境) | |||
| export default defineComponent({ | |||
| name: 'IconifyIconOffline', | |||
| components: { IconifyIcon }, | |||
| props: { | |||
| icon: { | |||
| type: [Object, null] as PropType<IconifyIcon | null>, | |||
| default: null | |||
| } | |||
| }, | |||
| render() { | |||
| // eslint-disable-next-line @typescript-eslint/ban-ts-comment | |||
| // @ts-expect-error | |||
| if (typeof this.icon === 'object') addIcon(this.icon, this.icon) | |||
| const attrs = this.$attrs | |||
| return h( | |||
| IconifyIcon, | |||
| { | |||
| // eslint-disable-next-line @typescript-eslint/ban-ts-comment | |||
| // @ts-expect-error | |||
| icon: this.icon, | |||
| style: attrs?.style ? Object.assign(attrs.style, { outline: 'none' }) : { outline: 'none' }, | |||
| ...attrs | |||
| }, | |||
| { | |||
| default: () => [] | |||
| } | |||
| ) | |||
| } | |||
| }) | |||
| @@ -0,0 +1,30 @@ | |||
| import { h, defineComponent } from "vue"; | |||
| import { Icon as IconifyIcon } from "@iconify/vue"; | |||
| // Iconify Icon在Vue里在线使用(用于外网环境) | |||
| export default defineComponent({ | |||
| name: "IconifyIconOnline", | |||
| components: { IconifyIcon }, | |||
| props: { | |||
| icon: { | |||
| type: String, | |||
| default: "" | |||
| } | |||
| }, | |||
| render() { | |||
| const attrs = this.$attrs; | |||
| return h( | |||
| IconifyIcon, | |||
| { | |||
| icon: `${this.icon}`, | |||
| style: attrs?.style | |||
| ? Object.assign(attrs.style, { outline: "none" }) | |||
| : { outline: "none" }, | |||
| ...attrs | |||
| }, | |||
| { | |||
| default: () => [] | |||
| } | |||
| ); | |||
| } | |||
| }); | |||
| @@ -0,0 +1,70 @@ | |||
| // 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载 | |||
| import { addIcon } from "@iconify/vue/dist/offline"; | |||
| // 本地菜单图标,后端在路由的 icon 中返回对应的图标字符串并且前端在此处使用 addIcon 添加即可渲染菜单图标 | |||
| // @iconify-icons/ep | |||
| import Menu from "@iconify-icons/ep/menu"; | |||
| import Edit from "@iconify-icons/ep/edit"; | |||
| import SetUp from "@iconify-icons/ep/set-up"; | |||
| import Guide from "@iconify-icons/ep/guide"; | |||
| import Monitor from "@iconify-icons/ep/monitor"; | |||
| import Lollipop from "@iconify-icons/ep/lollipop"; | |||
| import Histogram from "@iconify-icons/ep/histogram"; | |||
| import HomeFilled from "@iconify-icons/ep/home-filled"; | |||
| addIcon("ep:menu", Menu); | |||
| addIcon("ep:edit", Edit); | |||
| addIcon("ep:set-up", SetUp); | |||
| addIcon("ep:guide", Guide); | |||
| addIcon("ep:monitor", Monitor); | |||
| addIcon("ep:lollipop", Lollipop); | |||
| addIcon("ep:histogram", Histogram); | |||
| addIcon("ep:home-filled", HomeFilled); | |||
| // @iconify-icons/ri | |||
| import Tag from "@iconify-icons/ri/bookmark-2-line"; | |||
| import Ppt from "@iconify-icons/ri/file-ppt-2-line"; | |||
| import Card from "@iconify-icons/ri/bank-card-line"; | |||
| import Role from "@iconify-icons/ri/admin-fill"; | |||
| import Info from "@iconify-icons/ri/file-info-line"; | |||
| import Dept from "@iconify-icons/ri/git-branch-line"; | |||
| import Table from "@iconify-icons/ri/table-line"; | |||
| import Links from "@iconify-icons/ri/links-fill"; | |||
| import Search from "@iconify-icons/ri/search-line"; | |||
| import FlUser from "@iconify-icons/ri/admin-line"; | |||
| import Setting from "@iconify-icons/ri/settings-3-line"; | |||
| import MindMap from "@iconify-icons/ri/mind-map"; | |||
| import BarChart from "@iconify-icons/ri/bar-chart-horizontal-line"; | |||
| import LoginLog from "@iconify-icons/ri/window-line"; | |||
| import Artboard from "@iconify-icons/ri/artboard-line"; | |||
| import SystemLog from "@iconify-icons/ri/file-search-line"; | |||
| import ListCheck from "@iconify-icons/ri/list-check"; | |||
| import UbuntuFill from "@iconify-icons/ri/ubuntu-fill"; | |||
| import OnlineUser from "@iconify-icons/ri/user-voice-line"; | |||
| import EditBoxLine from "@iconify-icons/ri/edit-box-line"; | |||
| import OperationLog from "@iconify-icons/ri/history-fill"; | |||
| import InformationLine from "@iconify-icons/ri/information-line"; | |||
| import TerminalWindowLine from "@iconify-icons/ri/terminal-window-line"; | |||
| import CheckboxCircleLine from "@iconify-icons/ri/checkbox-circle-line"; | |||
| addIcon("ri:bookmark-2-line", Tag); | |||
| addIcon("ri:file-ppt-2-line", Ppt); | |||
| addIcon("ri:bank-card-line", Card); | |||
| addIcon("ri:admin-fill", Role); | |||
| addIcon("ri:file-info-line", Info); | |||
| addIcon("ri:git-branch-line", Dept); | |||
| addIcon("ri:links-fill", Links); | |||
| addIcon("ri:table-line", Table); | |||
| addIcon("ri:search-line", Search); | |||
| addIcon("ri:admin-line", FlUser); | |||
| addIcon("ri:settings-3-line", Setting); | |||
| addIcon("ri:mind-map", MindMap); | |||
| addIcon("ri:bar-chart-horizontal-line", BarChart); | |||
| addIcon("ri:window-line", LoginLog); | |||
| addIcon("ri:file-search-line", SystemLog); | |||
| addIcon("ri:artboard-line", Artboard); | |||
| addIcon("ri:list-check", ListCheck); | |||
| addIcon("ri:ubuntu-fill", UbuntuFill); | |||
| addIcon("ri:user-voice-line", OnlineUser); | |||
| addIcon("ri:edit-box-line", EditBoxLine); | |||
| addIcon("ri:history-fill", OperationLog); | |||
| addIcon("ri:information-line", InformationLine); | |||
| addIcon("ri:terminal-window-line", TerminalWindowLine); | |||
| addIcon("ri:checkbox-circle-line", CheckboxCircleLine); | |||
| @@ -0,0 +1,20 @@ | |||
| export interface iconType { | |||
| // iconify (https://docs.iconify.design/icon-components/vue/#properties) | |||
| inline?: boolean; | |||
| width?: string | number; | |||
| height?: string | number; | |||
| horizontalFlip?: boolean; | |||
| verticalFlip?: boolean; | |||
| flip?: string; | |||
| rotate?: number | string; | |||
| color?: string; | |||
| horizontalAlign?: boolean; | |||
| verticalAlign?: boolean; | |||
| align?: string; | |||
| onLoad?: Function; | |||
| includes?: Function; | |||
| // svg 需要什么SVG属性自行添加 | |||
| fill?: string; | |||
| // all icon | |||
| style?: object; | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| import reText from "./src/index.vue"; | |||
| import { withInstall } from "@pureadmin/utils"; | |||
| /** 支持`Tooltip`提示的文本省略组件 */ | |||
| export const ReText = withInstall(reText); | |||
| export default ReText; | |||
| @@ -0,0 +1,68 @@ | |||
| <script lang="ts" setup> | |||
| import { h, onMounted, PropType, ref, useSlots } from 'vue' | |||
| import { type TippyOptions, useTippy } from "vue-tippy"; | |||
| defineOptions({ | |||
| agent_name: "ReText" | |||
| }); | |||
| const props = defineProps({ | |||
| // 行数 | |||
| lineClamp: { | |||
| type: [String, Number] | |||
| }, | |||
| tippyProps: { | |||
| type: Object as PropType<TippyOptions>, | |||
| default: () => ({}) | |||
| } | |||
| }); | |||
| const $slots = useSlots(); | |||
| const textRef = ref(); | |||
| const tippyFunc = ref(); | |||
| const isTextEllipsis = (el: HTMLElement) => { | |||
| if (!props.lineClamp) { | |||
| // 单行省略判断 | |||
| return el.scrollWidth > el.clientWidth; | |||
| } else { | |||
| // 多行省略判断 | |||
| return el.scrollHeight > el.clientHeight; | |||
| } | |||
| }; | |||
| const getTippyProps = () => ({ | |||
| // eslint-disable-next-line @typescript-eslint/ban-ts-comment | |||
| // @ts-ignore | |||
| content: h($slots.content || $slots.default), | |||
| ...props.tippyProps | |||
| }); | |||
| function handleHover(event: MouseEvent) { | |||
| if (isTextEllipsis(event.target as HTMLElement)) { | |||
| tippyFunc.value.setProps(getTippyProps()); | |||
| tippyFunc.value.enable(); | |||
| } else { | |||
| tippyFunc.value.disable(); | |||
| } | |||
| } | |||
| onMounted(() => { | |||
| tippyFunc.value = useTippy(textRef.value?.$el, getTippyProps()); | |||
| }); | |||
| </script> | |||
| <template> | |||
| <el-text | |||
| v-bind="{ | |||
| truncated: !lineClamp, | |||
| lineClamp, | |||
| ...$attrs | |||
| }" | |||
| ref="textRef" | |||
| @mouseover.self="handleHover" | |||
| > | |||
| <slot /> | |||
| </el-text> | |||
| </template> | |||
| @@ -0,0 +1,13 @@ | |||
| <script setup lang="ts"> | |||
| import { reactive } from 'vue' | |||
| const versions = reactive({ ...window.electron.process.versions }) | |||
| </script> | |||
| <template> | |||
| <ul class="versions"> | |||
| <li class="electron-version">Electron v{{ versions.electron }}</li> | |||
| <li class="chrome-version">Chromium v{{ versions.chrome }}</li> | |||
| <li class="node-version">Node v{{ versions.node }}</li> | |||
| </ul> | |||
| </template> | |||
| @@ -0,0 +1,123 @@ | |||
| <template> | |||
| <el-card shadow="hover" class="file-card"> | |||
| <el-row :gutter="20" align="middle" justify="space-between"> | |||
| <el-col :span="20"> | |||
| <div class="d-flex align-items-center"> | |||
| <div class="file-icon mr-2"> | |||
| <Icon :icon="fileIcon" :width="24" :height="24" /> | |||
| </div> | |||
| <el-tooltip :content="fileName" placement="top" :show-after="1000"> | |||
| <span class="file-name">{{ fileName }}</span> | |||
| </el-tooltip> | |||
| </div> | |||
| </el-col> | |||
| <el-col :span="4" class="text-right"> | |||
| <el-popconfirm | |||
| title="确定要删除这个文件吗?" | |||
| confirm-button-text="确定" | |||
| cancel-button-text="取消" | |||
| @confirm="handleDelete" | |||
| > | |||
| <template #reference> | |||
| <el-button type="danger" :icon="Delete" circle size="small" /> | |||
| </template> | |||
| </el-popconfirm> | |||
| </el-col> | |||
| </el-row> | |||
| </el-card> | |||
| </template> | |||
| <script setup lang="ts"> | |||
| import { computed } from 'vue' | |||
| import { Delete } from '@element-plus/icons-vue' | |||
| import { Icon } from '@iconify/vue' | |||
| defineOptions({ | |||
| name: 'ReFileCard' | |||
| }) | |||
| interface Props { | |||
| fileName: string | |||
| lastUpdated: string | |||
| knowledgeBaseName: string | |||
| } | |||
| const props = defineProps<Props>() | |||
| const emit = defineEmits(['delete']) | |||
| const fileExtension = computed(() => { | |||
| const parts = props.fileName.split('.') | |||
| return parts.length > 1 ? parts.pop()!.toLowerCase() : '' | |||
| }) | |||
| const fileIcon = computed(() => { | |||
| switch (fileExtension.value) { | |||
| case 'md': | |||
| return 'vscode-icons:file-type-markdown' | |||
| case 'js': | |||
| return 'vscode-icons:file-type-js' | |||
| case 'ts': | |||
| return 'vscode-icons:file-type-typescript' | |||
| case 'html': | |||
| return 'vscode-icons:file-type-html' | |||
| case 'css': | |||
| return 'vscode-icons:file-type-css' | |||
| case 'json': | |||
| return 'vscode-icons:file-type-json' | |||
| case 'py': | |||
| return 'vscode-icons:file-type-python' | |||
| case 'jpg': | |||
| case 'jpeg': | |||
| case 'png': | |||
| case 'gif': | |||
| return 'vscode-icons:file-type-image' | |||
| case 'pdf': | |||
| return 'vscode-icons:file-type-pdf' | |||
| // 添加更多文件类型... | |||
| default: | |||
| return 'vscode-icons:default-file' | |||
| } | |||
| }) | |||
| const handleDelete = () => { | |||
| emit('delete', props.knowledgeBaseName, props.fileName) | |||
| } | |||
| </script> | |||
| <style scoped> | |||
| .file-card { | |||
| margin-bottom: 10px; | |||
| } | |||
| .file-name { | |||
| font-size: 14px; | |||
| white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| max-width: 200px; | |||
| display: inline-block; | |||
| vertical-align: middle; | |||
| } | |||
| .d-flex { | |||
| display: flex; | |||
| } | |||
| .align-items-center { | |||
| align-items: center; | |||
| } | |||
| .mr-2 { | |||
| margin-right: 15px; | |||
| } | |||
| .text-right { | |||
| text-align: right; | |||
| } | |||
| .file-icon { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,15 @@ | |||
| /// <reference types="vite/client" /> | |||
| declare module "*.vue" { | |||
| import type { DefineComponent } from "vue"; | |||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types | |||
| const component: DefineComponent<{}, {}, any>; | |||
| export default component; | |||
| } | |||
| declare module "*.svg?component" { | |||
| import { DefineComponent } from "vue"; | |||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types | |||
| const component: DefineComponent<{}, {}, any>; | |||
| export default component; | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| import { createApp } from 'vue' | |||
| import { createPinia } from 'pinia' | |||
| import App from './App.vue' | |||
| import { setupRouter } from './router' | |||
| import ElementPlus from 'element-plus' | |||
| import 'element-plus/dist/index.css' | |||
| import * as ElementPlusIconsVue from '@element-plus/icons-vue' | |||
| import contextmenu from 'v-contextmenu' | |||
| import 'v-contextmenu/dist/themes/default.css' | |||
| const app = createApp(App) | |||
| const pinia = createPinia() | |||
| app.use(contextmenu) | |||
| app.use(pinia) | |||
| app.use(ElementPlus) | |||
| setupRouter(app) | |||
| for (const [key, component] of Object.entries(ElementPlusIconsVue)) { | |||
| app.component(key, component) | |||
| } | |||
| app.mount('#app') | |||
| @@ -0,0 +1,32 @@ | |||
| import type { App } from 'vue' | |||
| import type { RouteRecordRaw } from 'vue-router' | |||
| import { createRouter, createWebHashHistory } from 'vue-router' | |||
| const routes: RouteRecordRaw[] = [ | |||
| { | |||
| path: '/', | |||
| name: 'Home', | |||
| component: () => import('@renderer/views/home.vue') | |||
| }, | |||
| { | |||
| path: '/agentconfig', | |||
| name: 'AgentConfig', | |||
| component: () => import('@renderer/views/agentconfig.vue') | |||
| }, | |||
| { | |||
| path: '/kbconfig', | |||
| name: 'kbconfig', | |||
| component: () => import('@renderer/views/knowledgebase/kbconfig.vue') | |||
| } | |||
| ] | |||
| export const router = createRouter({ | |||
| history: createWebHashHistory(), | |||
| routes, | |||
| scrollBehavior: () => ({ left: 0, top: 0 }) | |||
| }) | |||
| export async function setupRouter(app: App) { | |||
| app.use(router) | |||
| await router.isReady() | |||
| } | |||
| @@ -0,0 +1,28 @@ | |||
| import { defineStore } from 'pinia' | |||
| import { ref } from 'vue' | |||
| import { Conversation } from '../views/conversationApi' | |||
| export const useModelConfigStore = defineStore('modelConfigStore', () => { | |||
| const config_id_cache = ref('') | |||
| const setConfigId = (configId: string) => { | |||
| config_id_cache.value = configId | |||
| } | |||
| const getConfigId = () => { | |||
| return config_id_cache.value | |||
| } | |||
| return { config_id_cache, setConfigId, getConfigId } | |||
| }) | |||
| export const useConversationStore = defineStore('conversationStore', () => { | |||
| const currentConversation = ref<Conversation | null>(null) | |||
| const setCurrentConversation = (conversation: Conversation | null) => { | |||
| currentConversation.value = conversation | |||
| } | |||
| const getCurrentConversation = () => { | |||
| return currentConversation.value | |||
| } | |||
| return { currentConversation, setCurrentConversation, getCurrentConversation } | |||
| }) | |||
| @@ -0,0 +1,10 @@ | |||
| function generateFourDigitNumber(): string { | |||
| const min = 1000; | |||
| const max = 9999; | |||
| const randomNumber = Math.floor(Math.random() * (max - min + 1)) + min; | |||
| return randomNumber.toString(); | |||
| } | |||
| export const generateAssistantWithRandomID = () => { | |||
| return "assistant" + generateFourDigitNumber(); | |||
| }; | |||
| @@ -0,0 +1,686 @@ | |||
| <template> | |||
| <div class="agentconfig-layout"> | |||
| <el-container class="full-height"> | |||
| <el-header class="header"> | |||
| <div class="left-icons"> | |||
| <el-button class="back-button" type="text" @click="goBack"> | |||
| <Icon | |||
| icon="weui:back-filled" | |||
| width="40" | |||
| height="40" | |||
| style="color: #4d4d4d" | |||
| class="back-icon" | |||
| /> | |||
| <!-- <img :src="backButton" alt="Back" class="back-icon" />--> | |||
| </el-button> | |||
| <el-avatar :size="50" :src="avatarIcon" /> | |||
| </div> | |||
| <div class="config-sub-menu horizontal-menu" style="left: 50%; position: absolute"> | |||
| <el-dropdown @command="selectDebugModel"> | |||
| <span class="el-dropdown-link"> | |||
| {{ debugConfigSettings.config_name || '未选择配置' }} | |||
| <el-icon class="el-icon--right"><arrow-down /></el-icon> | |||
| </span> | |||
| <template #dropdown> | |||
| <el-dropdown-menu> | |||
| <el-dropdown-item | |||
| v-for="config in configs" | |||
| :key="config.config_id" | |||
| :command="config" | |||
| > | |||
| {{ config.config_name }} | |||
| </el-dropdown-item> | |||
| </el-dropdown-menu> | |||
| </template> | |||
| </el-dropdown> | |||
| </div> | |||
| <div class="right-buttons"> | |||
| <el-button | |||
| class="config-button delete-config-button" | |||
| type="primary" | |||
| plain | |||
| @click="deleteConfig" | |||
| >删除 | |||
| </el-button> | |||
| <el-button | |||
| class="config-button save-config-button" | |||
| type="danger" | |||
| plain | |||
| @click="saveConfig" | |||
| >发布 | |||
| </el-button> | |||
| </div> | |||
| </el-header> | |||
| <el-container class="content-container"> | |||
| <div style="width: 50%" class="config-menu"> | |||
| <div class="config-title">配置智能体</div> | |||
| <el-form :model="agentForm" label-position="top" class="config-form"> | |||
| <el-form-item label="图标"> | |||
| <el-avatar class="agent-avatar" :size="50" :src="avatarIcon" @click="onUploadIcon" /> | |||
| <div class="el-upload__tip"> 只能上传jpg/png文件,且不超过 1 mb</div> | |||
| </el-form-item> | |||
| <el-form-item label="名称"> | |||
| <el-input v-model="agentForm.agent_name" placeholder="命名你的工具"></el-input> | |||
| </el-form-item> | |||
| <el-form-item label="简介"> | |||
| <el-input | |||
| v-model="agentForm.agent_abstract" | |||
| type="textarea" | |||
| placeholder="一句话介绍你的工具" | |||
| ></el-input> | |||
| </el-form-item> | |||
| <el-form-item label="配置信息"> | |||
| <el-input | |||
| v-model="agentForm.agent_info" | |||
| type="textarea" | |||
| :placeholder="configPlaceHolder" | |||
| rows="4" | |||
| ></el-input> | |||
| </el-form-item> | |||
| <el-form-item label="能力配置"> | |||
| <el-select v-model="selectedCapabilities" multiple placeholder="选择能力"> | |||
| <el-option | |||
| v-for="item in capabilities" | |||
| :key="item.value" | |||
| :label="item.label" | |||
| :value="item.value" | |||
| > | |||
| </el-option> | |||
| </el-select> | |||
| </el-form-item> | |||
| <el-form-item label="自定义能力"> | |||
| <div | |||
| style=" | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| width: 100%; | |||
| " | |||
| > | |||
| <span class="config-tips">让智能体调用外部AP来实现复杂功能</span> | |||
| <el-button plain @click="createTool">自建插件</el-button> | |||
| </div> | |||
| </el-form-item> | |||
| <el-form-item label="温度"> | |||
| <span class="temperature-tips config-tips" | |||
| >温度越高,回答越随机,温度越低,回答越固定</span | |||
| > | |||
| <el-slider | |||
| v-model="temperatureValue" | |||
| :min="0.1" | |||
| :max="1" | |||
| :step="0.1" | |||
| show-input | |||
| ></el-slider> | |||
| </el-form-item> | |||
| <el-form-item label="最大Token"> | |||
| <div | |||
| style=" | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| width: 100%; | |||
| " | |||
| > | |||
| <span class="config-tips">模型最大输出长度</span> | |||
| <el-input-number | |||
| v-model="agentForm.max_tokens" | |||
| :min="1" | |||
| placeholder="输入最大长度" | |||
| ></el-input-number> | |||
| </div> | |||
| </el-form-item> | |||
| </el-form> | |||
| </div> | |||
| <div | |||
| class="preview-container" | |||
| :class="{ disabled: isPreviewDisabled }" | |||
| @mouseover="showTooltip = true" | |||
| @mouseleave="showTooltip = false" | |||
| > | |||
| <span class="config-title">调试与预览</span> | |||
| <div class="preview-area" :class="{ 'cursor-not-allowed': isPreviewDisabled }"> | |||
| <deep-chat | |||
| id="chat-element" | |||
| avatars="true" | |||
| :text-input="{ | |||
| placeholder: { text: '请输入你的问题...' } | |||
| }" | |||
| :speech-to-text="{ | |||
| button: { | |||
| default: { | |||
| container: { | |||
| default: { | |||
| bottom: '1em', | |||
| right: '0.6em', | |||
| borderRadius: '20px', | |||
| width: '1.9em', | |||
| height: '1.9em' | |||
| } | |||
| }, | |||
| svg: { styles: { default: { bottom: '0.35em', left: '0.35em' } } } | |||
| }, | |||
| position: 'inside-right' | |||
| } | |||
| }" | |||
| :mixed-files="{ button: { position: 'inside-left' } }" | |||
| :demo="true" | |||
| style="width: 100%; height: 100%; background-color: #ffffff; border: none" | |||
| > | |||
| </deep-chat> | |||
| </div> | |||
| <el-tooltip | |||
| :content="tooltipContent" | |||
| placement="top" | |||
| :disabled="!isPreviewDisabled || !showTooltip" | |||
| effect="light" | |||
| popper-class="custom-tooltip" | |||
| > | |||
| <div v-if="isPreviewDisabled" class="overlay"></div> | |||
| </el-tooltip> | |||
| </div> | |||
| </el-container> | |||
| </el-container> | |||
| </div> | |||
| </template> | |||
| <script setup lang="ts"> | |||
| import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue' | |||
| import 'deep-chat' | |||
| import { | |||
| ElForm, | |||
| ElFormItem, | |||
| ElInput, | |||
| ElButton, | |||
| ElUpload, | |||
| ElContainer, | |||
| ElHeader, | |||
| ElSlider, | |||
| ElSelect, | |||
| ElOption, | |||
| ElMessage | |||
| } from 'element-plus' | |||
| import { ArrowDown, UploadFilled } from '@element-plus/icons-vue' | |||
| import uploadIcon from '../assets/material-symbols--upload-sharp.png' | |||
| import { Icon } from '@iconify/vue' | |||
| import { useRoute, useRouter } from 'vue-router' | |||
| import axios from 'axios' | |||
| import type { DeepChat } from 'deep-chat' | |||
| import { Signals } from 'deep-chat/dist/types/handler' | |||
| import { ModelConfig, useConfigManagement } from './configManagement' | |||
| let chatElementRef: DeepChat | null = null | |||
| const capabilities = ref<Tool[]>([]) | |||
| const avatarIcon = ref(uploadIcon) | |||
| const selectedCapabilities = ref([]) | |||
| const temperatureValue = ref(1) | |||
| const agentForm = reactive({ | |||
| agent_name: '', | |||
| agent_abstract: '', | |||
| agent_info: '', | |||
| max_tokens: 4096 | |||
| }) | |||
| const fakeRAGOptions = ref([ | |||
| { | |||
| value: 'knowledge_base', | |||
| label: '知识库1' | |||
| }, | |||
| { | |||
| value: 'knowledge_base1', | |||
| label: '知识库2' | |||
| } | |||
| ]) | |||
| const selectedRAGOption = ref('') | |||
| const onUploadIcon = () => { | |||
| const input = document.createElement('input') | |||
| input.type = 'file' | |||
| input.accept = 'image/jpeg, image/png' | |||
| input.onchange = (event) => { | |||
| const file = (event.target as HTMLInputElement).files?.[0] | |||
| if (file) { | |||
| const reader = new FileReader() | |||
| reader.onload = (e) => { | |||
| avatarIcon.value = e.target?.result as string | |||
| } | |||
| reader.readAsDataURL(file) | |||
| } | |||
| } | |||
| input.click() | |||
| } | |||
| const configPlaceHolder = | |||
| '' + | |||
| '请详细描述你的工具设定,例如:\n' + | |||
| ' 工具特点,说明ta的能力、希望ta完成的工作或目标,ta的作用\n' + | |||
| ' 工具身份,描述ta的角色、和用户交互形式,需要规避的异常行为\n' + | |||
| ' 工具行为,指定ta的行为特点、性格或个性化回复用户的方式\n' | |||
| const router = useRouter() // 获取router实例 | |||
| const goBack = () => { | |||
| router.push('/') | |||
| } | |||
| onMounted(() => { | |||
| getAvailableTools() | |||
| const chatElement = document.querySelector('#chat-element') | |||
| if (chatElement?.shadowRoot) { | |||
| const observer = new MutationObserver((_, obs) => { | |||
| const intropanel = chatElement.shadowRoot?.querySelector( | |||
| '#messages > div.intro-panel' | |||
| ) as HTMLElement | null | |||
| if (intropanel) { | |||
| intropanel.style.display = 'flex' | |||
| obs.disconnect() // 停止观察 | |||
| } | |||
| }) | |||
| observer.observe(chatElement.shadowRoot, { | |||
| childList: true, // 观察直接子节点的添加或删除 | |||
| subtree: true, // 观察所有后代节点 | |||
| attributes: false, // 不观察属性变化 | |||
| characterData: false // 不观察文本内容变化 | |||
| }) | |||
| // 清理函数 | |||
| onUnmounted(() => { | |||
| observer.disconnect() | |||
| }) | |||
| } | |||
| }) | |||
| const createTool = () => { | |||
| // 创建工具的逻辑 | |||
| } | |||
| const getAvailableTools = async () => { | |||
| try { | |||
| const response = await axios.get('http://127.0.0.1:7861/api/tools/available_tools', { | |||
| headers: { | |||
| accept: 'application/json' | |||
| } | |||
| }) | |||
| console.log(response.data) | |||
| const tools = response.data.tools | |||
| capabilities.value = tools.map((tool: string) => ({ label: tool, value: tool })) | |||
| } catch (error) { | |||
| console.error('Error fetching available tools:', error) | |||
| } | |||
| } | |||
| const deleteConfig = async () => { | |||
| try { | |||
| const delete_agent_id = Number.parseInt(agentId.value as string, 10) | |||
| const response = await axios.delete('http://127.0.0.1:7861/api/agent/delete_agent', { | |||
| headers: { | |||
| accept: 'application/json', | |||
| 'Content-Type': 'application/json' | |||
| }, | |||
| data: delete_agent_id | |||
| }) | |||
| console.log(response) | |||
| ElMessage.success({ | |||
| message: '成功删除Agent配置', | |||
| type: 'success' | |||
| }) | |||
| // 处理成功响应 | |||
| } catch (error) { | |||
| console.error('Error deleting agent:', error) | |||
| ElMessage.error('删除配置失败') | |||
| } | |||
| } | |||
| //监视agentForm.agent_name 如果有变化,就更新introMessage | |||
| watch( | |||
| () => agentForm.agent_name, | |||
| (newValue, oldValue) => { | |||
| if (newValue !== oldValue) { | |||
| if (chatElementRef) { | |||
| chatElementRef.introMessage = { text: `你好!我是${newValue},有什么事情需要我帮忙吗?` } | |||
| } | |||
| } | |||
| } | |||
| ) | |||
| /*********************************** deep-chat 对话配置 ***********************************/ | |||
| function watchAndUpdateConfig() { | |||
| watch( | |||
| [selectedCapabilities, temperatureValue, agentForm], | |||
| ([newCapabilities, newTemperature, newAgentForm]) => { | |||
| // 更新 debugConversationConfig | |||
| debugConversationConfig.agent_config = { | |||
| ...debugConversationConfig.agent_config, | |||
| tool_config: newCapabilities, | |||
| temperature: newTemperature, | |||
| agent_name: newAgentForm.agent_name, | |||
| agent_abstract: newAgentForm.agent_abstract, | |||
| agent_info: newAgentForm.agent_info, | |||
| max_tokens: newAgentForm.max_tokens, | |||
| agent_enable: true | |||
| } | |||
| console.log('更新:', newCapabilities, newTemperature, newAgentForm) | |||
| }, | |||
| { deep: true } // 使用深度监视以捕获 agentForm 内部的变化 | |||
| ) | |||
| } | |||
| onMounted(() => { | |||
| chatElementRef = document.getElementById('chat-element') as DeepChat | |||
| chatElementRef.introMessage = { | |||
| text: `你好!我是${agentForm.agent_name},有什么事情需要我帮忙吗?` | |||
| } | |||
| chatElementRef.connect = { | |||
| handler: async (body, signals: Signals) => { | |||
| handleDebugConversation(body, signals, chatElementRef as DeepChat) | |||
| } | |||
| } | |||
| watchAndUpdateConfig() | |||
| }) | |||
| /******************************************************************************/ | |||
| import { useConversation } from './conversationApi' | |||
| import { Tool } from './toolConfig' | |||
| import IconifyIconOffline from '../components/ReIcon/src/iconifyIconOffline' | |||
| const { handleDebugConversation, debugConversationConfig } = useConversation() | |||
| const chatHistory = reactive([ | |||
| { | |||
| content: `${agentForm.agent_abstract}`, | |||
| role: 'user' | |||
| }, | |||
| { | |||
| content: `${agentForm.agent_info}`, | |||
| role: 'user' | |||
| } | |||
| ]) // 初始化 chatHistory | |||
| watch( | |||
| () => [agentForm.agent_abstract, agentForm.agent_info], | |||
| ([newAbstract, newInfo], [oldAbstract, oldInfo]) => { | |||
| if (newAbstract !== oldAbstract) { | |||
| chatHistory[0].content = newAbstract | |||
| } | |||
| if (newInfo !== oldInfo) { | |||
| chatHistory[1].content = newInfo | |||
| } | |||
| if (chatElementRef) { | |||
| chatElementRef.clearMessages() | |||
| } | |||
| } | |||
| ) | |||
| /*********************************** 对话配置 ***********************************/ | |||
| const { | |||
| configs, | |||
| fetchAllConfigs | |||
| } = useConfigManagement() | |||
| const debugConfigSettings = ref({ | |||
| config_id: '', | |||
| config_name: '' | |||
| }) | |||
| const selectDebugModel = (config: ModelConfig) => { | |||
| if (config) { | |||
| debugConfigSettings.value.config_id = config.config_id as string | |||
| debugConfigSettings.value.config_name = config.config_name | |||
| debugConversationConfig.config_id = Number.parseInt(config.config_id as string, 10) | |||
| console.log('debugConversationConfig.config_id:', debugConversationConfig.config_id) | |||
| } | |||
| } | |||
| onMounted(async () => { | |||
| await fetchAllConfigs() | |||
| }) | |||
| /******************************************************************************/ | |||
| /*********************************** 对话设置 ***********************************/ | |||
| /******************************************************************************/ | |||
| //从对话界面跳转到配置界面修改的处理逻辑 | |||
| const route = useRoute() | |||
| const agentId = ref<string>('') | |||
| onMounted(async () => { | |||
| agentId.value = route.query.agentId as string | |||
| if (agentId.value) { | |||
| await fetchAgentInfo(agentId.value) | |||
| } | |||
| }) | |||
| const fetchAgentInfo = async (id) => { | |||
| try { | |||
| const response = await axios.get(`http://127.0.0.1:7861/api/agent/get_agent?agent_id=${id}`) | |||
| console.log(response) | |||
| if (response.data.code === 200) { | |||
| const agentData = response.data.data | |||
| // 将获取的数据填充到表单中 | |||
| agentForm.agent_name = agentData.agent_name | |||
| agentForm.agent_abstract = agentData.agent_abstract | |||
| agentForm.agent_info = agentData.agent_info | |||
| temperatureValue.value = agentData.temperature | |||
| agentForm.max_tokens = agentData.max_tokens | |||
| selectedCapabilities.value = agentData.tool_config | |||
| avatarIcon.value = agentData.avatar || uploadIcon | |||
| // 其他字段同理... | |||
| } | |||
| } catch (error) { | |||
| console.error('Error fetching agent info:', error) | |||
| ElMessage.error('获取Agent信息失败') | |||
| } | |||
| } | |||
| const saveConfig = async () => { | |||
| try { | |||
| const avatarToUpload = avatarIcon.value === uploadIcon ? '' : avatarIcon.value | |||
| const url = agentId.value | |||
| ? 'http://127.0.0.1:7861/api/agent/update_agent' | |||
| : 'http://127.0.0.1:7861/api/agent/create_agent' | |||
| const method = agentId.value ? 'put' : 'post' | |||
| const data = { | |||
| agent_id: agentId.value, | |||
| agent_name: agentForm.agent_name, | |||
| agent_abstract: agentForm.agent_abstract, | |||
| agent_info: agentForm.agent_info, | |||
| temperature: temperatureValue.value, | |||
| max_tokens: agentForm.max_tokens, | |||
| tool_config: selectedCapabilities.value, | |||
| avatar: avatarToUpload | |||
| } | |||
| const response = await axios[method](url, data, { | |||
| headers: { | |||
| 'Content-Type': 'application/json' | |||
| } | |||
| }) | |||
| if (response.data.code === 200) { | |||
| ElMessage.success({ | |||
| message: agentId.value ? '成功更新Agent配置' : '成功保存Agent配置', | |||
| type: 'success' | |||
| }) | |||
| router.push('/') // 保存后返回主页面 | |||
| } else { | |||
| ElMessage.error({ | |||
| message: response.data.msg, | |||
| type: 'error' | |||
| }) | |||
| } | |||
| } catch (error) { | |||
| console.error('Error saving agent config:', error) | |||
| ElMessage.error('保存配置失败') | |||
| } | |||
| } | |||
| /*********************************** 监视聊天框是否可用 ***********************************/ | |||
| const showTooltip = ref(false) | |||
| const isPreviewDisabled = computed(() => { | |||
| return !agentForm.agent_name || !debugConfigSettings.value.config_name | |||
| }) | |||
| const tooltipContent = computed(() => { | |||
| if (!agentForm.agent_name && !debugConfigSettings.value.config_name) { | |||
| return '请设置 Agent 名称并选择配置' | |||
| } else if (!agentForm.agent_name) { | |||
| return '请设置 Agent 名称' | |||
| } else if (!debugConfigSettings.value.config_name) { | |||
| return '请选择配置' | |||
| } | |||
| return '' | |||
| }) | |||
| /******************************************************************************/ | |||
| </script> | |||
| <style scoped> | |||
| .agent-avatar:hover { | |||
| cursor: pointer; | |||
| } | |||
| .custom-tooltip { | |||
| font-size: 16px !important; /* 增大字体大小 */ | |||
| padding: 10px 12px !important; /* 增加内边距 */ | |||
| } | |||
| :deep(.intro-panel) { | |||
| display: block; | |||
| } | |||
| .config-button { | |||
| font-size: 16px; | |||
| padding: 10px 20px; | |||
| } | |||
| .header { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| padding: 0 20px; | |||
| } | |||
| .left-icons { | |||
| display: flex; | |||
| align-items: center; | |||
| } | |||
| .back-button { | |||
| margin-right: 10px; | |||
| padding: 0; | |||
| } | |||
| .back-icon { | |||
| width: 25px; | |||
| height: 25px; | |||
| } | |||
| .content-container { | |||
| flex: 1; | |||
| overflow: hidden; | |||
| display: flex; | |||
| } | |||
| .preview-container { | |||
| position: relative; | |||
| width: 50%; | |||
| padding: 20px; | |||
| display: flex; | |||
| flex-direction: column; | |||
| border: 1px solid #ddd; | |||
| border-radius: 10px; | |||
| } | |||
| .preview-container.disabled .preview-area { | |||
| opacity: 0.5; | |||
| pointer-events: none; | |||
| } | |||
| .preview-area { | |||
| flex: 1; | |||
| background-color: #f9f9f9; | |||
| overflow: hidden; | |||
| display: flex; | |||
| position: relative; | |||
| } | |||
| .cursor-not-allowed { | |||
| cursor: not-allowed; | |||
| } | |||
| .overlay { | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| right: 0; | |||
| bottom: 0; | |||
| background-color: rgba(255, 255, 255, 0.5); | |||
| z-index: 10; | |||
| cursor: not-allowed; | |||
| } | |||
| .preview-area { | |||
| flex: 1; | |||
| background-color: #f9f9f9; | |||
| overflow: hidden; | |||
| display: flex; | |||
| } | |||
| deep-chat { | |||
| flex: 1; | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| .config-title { | |||
| font-size: 1.5em; | |||
| font-weight: bold; | |||
| padding-bottom: 20px; | |||
| } | |||
| .config-menu { | |||
| padding: 20px; | |||
| overflow-y: auto; /* Enable vertical scrolling */ | |||
| border: 1px solid #ddd; /* Add this line to add a border */ | |||
| border-radius: 10px; /* Optional: to add rounded corners */ | |||
| } | |||
| .content-container { | |||
| flex: 1; | |||
| overflow: hidden; | |||
| display: flex; /* Add this to enable flex layout */ | |||
| } | |||
| .full-height { | |||
| height: 98vh; | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| .config-tips { | |||
| font-size: 12px; | |||
| color: #909399; | |||
| line-height: 1.5; | |||
| margin-top: 5px; | |||
| margin-bottom: 5px; | |||
| display: block; | |||
| } | |||
| .config-form { | |||
| font-weight: bold; | |||
| font-size: 1.2em; | |||
| } | |||
| .horizontal-menu { | |||
| font-weight: bold; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,214 @@ | |||
| import axios from "axios"; | |||
| import { ref, reactive, computed } from "vue"; | |||
| import { ElMessage } from "element-plus"; | |||
| const API_BASE_URL = "http://127.0.0.1:7861/api/model_configs"; | |||
| export interface ModelConfig { | |||
| config_id?: string; | |||
| config_name: string; | |||
| platform: string; | |||
| base_url: string; | |||
| api_key: string; | |||
| llm_model: { | |||
| model: string; | |||
| callbacks: boolean; | |||
| max_tokens: number; | |||
| temperature: number; | |||
| }; | |||
| } | |||
| export const useConfigManagement = () => { | |||
| const isEditMode = computed(() => !!activeConfigId.value); | |||
| const isDeleteButtonDisabled = computed(() => !activeConfigId.value); | |||
| const configs = ref<ModelConfig[]>([]); | |||
| const activeConfigId = ref(""); | |||
| const isShowConfigManagementDialog = ref(false); | |||
| const configManagementForm = reactive<ModelConfig>({ | |||
| config_name: "", | |||
| platform: "", | |||
| base_url: "", | |||
| api_key: "", | |||
| llm_model: { | |||
| model: "", | |||
| callbacks: true, | |||
| max_tokens: 4096, | |||
| temperature: 1 | |||
| } | |||
| }); | |||
| const isSaveButtonDisabled = computed(() => { | |||
| // 如果是编辑模式,按钮总是启用的 | |||
| if (isEditMode.value) { | |||
| return false; | |||
| } | |||
| // 如果是新建模式,只有当配置名称为空时才禁用按钮 | |||
| return !configManagementForm.config_name.trim(); | |||
| }); | |||
| const fetchAllConfigs = async () => { | |||
| try { | |||
| const response = await axios.get(API_BASE_URL); | |||
| if (response.data.code === 200) { | |||
| configs.value = response.data.data; | |||
| } else { | |||
| ElMessage.error(response.data.msg); | |||
| } | |||
| } catch (error) { | |||
| ElMessage.error("无法获取配置"); | |||
| console.error("无法获取配置:", error); | |||
| } | |||
| }; | |||
| const fetchSingleConfig = async (configId) => { | |||
| try { | |||
| const response = await axios.get(`${API_BASE_URL}/${configId}`); | |||
| if (response.data.code === 200) { | |||
| Object.assign(configManagementForm, response.data.data); | |||
| activeConfigId.value = configId; | |||
| } else { | |||
| ElMessage.error(response.data.msg); | |||
| } | |||
| } catch (error) { | |||
| ElMessage.error("无法获取配置"); | |||
| console.error("无法获取配置:", error); | |||
| } | |||
| }; | |||
| const addNewConfig = async () => { | |||
| try { | |||
| const response = await axios.post(API_BASE_URL + "/add", configManagementForm); | |||
| if (response.data.code === 200) { | |||
| ElMessage.success("配置新建成功"); | |||
| await fetchAllConfigs(); | |||
| // isShowConfigManagementDialog.value = false; | |||
| return response.data.data; // 返回新创建的配置 | |||
| } else { | |||
| ElMessage.error(response.data.msg); | |||
| } | |||
| } catch (error) { | |||
| ElMessage.error("添加配置失败"); | |||
| console.error("Failed to add configuration:", error); | |||
| } | |||
| return null; | |||
| }; | |||
| const handleNewConfig = () => { | |||
| activeConfigId.value = ""; | |||
| Object.assign(configManagementForm, { | |||
| config_name: "", | |||
| platform: "", | |||
| base_url: "", | |||
| api_key: "", | |||
| llm_model: { | |||
| model: "", | |||
| callbacks: true, | |||
| max_tokens: 4096, | |||
| temperature: 1 | |||
| } | |||
| }); | |||
| }; | |||
| if (isShowConfigManagementDialog.value) { | |||
| // 如果对话框已经打开,更新标题 | |||
| const dialogEl = document.querySelector(".el-dialog__title"); | |||
| if (dialogEl) { | |||
| dialogEl.textContent = "新建配置"; | |||
| } | |||
| } | |||
| const updateConfig = async () => { | |||
| try { | |||
| const response = await axios.put(`${API_BASE_URL}/${activeConfigId.value}`, configManagementForm); | |||
| if (response.data.code === 200) { | |||
| ElMessage.success("配置更新成功"); | |||
| await fetchAllConfigs(); | |||
| // isShowConfigManagementDialog.value = false; // 不自动关闭 | |||
| } else { | |||
| ElMessage.error(response.data.msg); | |||
| } | |||
| } catch (error) { | |||
| ElMessage.error("更新配置失败"); | |||
| console.error("Failed to update configuration:", error); | |||
| } | |||
| }; | |||
| const deleteConfig = async () => { | |||
| try { | |||
| const response = await axios.delete(`${API_BASE_URL}/${activeConfigId.value}`); | |||
| if (response.data.code === 200) { | |||
| ElMessage.success("已成功删除配置"); | |||
| Object.assign(configManagementForm, { | |||
| config_name: "", | |||
| platform: "", | |||
| base_url: "", | |||
| api_key: "", | |||
| llm_model: { | |||
| model: "", | |||
| callbacks: true, | |||
| max_tokens: 4096, | |||
| temperature: 1 | |||
| } | |||
| }); | |||
| activeConfigId.value = ""; | |||
| await fetchAllConfigs(); | |||
| // isShowConfigManagementDialog.value = false; | |||
| } else { | |||
| ElMessage.error(response.data.msg); | |||
| } | |||
| } catch (error) { | |||
| ElMessage.error("无法删除配置"); | |||
| console.error("Failed to delete configuration:", error); | |||
| } | |||
| }; | |||
| const handleSaveConfig = async () => { | |||
| if (activeConfigId.value) { | |||
| await updateConfig(); | |||
| } else { | |||
| const newConfig = await addNewConfig(); | |||
| if (newConfig) { | |||
| activeConfigId.value = newConfig.config_id.toString(); | |||
| } | |||
| } | |||
| }; | |||
| const handleDeleteConfig = async () => { | |||
| if (activeConfigId.value) { | |||
| await deleteConfig(); | |||
| } else { | |||
| ElMessage.warning("请选择要删除的配置"); | |||
| } | |||
| }; | |||
| const handleConfigSelect = async (configId) => { | |||
| await fetchSingleConfig(configId); | |||
| // 可以考虑添加一个小延迟,确保 activeConfigId 已经更新 | |||
| setTimeout(() => { | |||
| if (isShowConfigManagementDialog.value) { | |||
| // 如果对话框已经打开,更新标题 | |||
| const dialogEl = document.querySelector(".el-dialog__title"); | |||
| if (dialogEl) { | |||
| dialogEl.textContent = "编辑配置"; | |||
| } | |||
| } | |||
| }, 0); | |||
| }; | |||
| return { | |||
| isEditMode, | |||
| configs, | |||
| activeConfigId, | |||
| isShowConfigManagementDialog, | |||
| configManagementForm, | |||
| fetchAllConfigs, | |||
| isSaveButtonDisabled, | |||
| handleConfigSelect, isDeleteButtonDisabled, | |||
| handleSaveConfig, | |||
| handleDeleteConfig, | |||
| handleNewConfig // 添加这行 | |||
| }; | |||
| }; | |||
| @@ -0,0 +1,343 @@ | |||
| import { reactive, ref, watch } from 'vue' | |||
| import axios from 'axios' | |||
| import { ElMessage } from 'element-plus' | |||
| import { clearMessageElement, extractFirstJSON } from './utils' | |||
| import { Signals } from 'deep-chat/dist/types/handler' | |||
| import type { DeepChat } from 'deep-chat' | |||
| import { generateAssistantWithRandomID } from '../utils/tools' | |||
| import { Agent } from './type' | |||
| import { useConversationStore } from '../store/store' | |||
| const API_BASE_URL = 'http://127.0.0.1:7861/api' | |||
| export interface Conversation { | |||
| conversation_id: string | |||
| title: string | |||
| created_at: string | |||
| updated_at: string | |||
| is_summarized: boolean | |||
| agent_id: number | |||
| } | |||
| export interface Message { | |||
| message_id: number | |||
| agent_status: number | |||
| role: string | |||
| text: string | |||
| files?: Array<{ name: string; src: string; type: string }> | |||
| timestamp: string | |||
| } | |||
| export interface SendMessage { | |||
| role: string | |||
| agent_id: number | |||
| config_id: number | |||
| text: string | |||
| files?: string[] | |||
| tool_config: string[] | |||
| temperature: number | |||
| max_tokens: number | |||
| } | |||
| export interface ChatHistory { | |||
| content: string | |||
| role: string | |||
| } | |||
| export interface DebugConversationConfig { | |||
| config_id: number | null | |||
| agent_config: Agent // 假设 Agent 类型已在其他地方定义 | |||
| history: ChatHistory[] | |||
| } | |||
| export function useConversation() { | |||
| const conversations = ref<Conversation[]>([]) | |||
| const currentConversation = ref<Conversation | null>(null) | |||
| const messages = ref<Message[]>([]) | |||
| const error = ref<string | null>(null) | |||
| const localConversationConfig = ref<SendMessage>({ | |||
| role: 'user', | |||
| agent_id: NaN, | |||
| config_id: NaN, | |||
| text: '', | |||
| tool_config: [], | |||
| temperature: 1, | |||
| max_tokens: 4096 | |||
| }) | |||
| watch( | |||
| localConversationConfig, | |||
| async (newValue) => { | |||
| console.log('localConversationConfig:', newValue) | |||
| }, | |||
| { deep: true } | |||
| ) | |||
| watch(currentConversation, async (newValue) => { | |||
| useConversationStore().setCurrentConversation(newValue) | |||
| }) | |||
| const createConversation = async (agent_id: number): Promise<Conversation> => { | |||
| try { | |||
| const response = await axios.post(`${API_BASE_URL}/conversation`, agent_id) | |||
| const newConversation: Conversation = response.data.data | |||
| currentConversation.value = newConversation | |||
| conversations.value.push(newConversation) | |||
| error.value = null | |||
| return newConversation | |||
| } catch (err) { | |||
| console.error('Failed to create conversation:', err) | |||
| error.value = 'Failed to create conversation' | |||
| throw err // Re-throw the error so it can be caught by the caller | |||
| } | |||
| } | |||
| const getConversations = async (): Promise<Conversation[]> => { | |||
| try { | |||
| const response = await axios.get(`${API_BASE_URL}/conversations`) | |||
| const fetchedConversations = response.data.data | |||
| conversations.value = fetchedConversations | |||
| error.value = null | |||
| return fetchedConversations | |||
| } catch (err) { | |||
| console.error('Failed to get conversations:', err) | |||
| error.value = 'Failed to get conversations' | |||
| return [] | |||
| } | |||
| } | |||
| const getConversationDetails = async (conversation_id: string): Promise<void> => { | |||
| try { | |||
| const response = await axios.get(`${API_BASE_URL}/conversation/${conversation_id}`) | |||
| currentConversation.value = response.data.data | |||
| messages.value = response.data.data.messages | |||
| error.value = null | |||
| } catch (err) { | |||
| console.error('Failed to get conversation details:', err) | |||
| error.value = 'Failed to get conversation details' | |||
| } | |||
| } | |||
| const switchConversation = async ( | |||
| conversation: Conversation, | |||
| chatElementRef: DeepChat | |||
| ): Promise<void> => { | |||
| try { | |||
| await getConversationDetails(conversation.conversation_id) | |||
| currentConversation.value = conversation | |||
| console.log('localConversationConfig.value.agent_id:', localConversationConfig.value.agent_id) | |||
| console.log('conversation.agent_id:', conversation.agent_id) | |||
| localConversationConfig.value.agent_id = conversation.agent_id | |||
| error.value = null | |||
| // Clear the chat interface | |||
| if (chatElementRef) { | |||
| chatElementRef.clearMessages() | |||
| clearMessageElement(chatElementRef) | |||
| } | |||
| console.log(messages.value) | |||
| // 加载消息 | |||
| messages.value.forEach((message) => { | |||
| if (chatElementRef) { | |||
| if (message.agent_status === 3) { | |||
| const extractJson = extractFirstJSON(message.text) | |||
| if (extractJson) { | |||
| if (extractJson['action'] === 'Final Answer') { | |||
| chatElementRef.addMessage({ | |||
| text: extractJson['action_input'], | |||
| role: generateAssistantWithRandomID() | |||
| }) | |||
| } else { | |||
| const htmlContent = `<json-collapse label="执行 ${extractJson['action']}" data-json='${JSON.stringify(extractJson)}'></json-collapse>` | |||
| chatElementRef.addMessage({ | |||
| html: htmlContent, | |||
| role: generateAssistantWithRandomID() | |||
| }) | |||
| } | |||
| } else { | |||
| chatElementRef.addMessage({ | |||
| text: message.text, | |||
| role: generateAssistantWithRandomID() | |||
| }) | |||
| } | |||
| } else if (message.agent_status === 7) { | |||
| const htmlContent = `<message-collapse data-message="${message.text}"></message-collapse>` | |||
| chatElementRef.addMessage({ html: htmlContent, role: generateAssistantWithRandomID() }) | |||
| } else { | |||
| chatElementRef.addMessage({ text: message.text, role: message.role }) | |||
| } | |||
| } | |||
| }) | |||
| } catch (err) { | |||
| console.error('Failed to switch conversation:', err) | |||
| error.value = 'Failed to switch conversation' | |||
| } | |||
| } | |||
| const handleMessage = async (body, signals: Signals, chatElementRef: DeepChat) => { | |||
| console.log('localConversationConfig.value:', localConversationConfig.value) | |||
| console.log('body:', body) | |||
| const url = `${API_BASE_URL}/conversation/${currentConversation.value!.conversation_id}/messages` | |||
| const requestBody: SendMessage = { | |||
| role: 'user', | |||
| agent_id: localConversationConfig.value.agent_id, | |||
| config_id: localConversationConfig.value.config_id, | |||
| text: body.messages[0].text, | |||
| tool_config: localConversationConfig.value.tool_config, | |||
| temperature: localConversationConfig.value.temperature, | |||
| max_tokens: localConversationConfig.value.max_tokens | |||
| } | |||
| console.log('requestBody: ', requestBody) | |||
| try { | |||
| const response = await axios.post(url, requestBody, { | |||
| headers: { | |||
| accept: 'application/json', | |||
| 'Content-Type': 'application/json' | |||
| } | |||
| }) | |||
| if (response.data.code === 200) { | |||
| console.log('Response:', response) | |||
| const responseMessages = response.data.data | |||
| console.log('responseMessages:', responseMessages) | |||
| for (let i = 0; i < responseMessages.length; i++) { | |||
| if (i !== responseMessages.length - 1 && responseMessages[i].agent_status === 3) { | |||
| const extractJson = extractFirstJSON(responseMessages[i].text) | |||
| if (extractJson) { | |||
| const htmlContent = `<json-collapse label="执行 ${extractJson['action']}" data-json='${JSON.stringify(extractJson)}'></json-collapse>` | |||
| chatElementRef.addMessage({ | |||
| html: htmlContent, | |||
| role: generateAssistantWithRandomID() | |||
| }) | |||
| } else { | |||
| chatElementRef.addMessage({ | |||
| text: responseMessages[i].text, | |||
| role: generateAssistantWithRandomID() | |||
| }) | |||
| } | |||
| } else if (responseMessages[i].agent_status === -1) { | |||
| signals.onResponse({ | |||
| text: responseMessages[0].text, | |||
| role: generateAssistantWithRandomID() | |||
| }) | |||
| } else if (responseMessages[i].agent_status === 7) { | |||
| const htmlContent = `<message-collapse data-message="${responseMessages[i].text}"></message-collapse>` | |||
| chatElementRef.addMessage({ html: htmlContent, role: generateAssistantWithRandomID() }) | |||
| } else if (i === responseMessages.length - 1) { | |||
| const extractJson = extractFirstJSON(responseMessages[i].text) | |||
| if (extractJson) { | |||
| const finalAnswer = extractJson['action_input'] | |||
| signals.onResponse({ text: finalAnswer, role: generateAssistantWithRandomID() }) | |||
| } | |||
| } | |||
| } | |||
| await getConversationDetails(currentConversation.value!.conversation_id) | |||
| } else { | |||
| signals.onResponse({ error: '发送消息时发生错误,请重试' }) | |||
| } | |||
| } catch (error) { | |||
| ElMessage.error('发送消息时发生错误,请重试') | |||
| signals.onResponse({ error: '发送消息时发生错误,请重试' }) | |||
| } | |||
| } | |||
| const debugConversationConfig: DebugConversationConfig = reactive({ | |||
| config_id: null, | |||
| agent_config: {} as Agent, | |||
| history: [] | |||
| }) | |||
| const handleDebugConversation = async (body, signals: Signals, chatElementRef: DeepChat) => { | |||
| const url = `${API_BASE_URL}/conversation/debug` | |||
| const requestBody = { | |||
| config_id: debugConversationConfig.config_id, | |||
| query: body.messages[0].text, | |||
| history: debugConversationConfig.history, | |||
| agent_config: debugConversationConfig.agent_config | |||
| } | |||
| console.log('requestBody: ', requestBody) | |||
| try { | |||
| const response = await axios.post(url, requestBody, { | |||
| headers: { | |||
| accept: 'application/json', | |||
| 'Content-Type': 'application/json' | |||
| } | |||
| }) | |||
| if (response.data.code === 200) { | |||
| console.log('Response:', response) | |||
| const responseMessages = response.data.data | |||
| console.log('responseMessages:', responseMessages) | |||
| for (let i = 0; i < responseMessages.length; i++) { | |||
| if (i !== responseMessages.length - 1 && responseMessages[i].agent_status === 3) { | |||
| const extractJson = extractFirstJSON(responseMessages[i].text) | |||
| if (extractJson) { | |||
| console.log('exractJson: ', extractJson) | |||
| const htmlContent = `<json-collapse label="执行 ${extractJson['action']}" data-json='${JSON.stringify(extractJson)}'></json-collapse>` | |||
| chatElementRef.addMessage({ | |||
| html: htmlContent, | |||
| role: generateAssistantWithRandomID() | |||
| }) | |||
| } else { | |||
| chatElementRef.addMessage({ | |||
| text: responseMessages[i].text, | |||
| role: generateAssistantWithRandomID() | |||
| }) | |||
| } | |||
| } else if (responseMessages[i].agent_status === -1) { | |||
| signals.onResponse({ | |||
| text: responseMessages[0].text, | |||
| role: generateAssistantWithRandomID() | |||
| }) | |||
| } else if (responseMessages[i].agent_status === 7) { | |||
| const htmlContent = `<message-collapse data-message="${responseMessages[i].text}"></message-collapse>` | |||
| chatElementRef.addMessage({ html: htmlContent, role: generateAssistantWithRandomID() }) | |||
| } else if (i === responseMessages.length - 1) { | |||
| const extractJson = extractFirstJSON(responseMessages[i].text) | |||
| if (extractJson) { | |||
| const finalAnswer = extractJson['action_input'] | |||
| signals.onResponse({ text: finalAnswer, role: generateAssistantWithRandomID() }) | |||
| } | |||
| } | |||
| } | |||
| } else { | |||
| signals.onResponse({ error: '发送消息时发生错误,请重试' }) | |||
| } | |||
| } catch (error) { | |||
| console.log(error) | |||
| ElMessage.error('发送消息时发生错误,请重试') | |||
| signals.onResponse({ error: '发送消息时发生错误,请重试' }) | |||
| } | |||
| } | |||
| const deleteConversation = async (conversation_id: string): Promise<void> => { | |||
| try { | |||
| await axios.delete(`${API_BASE_URL}/conversation/${conversation_id}`) | |||
| conversations.value = conversations.value.filter( | |||
| (conv) => conv.conversation_id !== conversation_id | |||
| ) | |||
| if (currentConversation.value?.conversation_id === conversation_id) { | |||
| currentConversation.value = null | |||
| messages.value = [] | |||
| } | |||
| error.value = null | |||
| } catch (err) { | |||
| console.error('Failed to delete conversation:', err) | |||
| error.value = 'Failed to delete conversation' | |||
| } | |||
| } | |||
| return { | |||
| conversations, | |||
| currentConversation, | |||
| messages, | |||
| error, | |||
| createConversation, | |||
| getConversations, | |||
| debugConversationConfig, | |||
| getConversationDetails, | |||
| handleDebugConversation, | |||
| deleteConversation, | |||
| localConversationConfig, | |||
| switchConversation, | |||
| handleMessage | |||
| } | |||
| } | |||
| @@ -0,0 +1,67 @@ | |||
| const conversationSummaryQueryPrompt: string = `\n \n \n \n 请总结以上内容,总结的内容精炼且不超过5个字,并以以下JSON格式输出:\n \n | |||
| { | |||
| "summary": "对话的总结内容" | |||
| }`; | |||
| const axiosHanders = { | |||
| "accept": "application/json", | |||
| "Content-Type": "application/json" | |||
| }; | |||
| export const conversationSummary = async (chatConversation: string) => { | |||
| console.log("Conversation summary: ", chatConversation); | |||
| const requestPrompt = chatConversation + conversationSummaryQueryPrompt; | |||
| const requestBody = { | |||
| query: requestPrompt, | |||
| history: [], | |||
| stream: false, | |||
| agent_enable: false, | |||
| tool_config: [], | |||
| chat_model_config: { | |||
| api_key: "sk-cERDW9Fr2ujq8D2qYck9cpc9MtPytN26466bunfYXZVZWV7Y", | |||
| base_url: "https://api.chatanywhere.tech/v1/", | |||
| is_openai: true, | |||
| llm_model: { | |||
| "gpt-4o": { | |||
| callbacks: true, | |||
| max_tokens: 8192, | |||
| temperature: 0.8 | |||
| } | |||
| }, | |||
| platform: "OpenAI", | |||
| agent_id: -1 | |||
| } | |||
| }; | |||
| try { | |||
| const response = await fetch("http://127.0.0.1:7861/chat/chat/online", { | |||
| method: "POST", | |||
| headers: { | |||
| ...axiosHanders | |||
| }, | |||
| body: JSON.stringify(requestBody) | |||
| }); | |||
| if (!response.ok) { | |||
| throw new Error("Network response was not ok"); | |||
| } | |||
| const responseData = await response.json(); | |||
| const messageContent = responseData.choices[0].message.content; | |||
| //正则匹配messageContent中可能的json结构 | |||
| const jsonMatch = messageContent.match(/\{.*\}/s); | |||
| console.log("jsonMatch", jsonMatch); | |||
| if (jsonMatch) { | |||
| const jsonObject = JSON.parse(jsonMatch[0]); | |||
| console.log(jsonObject); | |||
| return jsonObject; | |||
| } | |||
| } catch (error) { | |||
| console.error("Error summarizing conversation:", error); | |||
| return "总结失败"; | |||
| } | |||
| }; | |||
| @@ -0,0 +1,153 @@ | |||
| export class JsonCollapse extends HTMLElement { | |||
| private header: HTMLDivElement | null = null; | |||
| private content: HTMLDivElement | null = null; | |||
| constructor() { | |||
| super(); | |||
| this.attachShadow({ mode: "open" }); | |||
| this.shadowRoot!.innerHTML = ` | |||
| <style> | |||
| :host { | |||
| display: block; | |||
| border: 1px solid #ccc; | |||
| border-radius: 8px; | |||
| font-family: 'Courier New', Courier, monospace; | |||
| } | |||
| #header { | |||
| background-color: #f1f1f1; | |||
| padding: 2px; | |||
| cursor: pointer; | |||
| font-size: 14px; | |||
| } | |||
| #content { | |||
| padding: 10px; | |||
| display: none; | |||
| white-space: pre-wrap; | |||
| background-color: #f9f9f9; | |||
| } | |||
| </style> | |||
| <div id="header">${this.getAttribute("label") || "JSON"}</div> | |||
| <div id="content"></div> | |||
| `; | |||
| this.header = this.shadowRoot!.querySelector("#header"); | |||
| this.content = this.shadowRoot!.querySelector("#content"); | |||
| this.header?.addEventListener("click", this.toggleContent.bind(this)); | |||
| } | |||
| connectedCallback() { | |||
| this.renderJson(); | |||
| } | |||
| static get observedAttributes() { | |||
| return ["data-json"]; | |||
| } | |||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
| attributeChangedCallback(name: string, _oldValue: string, _newValue: string) { | |||
| if (name === "data-json") { | |||
| this.renderJson(); | |||
| } | |||
| } | |||
| private toggleContent() { | |||
| if (this.content) { | |||
| this.content.style.display = this.content.style.display === "block" ? "none" : "block"; | |||
| } | |||
| } | |||
| private renderJson() { | |||
| const json = this.getAttribute("data-json"); | |||
| if (this.content) { | |||
| try { | |||
| const obj = JSON.parse(json || "{}"); | |||
| const formattedJson = JSON.stringify(obj, null, 2); | |||
| this.content.textContent = formattedJson; | |||
| } catch (e) { | |||
| this.content.textContent = "Invalid JSON"; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| export class MessageCollapse extends HTMLElement { | |||
| private header: HTMLDivElement | null = null; | |||
| private content: HTMLDivElement | null = null; | |||
| private isCollapsed = true; | |||
| constructor() { | |||
| super(); | |||
| this.attachShadow({ mode: "open" }); | |||
| this.shadowRoot!.innerHTML = ` | |||
| <style> | |||
| :host { | |||
| display: block; | |||
| border: 1px solid #ccc; | |||
| border-radius: 8px; | |||
| font-family: 'Arial', sans-serif; | |||
| } | |||
| #header { | |||
| background-color: #f1f1f1; | |||
| padding: 10px; | |||
| cursor: pointer; | |||
| font-size: 14px; | |||
| } | |||
| #content { | |||
| padding: 10px; | |||
| display: none; | |||
| white-space: pre-wrap; | |||
| background-color: #f9f9f9; | |||
| } | |||
| </style> | |||
| <div id="header"></div> | |||
| <div id="content"></div> | |||
| `; | |||
| this.header = this.shadowRoot!.querySelector("#header"); | |||
| this.content = this.shadowRoot!.querySelector("#content"); | |||
| this.header?.addEventListener("click", this.toggleContent.bind(this)); | |||
| } | |||
| connectedCallback() { | |||
| this.renderMessage(); | |||
| } | |||
| static get observedAttributes() { | |||
| return ["data-message"]; | |||
| } | |||
| attributeChangedCallback(name: string, _oldValue: string, _newValue: string) { | |||
| if (name === "data-message") { | |||
| this.renderMessage(); | |||
| } | |||
| } | |||
| private toggleContent() { | |||
| if (this.content) { | |||
| this.isCollapsed = !this.isCollapsed; | |||
| this.content.style.display = this.isCollapsed ? "none" : "block"; | |||
| this.header!.textContent = this.isCollapsed ? this.getCollapsedMessage() : this.getFullMessage(); | |||
| } | |||
| } | |||
| private renderMessage() { | |||
| const message = this.getAttribute("data-message") || ""; | |||
| if (this.header && this.content) { | |||
| this.header.textContent = this.getCollapsedMessage(); | |||
| this.content.textContent = message; | |||
| } | |||
| } | |||
| private getCollapsedMessage() { | |||
| const message = this.getAttribute("data-message") || ""; | |||
| return message.length > 50 ? message.substring(0, 50) + "..." : message; | |||
| } | |||
| private getFullMessage() { | |||
| return this.getAttribute("data-message") || ""; | |||
| } | |||
| } | |||
| @@ -0,0 +1,363 @@ | |||
| import { ref } from "vue"; | |||
| export const products = ref([ | |||
| { | |||
| type: 1, | |||
| isSetup: true, | |||
| name: "商店系统", | |||
| description: "管理您的在线商店和产品目录" | |||
| }, | |||
| { | |||
| type: 2, | |||
| isSetup: false, | |||
| name: "预约系统", | |||
| description: "管理客户预约和日程安排" | |||
| }, | |||
| { | |||
| type: 3, | |||
| isSetup: true, | |||
| name: "客户服务中心", | |||
| description: "处理客户查询和支持请求" | |||
| }, | |||
| { | |||
| type: 4, | |||
| isSetup: true, | |||
| name: "用户管理系统", | |||
| description: "管理用户账户和权限设置" | |||
| }, | |||
| { | |||
| type: 5, | |||
| isSetup: false, | |||
| name: "库存管理系统", | |||
| description: "追踪和管理产品库存" | |||
| }, | |||
| { | |||
| type: 1, | |||
| isSetup: true, | |||
| name: "支付网关", | |||
| description: "处理在线支付交易" | |||
| }, | |||
| { | |||
| type: 2, | |||
| isSetup: true, | |||
| name: "员工排班系统", | |||
| description: "管理员工工作时间和排班" | |||
| }, | |||
| { | |||
| type: 3, | |||
| isSetup: false, | |||
| name: "知识库系统", | |||
| description: "集中存储和管理公司信息" | |||
| }, | |||
| { | |||
| type: 4, | |||
| isSetup: true, | |||
| name: "会员管理系统", | |||
| description: "管理会员信息和忠诚度计划" | |||
| }, | |||
| { | |||
| type: 5, | |||
| isSetup: true, | |||
| name: "数据分析平台", | |||
| description: "分析业务数据和生成报告" | |||
| }, | |||
| { | |||
| type: 1, | |||
| isSetup: false, | |||
| name: "多渠道销售系统", | |||
| description: "整合多个销售渠道的订单管理" | |||
| }, | |||
| { | |||
| type: 2, | |||
| isSetup: true, | |||
| name: "会议室预订系统", | |||
| description: "管理公司会议室的预订" | |||
| }, | |||
| { | |||
| type: 3, | |||
| isSetup: true, | |||
| name: "工单管理系统", | |||
| description: "跟踪和管理客户服务工单" | |||
| }, | |||
| { | |||
| type: 4, | |||
| isSetup: false, | |||
| name: "社交媒体管理", | |||
| description: "管理多个社交媒体账户和内容" | |||
| }, | |||
| { | |||
| type: 5, | |||
| isSetup: true, | |||
| name: "项目管理工具", | |||
| description: "跟踪项目进度和资源分配" | |||
| }, | |||
| { | |||
| type: 1, | |||
| isSetup: true, | |||
| name: "折扣管理系统", | |||
| description: "创建和管理促销活动和折扣" | |||
| }, | |||
| { | |||
| type: 2, | |||
| isSetup: false, | |||
| name: "培训管理系统", | |||
| description: "组织和跟踪员工培训课程" | |||
| }, | |||
| { | |||
| type: 3, | |||
| isSetup: true, | |||
| name: "聊天机器人平台", | |||
| description: "部署智能客服聊天机器人" | |||
| }, | |||
| { | |||
| type: 4, | |||
| isSetup: true, | |||
| name: "反馈收集系统", | |||
| description: "收集和分析客户反馈" | |||
| }, | |||
| { | |||
| type: 5, | |||
| isSetup: false, | |||
| name: "资产管理系统", | |||
| description: "跟踪公司资产和设备" | |||
| }, | |||
| { | |||
| type: 1, | |||
| isSetup: true, | |||
| name: "物流跟踪系统", | |||
| description: "实时跟踪订单配送状态" | |||
| }, | |||
| { | |||
| type: 2, | |||
| isSetup: true, | |||
| name: "绩效评估系统", | |||
| description: "管理员工绩效评估流程" | |||
| }, | |||
| { | |||
| type: 3, | |||
| isSetup: false, | |||
| name: "文档管理系统", | |||
| description: "存储和组织公司文档" | |||
| }, | |||
| { | |||
| type: 4, | |||
| isSetup: true, | |||
| name: "电子邮件营销平台", | |||
| description: "创建和管理电子邮件营销活动" | |||
| }, | |||
| { | |||
| type: 5, | |||
| isSetup: true, | |||
| name: "业务流程自动化", | |||
| description: "自动化重复性业务流程" | |||
| }, | |||
| { | |||
| type: 1, | |||
| isSetup: false, | |||
| name: "退货管理系统", | |||
| description: "处理产品退货和退款" | |||
| }, | |||
| { | |||
| type: 2, | |||
| isSetup: true, | |||
| name: "远程办公管理", | |||
| description: "管理远程工作团队和任务" | |||
| }, | |||
| { | |||
| type: 3, | |||
| isSetup: true, | |||
| name: "多语言支持系统", | |||
| description: "为客户提供多语言支持服务" | |||
| }, | |||
| { | |||
| type: 4, | |||
| isSetup: false, | |||
| name: "内容管理系统", | |||
| description: "管理网站和营销内容" | |||
| }, | |||
| { | |||
| type: 5, | |||
| isSetup: true, | |||
| name: "风险评估工具", | |||
| description: "评估和管理业务风险" | |||
| }, { | |||
| type: 1, | |||
| isSetup: true, | |||
| name: "商店系统", | |||
| description: "管理您的在线商店和产品目录" | |||
| }, | |||
| { | |||
| type: 2, | |||
| isSetup: false, | |||
| name: "预约系统", | |||
| description: "管理客户预约和日程安排" | |||
| }, | |||
| { | |||
| type: 3, | |||
| isSetup: true, | |||
| name: "客户服务中心", | |||
| description: "处理客户查询和支持请求" | |||
| }, | |||
| { | |||
| type: 4, | |||
| isSetup: true, | |||
| name: "用户管理系统", | |||
| description: "管理用户账户和权限设置" | |||
| }, | |||
| { | |||
| type: 5, | |||
| isSetup: false, | |||
| name: "库存管理系统", | |||
| description: "追踪和管理产品库存" | |||
| }, | |||
| { | |||
| type: 1, | |||
| isSetup: true, | |||
| name: "支付网关", | |||
| description: "处理在线支付交易" | |||
| }, | |||
| { | |||
| type: 2, | |||
| isSetup: true, | |||
| name: "员工排班系统", | |||
| description: "管理员工工作时间和排班" | |||
| }, | |||
| { | |||
| type: 3, | |||
| isSetup: false, | |||
| name: "知识库系统", | |||
| description: "集中存储和管理公司信息" | |||
| }, | |||
| { | |||
| type: 4, | |||
| isSetup: true, | |||
| name: "会员管理系统", | |||
| description: "管理会员信息和忠诚度计划" | |||
| }, | |||
| { | |||
| type: 5, | |||
| isSetup: true, | |||
| name: "数据分析平台", | |||
| description: "分析业务数据和生成报告" | |||
| }, | |||
| { | |||
| type: 1, | |||
| isSetup: false, | |||
| name: "多渠道销售系统", | |||
| description: "整合多个销售渠道的订单管理" | |||
| }, | |||
| { | |||
| type: 2, | |||
| isSetup: true, | |||
| name: "会议室预订系统", | |||
| description: "管理公司会议室的预订" | |||
| }, | |||
| { | |||
| type: 3, | |||
| isSetup: true, | |||
| name: "工单管理系统", | |||
| description: "跟踪和管理客户服务工单" | |||
| }, | |||
| { | |||
| type: 4, | |||
| isSetup: false, | |||
| name: "社交媒体管理", | |||
| description: "管理多个社交媒体账户和内容" | |||
| }, | |||
| { | |||
| type: 5, | |||
| isSetup: true, | |||
| name: "项目管理工具", | |||
| description: "跟踪项目进度和资源分配" | |||
| }, | |||
| { | |||
| type: 1, | |||
| isSetup: true, | |||
| name: "折扣管理系统", | |||
| description: "创建和管理促销活动和折扣" | |||
| }, | |||
| { | |||
| type: 2, | |||
| isSetup: false, | |||
| name: "培训管理系统", | |||
| description: "组织和跟踪员工培训课程" | |||
| }, | |||
| { | |||
| type: 3, | |||
| isSetup: true, | |||
| name: "聊天机器人平台", | |||
| description: "部署智能客服聊天机器人" | |||
| }, | |||
| { | |||
| type: 4, | |||
| isSetup: true, | |||
| name: "反馈收集系统", | |||
| description: "收集和分析客户反馈" | |||
| }, | |||
| { | |||
| type: 5, | |||
| isSetup: false, | |||
| name: "资产管理系统", | |||
| description: "跟踪公司资产和设备" | |||
| }, | |||
| { | |||
| type: 1, | |||
| isSetup: true, | |||
| name: "物流跟踪系统", | |||
| description: "实时跟踪订单配送状态" | |||
| }, | |||
| { | |||
| type: 2, | |||
| isSetup: true, | |||
| name: "绩效评估系统", | |||
| description: "管理员工绩效评估流程" | |||
| }, | |||
| { | |||
| type: 3, | |||
| isSetup: false, | |||
| name: "文档管理系统", | |||
| description: "存储和组织公司文档" | |||
| }, | |||
| { | |||
| type: 4, | |||
| isSetup: true, | |||
| name: "电子邮件营销平台", | |||
| description: "创建和管理电子邮件营销活动" | |||
| }, | |||
| { | |||
| type: 5, | |||
| isSetup: true, | |||
| name: "业务流程自动化", | |||
| description: "自动化重复性业务流程" | |||
| }, | |||
| { | |||
| type: 1, | |||
| isSetup: false, | |||
| name: "退货管理系统", | |||
| description: "处理产品退货和退款" | |||
| }, | |||
| { | |||
| type: 2, | |||
| isSetup: true, | |||
| name: "远程办公管理", | |||
| description: "管理远程工作团队和任务" | |||
| }, | |||
| { | |||
| type: 3, | |||
| isSetup: true, | |||
| name: "多语言支持系统", | |||
| description: "为客户提供多语言支持服务" | |||
| }, | |||
| { | |||
| type: 4, | |||
| isSetup: false, | |||
| name: "内容管理系统", | |||
| description: "管理网站和营销内容" | |||
| }, | |||
| { | |||
| type: 5, | |||
| isSetup: true, | |||
| name: "风险评估工具", | |||
| description: "评估和管理业务风险" | |||
| } | |||
| ]); | |||
| @@ -0,0 +1,302 @@ | |||
| <template> | |||
| <el-dialog | |||
| v-model="dialogVisible" | |||
| :title="`${kbName} 的文件列表`" | |||
| width="80%" | |||
| :fullscreen="false" | |||
| :close-on-click-modal="false" | |||
| :close-on-press-escape="false" | |||
| custom-class="fixed-size-dialog" | |||
| > | |||
| <div class="dialog-header"> | |||
| <div class="left-side"> | |||
| <el-button @click="handleNewFile"> | |||
| <Icon icon="material-symbols:add" /> | |||
| 新建文件 | |||
| </el-button> | |||
| </div> | |||
| <div class="right-side"> | |||
| <el-input v-model="searchValue" style="width: 300px" placeholder="请输入文件名称" clearable> | |||
| <template #suffix> | |||
| <Icon v-show="searchValue.length === 0" icon="ri:search-line" /> | |||
| </template> | |||
| </el-input> | |||
| </div> | |||
| </div> | |||
| <div class="file-list-container" @dragover.prevent @drop.prevent="handleDrop"> | |||
| <el-scrollbar height="500px"> | |||
| <el-row :gutter="16"> | |||
| <el-col | |||
| v-for="doc in filteredDocuments" | |||
| :key="doc.file_name" | |||
| :xs="24" | |||
| :sm="12" | |||
| :md="8" | |||
| :lg="6" | |||
| :xl="4" | |||
| > | |||
| <ReFileCard | |||
| :file-name="doc.file_name" | |||
| :last-updated="doc.create_time" | |||
| :knowledge-base-name="kbName" | |||
| @delete="handleDeleteFile" | |||
| /> | |||
| </el-col> | |||
| </el-row> | |||
| </el-scrollbar> | |||
| </div> | |||
| <!-- 新建文件对话框 --> | |||
| <el-dialog | |||
| v-model="newFileDialogVisible" | |||
| title="新建文件" | |||
| width="500px" | |||
| :close-on-click-modal="false" | |||
| :close-on-press-escape="false" | |||
| > | |||
| <el-form :model="uploadOptions" label-width="150px"> | |||
| <el-form-item label="覆盖已有文件"> | |||
| <el-switch v-model="uploadOptions.override" /> | |||
| </el-form-item> | |||
| <el-form-item label="上传后向量化"> | |||
| <el-switch v-model="uploadOptions.to_vector_store" /> | |||
| </el-form-item> | |||
| <el-form-item label="单段文本最大长度"> | |||
| <el-input-number v-model="uploadOptions.chunk_size" :min="1" /> | |||
| </el-form-item> | |||
| <el-form-item label="相邻文本重合长度"> | |||
| <el-input-number v-model="uploadOptions.chunk_overlap" :min="0" /> | |||
| </el-form-item> | |||
| <el-form-item label="中文标题加强"> | |||
| <el-switch v-model="uploadOptions.zh_title_enhance" /> | |||
| </el-form-item> | |||
| <el-form-item label="暂不保存向量库"> | |||
| <el-switch v-model="uploadOptions.not_refresh_vs_cache" /> | |||
| </el-form-item> | |||
| <el-form-item label="选择文件"> | |||
| <el-upload | |||
| class="file-upload" | |||
| :auto-upload="false" | |||
| :on-change="handleNewFileChange" | |||
| :file-list="newFileUpload" | |||
| multiple | |||
| > | |||
| <el-button type="primary">选择文件</el-button> | |||
| </el-upload> | |||
| </el-form-item> | |||
| </el-form> | |||
| <template #footer> | |||
| <span class="dialog-footer"> | |||
| <el-button @click="newFileDialogVisible = false">取消</el-button> | |||
| <el-button type="primary" @click="handleUploadNewFile">确定</el-button> | |||
| </span> | |||
| </template> | |||
| </el-dialog> | |||
| <template #footer> | |||
| <span class="dialog-footer"> | |||
| <el-button @click="dialogVisible = false">关闭</el-button> | |||
| </span> | |||
| </template> | |||
| </el-dialog> | |||
| </template> | |||
| <script setup lang="ts"> | |||
| import { ref, watch, computed, reactive } from 'vue' | |||
| import ReFileCard from '../../components/fileCard.vue' | |||
| import { useKnowledgeBase } from './kbapi' | |||
| import { Icon } from '@iconify/vue' | |||
| import { ElMessage } from 'element-plus' | |||
| defineOptions({ | |||
| name: 'FileListDialog' | |||
| }) | |||
| const searchValue = ref('') | |||
| const newFileUpload = ref<[]>([]) | |||
| const newFileDialogVisible = ref(false) | |||
| const uploadOptions = reactive({ | |||
| override: false, | |||
| to_vector_store: true, | |||
| chunk_size: 250, | |||
| chunk_overlap: 50, | |||
| zh_title_enhance: false, | |||
| not_refresh_vs_cache: false | |||
| }) | |||
| const filteredDocuments = computed(() => { | |||
| return props.documents.filter((doc) => | |||
| doc.file_name.toLowerCase().includes(searchValue.value.toLowerCase()) | |||
| ) | |||
| }) | |||
| const handleNewFileChange = (file: File, fileList: File[]) => { | |||
| newFileUpload.value = fileList.map((file) => file.raw) as [] | |||
| } | |||
| interface Props { | |||
| visible: boolean | |||
| kbName: string | |||
| documents: { | |||
| kb_name: string | |||
| file_name: string | |||
| file_ext: string | |||
| file_version: number | |||
| document_loader: string | |||
| docs_count: number | |||
| text_splitter: string | |||
| create_time: string | |||
| in_folder: boolean | |||
| in_db: boolean | |||
| file_mtime: number | |||
| file_size: number | |||
| custom_docs: boolean | |||
| No: number | |||
| }[] | |||
| } | |||
| const props = defineProps<Props>() | |||
| const emit = defineEmits(['update:visible', 'refresh-documents']) | |||
| const dialogVisible = ref(props.visible) | |||
| const { deleteDocument, fetchDocuments, uploadDocument } = useKnowledgeBase() | |||
| watch( | |||
| () => props.visible, | |||
| (newValue) => { | |||
| dialogVisible.value = newValue | |||
| } | |||
| ) | |||
| watch(dialogVisible, (newValue) => { | |||
| emit('update:visible', newValue) | |||
| }) | |||
| const handleNewFile = () => { | |||
| newFileDialogVisible.value = true | |||
| } | |||
| const handleUploadNewFile = async () => { | |||
| const files = newFileUpload.value | |||
| if (files.length === 0) { | |||
| ElMessage.warning('请选择要上传的文件') | |||
| return | |||
| } | |||
| const success = await uploadDocument(files, props.kbName, uploadOptions) | |||
| if (success) { | |||
| newFileDialogVisible.value = false | |||
| await refreshDocuments() | |||
| } | |||
| // Clear the file list after upload attempt | |||
| newFileUpload.value = [] | |||
| } | |||
| const handleDrop = async (e: DragEvent) => { | |||
| e.preventDefault() | |||
| const files = e.dataTransfer?.files | |||
| if (files && files.length > 0) { | |||
| for (let i = 0; i < files.length; i++) { | |||
| try { | |||
| const success = await uploadDocument([files[i]], props.kbName) | |||
| if (success) { | |||
| ElMessage.success(`文件 ${files[i].name} 上传成功`) | |||
| } | |||
| } catch (error) { | |||
| if (error instanceof Error) { | |||
| ElMessage.error(`文件 ${files[i].name} 上传失败: ${error.message}`) | |||
| } else { | |||
| ElMessage.error(`文件 ${files[i].name} 上传失败: 未知错误`) | |||
| } | |||
| } | |||
| } | |||
| await refreshDocuments() | |||
| } | |||
| } | |||
| const handleDeleteFile = async (knowledgeBaseName: string, fileName: string) => { | |||
| try { | |||
| await deleteDocument(knowledgeBaseName, fileName) | |||
| await refreshDocuments() | |||
| } catch (error) { | |||
| console.error('删除文件失败:', error) | |||
| } | |||
| } | |||
| const refreshDocuments = async () => { | |||
| await fetchDocuments(props.kbName) | |||
| emit('refresh-documents') | |||
| } | |||
| </script> | |||
| <style scoped> | |||
| .fixed-size-dialog { | |||
| display: flex; | |||
| flex-direction: column; | |||
| height: 90vh; | |||
| max-height: 800px; | |||
| } | |||
| .fixed-size-dialog :deep(.el-dialog__body) { | |||
| flex: 1; | |||
| overflow: hidden; | |||
| padding: 10px; | |||
| } | |||
| .dialog-header { | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| margin-bottom: 16px; | |||
| } | |||
| .left-side { | |||
| display: flex; | |||
| align-items: center; | |||
| } | |||
| .right-side { | |||
| display: flex; | |||
| align-items: center; | |||
| } | |||
| .file-list-container { | |||
| height: 100%; | |||
| } | |||
| .el-row { | |||
| margin-bottom: -16px; | |||
| margin-right: -8px; | |||
| margin-left: -8px; | |||
| } | |||
| .el-col { | |||
| padding-bottom: 16px; | |||
| padding-right: 8px; | |||
| padding-left: 8px; | |||
| } | |||
| .file-upload { | |||
| display: inline-block; | |||
| } | |||
| .file-upload .el-upload { | |||
| width: auto; | |||
| } | |||
| .file-upload .el-upload-dragger { | |||
| width: auto; | |||
| height: auto; | |||
| border: none; | |||
| border-radius: 0; | |||
| } | |||
| .file-upload .el-upload-dragger:hover { | |||
| border: none; | |||
| } | |||
| .file-upload .el-button { | |||
| margin-right: 10px; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,280 @@ | |||
| import axios from 'axios' | |||
| import { ref, reactive, computed } from 'vue' | |||
| import { ElLoading, ElMessage } from 'element-plus' | |||
| export const KB_API_BASE_URL = 'http://127.0.0.1:7861/knowledge_base' | |||
| export interface KnowledgeBase { | |||
| id: number | |||
| kb_name: string | |||
| kb_info: string | |||
| vs_type: string | |||
| embed_model: string | |||
| file_count: number | |||
| create_time: string | |||
| } | |||
| export interface Document { | |||
| kb_name: string | |||
| file_name: string | |||
| file_ext: string | |||
| file_version: number | |||
| document_loader: string | |||
| docs_count: number | |||
| text_splitter: string | |||
| create_time: string | |||
| in_folder: boolean | |||
| in_db: boolean | |||
| file_mtime: number | |||
| file_size: number | |||
| custom_docs: boolean | |||
| No: number | |||
| } | |||
| export interface UploadDocumentOptions { | |||
| override?: boolean | |||
| to_vector_store?: boolean | |||
| chunk_size?: number | |||
| chunk_overlap?: number | |||
| zh_title_enhance?: boolean | |||
| not_refresh_vs_cache?: boolean | |||
| } | |||
| export const useKnowledgeBase = () => { | |||
| const knowledgeBases = ref<KnowledgeBase[]>([]) | |||
| const activeKnowledgeBase = ref('') | |||
| const isShowKnowledgeBaseDialog = ref(false) | |||
| const knowledgeBaseForm = reactive({ | |||
| knowledge_base_name: '', | |||
| vector_store_type: 'faiss', | |||
| kb_info: '' | |||
| }) | |||
| // 文档列表 | |||
| const documents = ref<Document[]>([]) | |||
| const isSaveButtonDisabled = computed(() => !knowledgeBaseForm.knowledge_base_name?.trim()) | |||
| const fetchAllKnowledgeBases = async () => { | |||
| try { | |||
| const response = await axios.get(`${KB_API_BASE_URL}/list_knowledge_bases`) | |||
| if (response.data.code === 200) { | |||
| knowledgeBases.value = response.data.data | |||
| } else { | |||
| ElMessage.error(response.data.msg || '获取知识库列表失败') | |||
| } | |||
| } catch (error) { | |||
| ElMessage.error('无法获取知识库列表') | |||
| console.error('Failed to fetch knowledge bases:', error) | |||
| } | |||
| } | |||
| const createKnowledgeBase = async () => { | |||
| try { | |||
| const response = await axios.post( | |||
| `${KB_API_BASE_URL}/create_knowledge_base`, | |||
| knowledgeBaseForm | |||
| ) | |||
| if (response.data.code === 200) { | |||
| ElMessage.success('知识库创建成功') | |||
| await fetchAllKnowledgeBases() | |||
| return response.data.data | |||
| } else { | |||
| ElMessage.error(response.data.msg) | |||
| } | |||
| } catch (error) { | |||
| ElMessage.error('创建知识库失败') | |||
| console.error('Failed to create knowledge base:', error) | |||
| } | |||
| return null | |||
| } | |||
| const deleteKnowledgeBase = async (kbName: string) => { | |||
| try { | |||
| const response = await axios.post(`${KB_API_BASE_URL}/delete_knowledge_base`, kbName) | |||
| if (response.data.code === 200) { | |||
| ElMessage.success('知识库删除成功') | |||
| await fetchAllKnowledgeBases() | |||
| activeKnowledgeBase.value = '' | |||
| } else { | |||
| ElMessage.error(response.data.msg || '删除知识库失败') | |||
| } | |||
| } catch (error) { | |||
| ElMessage.error('删除知识库失败') | |||
| console.error('Failed to delete knowledge base:', error) | |||
| } | |||
| } | |||
| const fetchDocuments = async (kbName: string) => { | |||
| try { | |||
| const response = await axios.get(`${KB_API_BASE_URL}/list_files`, { | |||
| params: { knowledge_base_name: kbName } | |||
| }) | |||
| if (response.data.code === 200) { | |||
| documents.value = response.data.data | |||
| console.log('documents.value:', documents.value) | |||
| } else { | |||
| ElMessage.error(response.data.msg) | |||
| } | |||
| } catch (error) { | |||
| ElMessage.error('无法获取文档列表') | |||
| console.error('Failed to fetch documents:', error) | |||
| } | |||
| } | |||
| const uploadDocument = async ( | |||
| file: File | File[], | |||
| kbName: string, | |||
| options: UploadDocumentOptions = {} | |||
| ) => { | |||
| const formData = new FormData() | |||
| // 处理单个文件或多个文件 | |||
| if (Array.isArray(file)) { | |||
| file.forEach((f) => formData.append('files', f)) | |||
| } else { | |||
| formData.append('files', file) | |||
| } | |||
| formData.append('knowledge_base_name', kbName) | |||
| // 添加可选参数 | |||
| if (options.override !== undefined) formData.append('override', options.override.toString()) | |||
| if (options.to_vector_store !== undefined) | |||
| formData.append('to_vector_store', options.to_vector_store.toString()) | |||
| if (options.chunk_size !== undefined) | |||
| formData.append('chunk_size', Math.floor(options.chunk_size).toString()) | |||
| if (options.chunk_overlap !== undefined) | |||
| formData.append('chunk_overlap', Math.floor(options.chunk_overlap).toString()) | |||
| if (options.zh_title_enhance !== undefined) | |||
| formData.append('zh_title_enhance', options.zh_title_enhance.toString()) | |||
| if (options.not_refresh_vs_cache !== undefined) | |||
| formData.append('not_refresh_vs_cache', options.not_refresh_vs_cache.toString()) | |||
| // 显示加载动画 | |||
| const loadingInstance = ElLoading.service({ | |||
| lock: true, | |||
| text: '文件上传中...', | |||
| background: 'rgba(0, 0, 0, 0.7)' | |||
| }) | |||
| try { | |||
| const response = await axios.post(`${KB_API_BASE_URL}/upload_docs`, formData, { | |||
| headers: { 'Content-Type': 'multipart/form-data' } | |||
| }) | |||
| if (response.data.code === 200) { | |||
| ElMessage.success('文档上传成功') | |||
| // 假设 fetchDocuments 是一个更新文档列表的函数 | |||
| await fetchDocuments(kbName) | |||
| return true | |||
| } else { | |||
| ElMessage.error(response.data.msg || '上传文档失败') | |||
| return false | |||
| } | |||
| } catch (error) { | |||
| if (axios.isAxiosError(error) && error.response) { | |||
| // 处理 Axios 错误 | |||
| const errorMessage = | |||
| error.response.data?.detail || error.response.data?.msg || '上传文档失败' | |||
| ElMessage.error(errorMessage) | |||
| console.error('Failed to upload document:', error.response.data) | |||
| } else { | |||
| // 处理非 Axios 错误 | |||
| ElMessage.error('上传文档时发生未知错误') | |||
| console.error('Failed to upload document:', error) | |||
| } | |||
| return false | |||
| } finally { | |||
| // 关闭加载动画 | |||
| loadingInstance.close() | |||
| } | |||
| } | |||
| const deleteDocument = async (kbName: string, fileName: string) => { | |||
| try { | |||
| const response = await axios.post(`${KB_API_BASE_URL}/delete_docs`, { | |||
| knowledge_base_name: kbName, | |||
| file_names: [fileName], | |||
| delete_content: true, | |||
| not_refresh_vs_cache: false | |||
| }) | |||
| if (response.data.code === 200) { | |||
| ElMessage.success('文档删除成功') | |||
| await fetchDocuments(kbName) | |||
| } else { | |||
| ElMessage.error(response.data.msg) | |||
| } | |||
| } catch (error) { | |||
| ElMessage.error('删除文档失败') | |||
| console.error('Failed to delete document:', error) | |||
| } | |||
| } | |||
| const updateKnowledgeBaseInfo = async (kbName: string, kbInfo: string) => { | |||
| try { | |||
| const response = await axios.post(`${KB_API_BASE_URL}/update_info`, { | |||
| kb_name: kbName, | |||
| kb_info: kbInfo | |||
| }) | |||
| if (response.data.code === 200) { | |||
| ElMessage.success('知识库信息更新成功') | |||
| await fetchAllKnowledgeBases() | |||
| } else { | |||
| ElMessage.error(response.data.msg) | |||
| } | |||
| } catch (error) { | |||
| ElMessage.error('更新知识库信息失败') | |||
| console.error('Failed to update knowledge base info:', error) | |||
| } | |||
| } | |||
| const handleNewKnowledgeBase = () => { | |||
| activeKnowledgeBase.value = '' | |||
| Object.assign(knowledgeBaseForm, { | |||
| kb_name: '', | |||
| vs_type: 'faiss', | |||
| kb_info: '' | |||
| }) | |||
| } | |||
| const handleSaveKnowledgeBase = async () => { | |||
| if (activeKnowledgeBase.value) { | |||
| await updateKnowledgeBaseInfo(activeKnowledgeBase.value, knowledgeBaseForm.kb_info || '') | |||
| } else { | |||
| const newKnowledgeBase = await createKnowledgeBase() | |||
| if (newKnowledgeBase) { | |||
| activeKnowledgeBase.value = newKnowledgeBase.kb_name | |||
| } | |||
| } | |||
| } | |||
| const handleDeleteKnowledgeBase = async (kbName: string) => { | |||
| if (kbName) { | |||
| await deleteKnowledgeBase(kbName) | |||
| } else { | |||
| ElMessage.warning('请选择要删除的知识库') | |||
| } | |||
| } | |||
| const handleKnowledgeBaseSelect = async (kbName: string) => { | |||
| activeKnowledgeBase.value = kbName | |||
| await fetchDocuments(kbName) | |||
| } | |||
| return { | |||
| knowledgeBases, | |||
| activeKnowledgeBase, | |||
| isShowKnowledgeBaseDialog, | |||
| knowledgeBaseForm, | |||
| documents, | |||
| isSaveButtonDisabled, | |||
| fetchAllKnowledgeBases, | |||
| handleNewKnowledgeBase, | |||
| handleSaveKnowledgeBase, | |||
| handleDeleteKnowledgeBase, | |||
| handleKnowledgeBaseSelect, | |||
| uploadDocument, | |||
| deleteDocument, | |||
| deleteKnowledgeBase, | |||
| updateKnowledgeBaseInfo, | |||
| fetchDocuments | |||
| } | |||
| } | |||
| @@ -0,0 +1,261 @@ | |||
| <template> | |||
| <div class="kb-layout"> | |||
| <el-container> | |||
| <el-header class="flex items-center"> | |||
| <Icon icon="weui:back-filled" @click="goBack" class="cursor-pointer mr-2" /> | |||
| <h1>知识中心</h1> | |||
| </el-header> | |||
| <el-main> | |||
| <div class="w-full flex justify-between mb-4"> | |||
| <el-button @click="handleNewKnowledgeBaseClick"> | |||
| <Icon icon="material-symbols:add" /> | |||
| 新建知识库 | |||
| </el-button> | |||
| <el-input | |||
| v-model="searchValue" | |||
| style="width: 300px" | |||
| placeholder="请输入知识库名称" | |||
| clearable | |||
| > | |||
| <template #suffix> | |||
| <Icon v-show="searchValue.length === 0" icon="ri:search-line" /> | |||
| </template> | |||
| </el-input> | |||
| </div> | |||
| <div v-loading="dataLoading"> | |||
| <el-empty | |||
| v-show="displayedKnowledgeBases.length === 0" | |||
| :description="`${searchValue} 知识库不存在`" | |||
| /> | |||
| <template v-if="pagination.total > 0"> | |||
| <el-row :gutter="16"> | |||
| <el-col | |||
| v-for="kb in displayedKnowledgeBases" | |||
| :key="kb.kb_name" | |||
| :xs="24" | |||
| :sm="12" | |||
| :md="8" | |||
| :lg="6" | |||
| :xl="4" | |||
| > | |||
| <ReCard | |||
| :product="mapKnowledgeBaseToProduct(kb)" | |||
| @delete-item="handleDeleteItem" | |||
| @manage-product="handleManageProduct" | |||
| /> | |||
| </el-col> | |||
| </el-row> | |||
| </template> | |||
| </div> | |||
| </el-main> | |||
| <el-footer> | |||
| <el-pagination | |||
| v-model:currentPage="pagination.current" | |||
| class="float-right mt-4" | |||
| :page-size="pagination.pageSize" | |||
| :total="pagination.total" | |||
| :page-sizes="[12, 24, 36]" | |||
| :background="true" | |||
| layout="total, sizes, prev, pager, next, jumper" | |||
| @size-change="onPageSizeChange" | |||
| @current-change="onCurrentChange" | |||
| /> | |||
| </el-footer> | |||
| </el-container> | |||
| <ListDialogForm v-model:visible="isShowKnowledgeBaseDialog" :data="knowledgeBaseForm" /> | |||
| <FileListDialog | |||
| v-model:visible="isShowFileListDialog" | |||
| :kb-name="activeKnowledgeBase" | |||
| :documents="documents" | |||
| @refresh-documents="handleRefreshDocuments" | |||
| /> | |||
| <NewKBDialog | |||
| v-model:visible="isShowKnowledgeBaseDialog" | |||
| :loading="loading" | |||
| :form="knowledgeBaseForm" | |||
| @save="handleSaveKnowledgeBaseClick" | |||
| /> | |||
| </div> | |||
| </template> | |||
| <script setup lang="ts"> | |||
| import { ref, computed, onMounted, nextTick } from 'vue' | |||
| import { ElMessage, ElMessageBox } from 'element-plus' | |||
| import ReCard from '../../components/ListCard.vue' | |||
| import { Icon } from '@iconify/vue' | |||
| import { useKnowledgeBase } from './kbapi' | |||
| import FileListDialog from './filelistdialog.vue' | |||
| import NewKBDialog from './newkbdialog.vue' | |||
| import { router } from '../../router' | |||
| const { | |||
| knowledgeBases, | |||
| activeKnowledgeBase, | |||
| isShowKnowledgeBaseDialog, | |||
| knowledgeBaseForm, | |||
| documents, | |||
| fetchAllKnowledgeBases, | |||
| handleNewKnowledgeBase, | |||
| handleSaveKnowledgeBase, | |||
| handleDeleteKnowledgeBase, | |||
| fetchDocuments | |||
| } = useKnowledgeBase() | |||
| const pagination = ref({ current: 1, pageSize: 12, total: 0 }) | |||
| const dataLoading = ref(true) | |||
| const loading = ref(false) | |||
| const searchValue = ref('') | |||
| const isShowFileListDialog = ref(false) | |||
| const displayedKnowledgeBases = computed(() => { | |||
| const start = (pagination.value.current - 1) * pagination.value.pageSize | |||
| const end = start + pagination.value.pageSize | |||
| return knowledgeBases.value | |||
| .slice(start, end) | |||
| .filter((kb) => kb.kb_name.toLowerCase().includes(searchValue.value.toLowerCase())) | |||
| }) | |||
| const mapKnowledgeBaseToProduct = (kb) => ({ | |||
| name: kb.kb_name, | |||
| isSetup: true, | |||
| description: kb.kb_info, | |||
| type: 1, | |||
| fileCount: kb.file_count, | |||
| lastUpdatedAt: kb.create_time | |||
| }) | |||
| onMounted(async () => { | |||
| dataLoading.value = true | |||
| await fetchAllKnowledgeBases() | |||
| pagination.value.total = knowledgeBases.value.length | |||
| dataLoading.value = false | |||
| }) | |||
| const onPageSizeChange = (size: number) => { | |||
| pagination.value.pageSize = size | |||
| pagination.value.current = 1 | |||
| } | |||
| const onCurrentChange = (current: number) => { | |||
| pagination.value.current = current | |||
| } | |||
| const handleDeleteItem = (product) => { | |||
| ElMessageBox.confirm( | |||
| product ? `确认删除后${product.name}的所有知识库信息将被清空, 且无法恢复` : '', | |||
| '提示', | |||
| { | |||
| type: 'warning' | |||
| } | |||
| ) | |||
| .then(() => { | |||
| handleDeleteKnowledgeBase(product.name) | |||
| fetchAllKnowledgeBases() | |||
| }) | |||
| .catch(() => {}) | |||
| } | |||
| const handleManageProduct = (product) => { | |||
| handleKnowledgeBaseClick(product.name) | |||
| } | |||
| const handleKnowledgeBaseClick = async (kbName: string) => { | |||
| activeKnowledgeBase.value = kbName | |||
| await fetchDocuments(kbName) | |||
| isShowFileListDialog.value = true | |||
| } | |||
| const handleNewKnowledgeBaseClick = () => { | |||
| handleNewKnowledgeBase() // Reset the form | |||
| isShowKnowledgeBaseDialog.value = true // Show the dialog | |||
| } | |||
| const handleRefreshDocuments = async () => { | |||
| await fetchDocuments(activeKnowledgeBase.value) | |||
| await fetchAllKnowledgeBases() | |||
| } | |||
| const handleSaveKnowledgeBaseClick = async () => { | |||
| loading.value = true // 启动 Loading | |||
| try { | |||
| await handleSaveKnowledgeBase() | |||
| isShowKnowledgeBaseDialog.value = false | |||
| await fetchAllKnowledgeBases() | |||
| await nextTick() | |||
| } catch (error) { | |||
| ElMessage.error('创建知识库失败') | |||
| } finally { | |||
| loading.value = false // 关闭 Loading | |||
| } | |||
| } | |||
| const goBack = () => { | |||
| router.back() | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .icon-back { | |||
| margin-right: 8px; | |||
| } | |||
| .flex { | |||
| display: flex; | |||
| } | |||
| .items-center { | |||
| align-items: center; | |||
| } | |||
| .cursor-pointer { | |||
| cursor: pointer; | |||
| } | |||
| .mr-2 { | |||
| margin-right: 0.5rem; | |||
| } | |||
| .kb-layout { | |||
| //height: 100svh; | |||
| display: flex; | |||
| flex-direction: column; | |||
| .el-container { | |||
| height: 100%; | |||
| } | |||
| .el-header { | |||
| background-color: #f5f7fa; | |||
| color: #333; | |||
| } | |||
| .el-main { | |||
| overflow-y: auto; | |||
| } | |||
| } | |||
| .w-full { | |||
| width: 100%; | |||
| } | |||
| .flex { | |||
| display: flex; | |||
| } | |||
| .justify-between { | |||
| justify-content: space-between; | |||
| } | |||
| .mb-4 { | |||
| margin-bottom: 1rem; | |||
| } | |||
| .float-right { | |||
| float: right; | |||
| } | |||
| .mt-4 { | |||
| margin-top: 1rem; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,74 @@ | |||
| <template> | |||
| <el-dialog v-model="dialogVisible" title="新建知识库" width="30%" @close="handleClose"> | |||
| <div | |||
| v-loading="loading" | |||
| :element-loading-text="loadingText" | |||
| element-loading-spinner="el-icon-loading" | |||
| > | |||
| <el-form :model="form" label-width="120px"> | |||
| <el-form-item label="知识库名称"> | |||
| <el-input v-model="form.knowledge_base_name" /> | |||
| </el-form-item> | |||
| <el-form-item label="向量库类型"> | |||
| <el-select v-model="form.vector_store_type"> | |||
| <el-option label="Faiss" value="faiss" /> | |||
| <!-- Add other options if needed --> | |||
| </el-select> | |||
| </el-form-item> | |||
| <el-form-item label="知识库描述"> | |||
| <el-input v-model="form.kb_info" type="textarea" /> | |||
| </el-form-item> | |||
| </el-form> | |||
| </div> | |||
| <template #footer> | |||
| <span class="dialog-footer"> | |||
| <el-button @click="handleClose" :disabled="loading">取消</el-button> | |||
| <el-button type="primary" @click="handleSave" :disabled="isSaveDisabled || loading"> | |||
| 确定 | |||
| </el-button> | |||
| </span> | |||
| </template> | |||
| </el-dialog> | |||
| </template> | |||
| <script setup lang="ts"> | |||
| import { ref, computed } from 'vue' | |||
| defineOptions({ | |||
| name: 'NewKBDialog' | |||
| }) | |||
| const props = defineProps<{ | |||
| visible: boolean | |||
| loading: boolean | |||
| form: { | |||
| knowledge_base_name: string | |||
| vector_store_type: string | |||
| kb_info: string | |||
| } | |||
| }>() | |||
| const emit = defineEmits<{ | |||
| (e: 'update:visible', value: boolean): void | |||
| (e: 'save'): void | |||
| }>() | |||
| const dialogVisible = computed({ | |||
| get: () => props.visible, | |||
| set: (value) => emit('update:visible', value) | |||
| }) | |||
| const isSaveDisabled = computed(() => !props.form.knowledge_base_name.trim()) | |||
| const loadingText = ref('创建知识库中...') | |||
| const handleClose = () => { | |||
| if (!props.loading) { | |||
| emit('update:visible', false) | |||
| } | |||
| } | |||
| const handleSave = () => { | |||
| emit('save') | |||
| } | |||
| </script> | |||
| @@ -0,0 +1,34 @@ | |||
| import { ref } from 'vue' | |||
| import axios from 'axios' | |||
| export interface Tool { | |||
| value: string | |||
| label: string | |||
| } | |||
| export const useToolConfig = () => { | |||
| const availableTools = ref<Tool[]>([]) | |||
| const selectedTools = ref<string[]>([]) | |||
| const fetchAvailableTools = async () => { | |||
| try { | |||
| const response = await axios.get('http://127.0.0.1:7861/api/tools/available_tools') | |||
| if (response.data && response.data.tools) { | |||
| availableTools.value = response.data.tools.map((tool: string) => ({ | |||
| value: tool, | |||
| label: tool | |||
| })) | |||
| } | |||
| } catch (error) { | |||
| console.error('Failed to fetch available tools:', error) | |||
| } | |||
| } | |||
| return { | |||
| availableTools, | |||
| selectedTools, | |||
| fetchAvailableTools | |||
| } | |||
| } | |||
| @@ -0,0 +1,37 @@ | |||
| export interface Agent { | |||
| agent_id?: number; | |||
| agent_name: string; | |||
| agent_abstract: string; | |||
| agent_info: string; | |||
| temperature: number; | |||
| max_tokens: number; | |||
| tool_config: string[]; | |||
| kb_name?: string[]; | |||
| avatar?: string; | |||
| agent_enable?: boolean; | |||
| } | |||
| export interface backendChat { | |||
| content: string; | |||
| role: string; | |||
| } | |||
| export interface HistoryItems { | |||
| id: string; | |||
| text: string; | |||
| timestamp: Date; | |||
| summarized: boolean; | |||
| } | |||
| export interface configManagementFormInterface { | |||
| config_name: string; | |||
| platform: string; | |||
| base_url: string; | |||
| api_key: string; | |||
| llm_model: { | |||
| model: string | |||
| callbacks: boolean | |||
| max_tokens: number | |||
| temperature: number | |||
| }; | |||
| } | |||
| @@ -0,0 +1,32 @@ | |||
| import type { DeepChat } from 'deep-chat' | |||
| export function extractFirstJSON(text: string): any | null { | |||
| const jsonRegex = /\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\}/ | |||
| const match = text.match(jsonRegex) | |||
| if (match) { | |||
| try { | |||
| return JSON.parse(match[0]) | |||
| } catch (error) { | |||
| console.warn('Failed to parse JSON:', error) | |||
| } | |||
| } | |||
| return null | |||
| } | |||
| export const clearMessageElement = (deepChatRef: DeepChat | null) => { | |||
| if (!deepChatRef) { | |||
| console.log('deepChatRef is not valid') | |||
| return | |||
| } | |||
| // 获取所有 class="outer-message-container" 的元素 | |||
| const messagesElements = deepChatRef.shadowRoot!.querySelectorAll('.outer-message-container') | |||
| console.log('messagesElements:', messagesElements) | |||
| // 遍历并删除每个元素 | |||
| messagesElements.forEach((element) => { | |||
| console.log('Removing element:', element) | |||
| element.remove() | |||
| }) | |||
| } | |||
| @@ -0,0 +1,4 @@ | |||
| { | |||
| "files": [], | |||
| "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }] | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| { | |||
| "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", | |||
| "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"], | |||
| "compilerOptions": { | |||
| "composite": true, | |||
| "types": ["electron-vite/node"] | |||
| } | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| { | |||
| "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", | |||
| "include": [ | |||
| "src/renderer/src/env.d.ts", | |||
| "src/renderer/src/**/*", | |||
| "src/renderer/src/**/*.vue", | |||
| "src/preload/*.d.ts" | |||
| ], | |||
| "compilerOptions": { | |||
| "composite": true, | |||
| "baseUrl": ".", | |||
| "paths": { | |||
| "@renderer/*": [ | |||
| "src/renderer/src/*" | |||
| ] | |||
| } | |||
| } | |||
| } | |||