Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7e18c97a2f | ||
|
e190e1484e | ||
|
e3202ade12 | ||
|
ec35d42936 | ||
|
da607e31b2 | ||
|
4a0c3c6231 | ||
|
2d507c0bcf | ||
|
7e42e4ccd2 | ||
|
bcada6ead5 | ||
|
d08db8a76a | ||
|
94cf2be101 | ||
|
00bf070dbd | ||
|
77b42aaf47 | ||
|
ae053d6e27 | ||
|
0710570811 | ||
|
38e34219d3 | ||
|
693f6a90ad | ||
|
d847b550d6 | ||
|
5bba31db3e | ||
|
548c402316 | ||
|
c96c016feb | ||
|
728fdaffd6 | ||
|
fa2b05b658 | ||
|
7b96e12858 | ||
|
62f937c1ca |
3
.gitignore
vendored
3
.gitignore
vendored
@ -178,3 +178,6 @@ data/
|
||||
|
||||
# Workspace
|
||||
workspace/
|
||||
|
||||
# Private Config
|
||||
config/config.toml
|
||||
|
50
app.py
50
app.py
@ -1,13 +1,22 @@
|
||||
import asyncio
|
||||
import os
|
||||
import threading
|
||||
import tomllib
|
||||
import uuid
|
||||
import webbrowser
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from json import dumps
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import Body, FastAPI, HTTPException, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
|
||||
from fastapi.responses import (
|
||||
FileResponse,
|
||||
HTMLResponse,
|
||||
JSONResponse,
|
||||
StreamingResponse,
|
||||
)
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pydantic import BaseModel
|
||||
@ -90,6 +99,14 @@ async def index(request: Request):
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
|
||||
@app.get("/download")
|
||||
async def download_file(file_path: str):
|
||||
if not os.path.exists(file_path):
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
return FileResponse(file_path, filename=os.path.basename(file_path))
|
||||
|
||||
|
||||
@app.post("/tasks")
|
||||
async def create_task(prompt: str = Body(..., embed=True)):
|
||||
task = task_manager.create_task(prompt)
|
||||
@ -107,7 +124,6 @@ async def run_task(task_id: str, prompt: str):
|
||||
agent = Manus(
|
||||
name="Manus",
|
||||
description="A versatile agent that can solve various tasks using multiple tools",
|
||||
max_steps=30,
|
||||
)
|
||||
|
||||
async def on_think(thought):
|
||||
@ -135,7 +151,7 @@ async def run_task(task_id: str, prompt: str):
|
||||
async def __call__(self, message):
|
||||
import re
|
||||
|
||||
# 提取 - 后面的内容
|
||||
# Extract - Subsequent Content
|
||||
cleaned_message = re.sub(r"^.*? - ", "", message)
|
||||
|
||||
event_type = "log"
|
||||
@ -244,12 +260,32 @@ async def generic_exception_handler(request: Request, exc: Exception):
|
||||
)
|
||||
|
||||
|
||||
def open_local_browser():
|
||||
webbrowser.open_new_tab("http://localhost:5172")
|
||||
def open_local_browser(config):
|
||||
webbrowser.open_new_tab(f"http://{config['host']}:{config['port']}")
|
||||
|
||||
|
||||
def load_config():
|
||||
try:
|
||||
config_path = Path(__file__).parent / "config" / "config.toml"
|
||||
|
||||
with open(config_path, "rb") as f:
|
||||
config = tomllib.load(f)
|
||||
|
||||
return {"host": config["server"]["host"], "port": config["server"]["port"]}
|
||||
except FileNotFoundError:
|
||||
raise RuntimeError(
|
||||
"Configuration file not found, please check if config/fig.toml exists"
|
||||
)
|
||||
except KeyError as e:
|
||||
raise RuntimeError(
|
||||
f"The configuration file is missing necessary fields: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
threading.Timer(3, open_local_browser).start()
|
||||
uvicorn.run(app, host="localhost", port=5172)
|
||||
config = load_config()
|
||||
open_with_config = partial(open_local_browser, config)
|
||||
threading.Timer(3, open_with_config).start()
|
||||
uvicorn.run(app, host=config["host"], port=config["port"])
|
||||
|
@ -26,6 +26,9 @@ class Manus(ToolCallAgent):
|
||||
system_prompt: str = SYSTEM_PROMPT
|
||||
next_step_prompt: str = NEXT_STEP_PROMPT
|
||||
|
||||
max_observe: int = 2000
|
||||
max_steps: int = 20
|
||||
|
||||
# Add general-purpose tools to the tool collection
|
||||
available_tools: ToolCollection = Field(
|
||||
default_factory=lambda: ToolCollection(
|
||||
|
@ -1,5 +1,5 @@
|
||||
import json
|
||||
from typing import Any, List, Literal
|
||||
from typing import Any, List, Literal, Optional, Union
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
@ -31,6 +31,7 @@ class ToolCallAgent(ReActAgent):
|
||||
tool_calls: List[ToolCall] = Field(default_factory=list)
|
||||
|
||||
max_steps: int = 30
|
||||
max_observe: Optional[Union[int, bool]] = None
|
||||
|
||||
async def think(self) -> bool:
|
||||
"""Process current state and decide next actions using tools"""
|
||||
@ -114,6 +115,9 @@ class ToolCallAgent(ReActAgent):
|
||||
f"🎯 Tool '{command.function.name}' completed its mission! Result: {result}"
|
||||
)
|
||||
|
||||
if self.max_observe:
|
||||
result = result[: self.max_observe]
|
||||
|
||||
# Add tool response to memory
|
||||
tool_msg = Message.tool_message(
|
||||
content=result, tool_call_id=command.id, name=command.function.name
|
||||
|
@ -103,7 +103,12 @@ class BrowserUseTool(BaseTool):
|
||||
async def _ensure_browser_initialized(self) -> BrowserContext:
|
||||
"""Ensure browser and context are initialized."""
|
||||
if self.browser is None:
|
||||
self.browser = BrowserUseBrowser(BrowserConfig(headless=False))
|
||||
# 使用Chrome命令行参数设置窗口大小和位置
|
||||
browser_config = BrowserConfig(
|
||||
headless=False,
|
||||
disable_security=True,
|
||||
)
|
||||
self.browser = BrowserUseBrowser(browser_config)
|
||||
if self.context is None:
|
||||
self.context = await self.browser.new_context()
|
||||
self.dom_service = DomService(await self.context.get_current_page())
|
||||
|
@ -1,7 +1,8 @@
|
||||
from app.tool.base import BaseTool
|
||||
|
||||
|
||||
_TERMINATE_DESCRIPTION = """Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task."""
|
||||
_TERMINATE_DESCRIPTION = """Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task.
|
||||
When you have finished all the tasks, call this tool to end the work."""
|
||||
|
||||
|
||||
class Terminate(BaseTool):
|
||||
|
@ -20,3 +20,8 @@ temperature = 0.0
|
||||
model = "claude-3-5-sonnet"
|
||||
base_url = "https://api.openai.com/v1"
|
||||
api_key = "sk-..."
|
||||
|
||||
# Server configuration
|
||||
[server]
|
||||
host = "localhost"
|
||||
port = 5172
|
||||
|
5
desktop/.gitignore
vendored
Normal file
5
desktop/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
build
|
||||
node_modules
|
||||
frontend/dist
|
||||
frontend/package.json.md5
|
||||
*.log
|
71
desktop/README.md
Normal file
71
desktop/README.md
Normal file
@ -0,0 +1,71 @@
|
||||
# OpenManus-Desktop Project
|
||||
|
||||
## Project Overview
|
||||
|
||||
OpenManus-Desktop is a desktop application built on the Wails framework, combining Go backend and Vue3 frontend technologies. The project utilizes Vite as the frontend build tool, offering an efficient development experience.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- Backend: Go
|
||||
- Frontend: Vue3 + Vite
|
||||
- UI Framework: Element Plus
|
||||
- State Management: Pinia
|
||||
- Routing: Vue Router
|
||||
- Build Tool: Wails
|
||||
|
||||
## Development Environment Requirements
|
||||
|
||||
- Go 1.18+
|
||||
- Node.js 20+
|
||||
- Wails CLI v2+
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Install Development Environment
|
||||
|
||||
#### 1.1. Install Golang Environment
|
||||
|
||||
Golang environment : https://go.dev/dl/
|
||||
|
||||
#### 1.2. Install Wails Client
|
||||
|
||||
wails: https://wails.io/
|
||||
|
||||
// For users in mainland China, use a proxy
|
||||
go env -w GOPROXY=https://goproxy.cn
|
||||
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
|
||||
Run the following command to check if the Wails client is installed successfully:
|
||||
|
||||
wails doctor
|
||||
|
||||
#### 1.3. Install Node.js Environment
|
||||
|
||||
nodejs: https://nodejs.org/en
|
||||
|
||||
### 2. Install Project Dependencies
|
||||
|
||||
cd .\desktop\frontend
|
||||
npm install
|
||||
|
||||
### 3. Run the Project
|
||||
|
||||
To run the project:
|
||||
|
||||
cd .\desktop
|
||||
wails dev
|
||||
|
||||
To start the backend service:
|
||||
|
||||
After configuring the config/config.toml file, execute the following command to start the server:
|
||||
|
||||
cd .. (Project root directory)
|
||||
python app.py
|
||||
|
||||
### 4. Package the Project
|
||||
|
||||
To build the application:
|
||||
|
||||
wails build
|
||||
|
||||
The built application will be located in the project’s dist directory.
|
71
desktop/README_zh.md
Normal file
71
desktop/README_zh.md
Normal file
@ -0,0 +1,71 @@
|
||||
# OpenManus-Desktop 项目
|
||||
|
||||
## 项目简介
|
||||
|
||||
OpenManus-Desktop 是一个基于Wails框架构建的桌面应用程序,结合了Go后端和Vue3前端技术栈。项目采用Vite作为前端构建工具,提供了高效的开发体验。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 后端: Go
|
||||
- 前端: Vue3 + Vite
|
||||
- UI 框架: Element Plus
|
||||
- 状态管理: Pinia
|
||||
- 路由: Vue Router
|
||||
- 构建工具: Wails
|
||||
|
||||
## 开发环境要求
|
||||
|
||||
- Go 1.18+
|
||||
- Node.js 20+
|
||||
- Wails CLI v2+
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 安装开发环境
|
||||
|
||||
#### 1.1. 安装Go语言环境
|
||||
|
||||
Go环境下载: https://go.dev/dl/
|
||||
|
||||
#### 1.2. 安装wails客户端
|
||||
|
||||
wails官网: https://wails.io/
|
||||
|
||||
// 中国大陆使用代理
|
||||
go env -w GOPROXY=https://goproxy.cn
|
||||
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
|
||||
执行以下命名令检查wails客户端安装是否成功:
|
||||
|
||||
wails doctor
|
||||
|
||||
#### 1.3. 安装Node.js环境
|
||||
|
||||
nodejs官网安装: https://nodejs.org/en
|
||||
|
||||
### 2. 安装项目依赖
|
||||
|
||||
cd .\desktop\frontend
|
||||
npm install
|
||||
|
||||
### 3. 运行项目
|
||||
|
||||
运行项目:
|
||||
|
||||
cd .\desktop
|
||||
wails dev
|
||||
|
||||
启动服务端:
|
||||
|
||||
配置好config/config.toml文件后, 执行以下命令启动服务端:
|
||||
|
||||
cd .. (项目根目录)
|
||||
python app.py
|
||||
|
||||
### 4. 打包项目
|
||||
|
||||
构建应用:
|
||||
|
||||
wails build
|
||||
|
||||
构建好的应用在项目dist目录下
|
42
desktop/app.go
Normal file
42
desktop/app.go
Normal file
@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"OpenManus/src/utils"
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Result string `json:"result"`
|
||||
Error string `json:"error"`
|
||||
Callbackid string `json:"callbackid"`
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
// startup is called when the app starts. The context is saved
|
||||
// so we can call the runtime methods
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
func (a *App) Greet(name string) string {
|
||||
return fmt.Sprintf("Hello %s, It's show time!", name)
|
||||
}
|
||||
|
||||
// ReadAll reads file content
|
||||
func (a *App) ReadAll(filePath string) string {
|
||||
// Read the file content, resulting in a JSON string containing file content and callback ID
|
||||
data := string(utils.ReadAll(filePath))
|
||||
utils.Log("ReadAll data: ", data)
|
||||
return data
|
||||
}
|
8
desktop/frontend/README.md
Normal file
8
desktop/frontend/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs,
|
||||
check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
12
desktop/frontend/index.html
Normal file
12
desktop/frontend/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>OpenManus</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="./src/main.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
3496
desktop/frontend/package-lock.json
generated
Normal file
3496
desktop/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
desktop/frontend/package.json
Normal file
31
desktop/frontend/package.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.8.3",
|
||||
"element-plus": "^2.9.2",
|
||||
"pinia": "^3.0.1",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"qs": "^6.14.0",
|
||||
"sql-formatter": "^15.4.9",
|
||||
"vue": "^3.2.37",
|
||||
"vue-i18n": "^11.0.0-rc.1",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/qs": "^6.9.18",
|
||||
"@vitejs/plugin-vue": "^3.0.3",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"sass": "^1.83.1",
|
||||
"unplugin-auto-import": "^0.19.0",
|
||||
"unplugin-vue-components": "^0.28.0",
|
||||
"vite": "^3.0.7"
|
||||
}
|
||||
}
|
2069
desktop/frontend/pnpm-lock.yaml
generated
Normal file
2069
desktop/frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
desktop/frontend/src/App.vue
Normal file
53
desktop/frontend/src/App.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<!-- Global Configuration -->
|
||||
<el-config-provider :size="size" :z-index="zIndex" :locale="locale" :button="config" :message="config"
|
||||
:value-on-clear="null" :empty-values="[undefined, null]">
|
||||
<RouterView />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import en from 'element-plus/es/locale/lang/en'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
/** Dark Theme */
|
||||
import { useDark, useStorage } from '@vueuse/core'
|
||||
|
||||
const size = 'default'
|
||||
const zIndex = 2000
|
||||
|
||||
const localConfig = localStorage.getItem('config') ? JSON.parse(localStorage.getItem('config')) : {}
|
||||
|
||||
const localeStr = localConfig.selectedLang ? localConfig.selectedLang.code : 'en'
|
||||
const locale = localeStr == 'en' ? en : zhCn
|
||||
|
||||
const isDark = useDark()
|
||||
// Store user preferences
|
||||
const userPrefersDark = ref(null)
|
||||
onMounted(() => {
|
||||
|
||||
// Use useStorage hook to sync isDark and local storage
|
||||
useStorage(
|
||||
'user-prefers-dark',
|
||||
userPrefersDark,
|
||||
localStorage,
|
||||
isDark.value ? 'dark' : 'light'
|
||||
)
|
||||
})
|
||||
|
||||
// Watch isDark changes and update local storage
|
||||
watch(isDark, (newValue) => {
|
||||
userPrefersDark.value = newValue ? 'dark' : 'light'
|
||||
})
|
||||
|
||||
|
||||
/* Global Configuration */
|
||||
const config = reactive({
|
||||
// Button - Automatically insert space between Chinese characters
|
||||
autoInsertSpace: true,
|
||||
// Message - Maximum number of messages that can be displayed simultaneously
|
||||
max: 3,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
1768
desktop/frontend/src/assets/css/main.css
Normal file
1768
desktop/frontend/src/assets/css/main.css
Normal file
File diff suppressed because it is too large
Load Diff
BIN
desktop/frontend/src/assets/img/list-add.png
Normal file
BIN
desktop/frontend/src/assets/img/list-add.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
BIN
desktop/frontend/src/assets/img/list-delete.png
Normal file
BIN
desktop/frontend/src/assets/img/list-delete.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
desktop/frontend/src/assets/img/logo-sm.png
Normal file
BIN
desktop/frontend/src/assets/img/logo-sm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
BIN
desktop/frontend/src/assets/img/logo-universal.png
Normal file
BIN
desktop/frontend/src/assets/img/logo-universal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 136 KiB |
BIN
desktop/frontend/src/assets/img/user.png
Normal file
BIN
desktop/frontend/src/assets/img/user.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 989 B |
210
desktop/frontend/src/assets/js/files.js
Normal file
210
desktop/frontend/src/assets/js/files.js
Normal file
@ -0,0 +1,210 @@
|
||||
import { ReadAll } from '@/../wailsjs/go/main/App.js'
|
||||
import utils from '@/assets/js/utils'
|
||||
|
||||
// Temporary cache for file information
|
||||
function cache(fileObj, $event) {
|
||||
console.log('Caching fileObj start:', fileObj, $event.target, $event.dataTransfer)
|
||||
console.log('typeof fileObj:', Array.isArray(fileObj))
|
||||
// If fileObj is an array, create a new element and append to the array
|
||||
// event.target.files and event.dataTransfer.files are event properties in JavaScript related to file upload and drag-and-drop.
|
||||
// event.target.files: This property is used with HTML file input elements (<input type="file">),
|
||||
// When the user selects a file and triggers the change event, event.target.files can be used to get the list of files selected by the user.
|
||||
// event.dataTransfer.files: This property is used when the user drags and drops files onto an element,
|
||||
// event.dataTransfer.files can be used to get the list of dropped files.
|
||||
console.log('$event:', $event, $event.type)
|
||||
let files
|
||||
if ($event.type == 'change') {
|
||||
files = $event.target.files
|
||||
} else if ($event.type == 'drop') {
|
||||
files = $event.dataTransfer.files
|
||||
} else {
|
||||
console.error("Unrecognized event type")
|
||||
return
|
||||
}
|
||||
const file = files[0]
|
||||
console.log("Selected file:", file)
|
||||
const fileInfo = Array.isArray(fileObj) ? new Object() : fileObj
|
||||
fileInfo.file = file
|
||||
let URL = window.URL || window.webkitURL
|
||||
fileInfo.fileUrl = URL.createObjectURL(file)
|
||||
const fileType = file.type
|
||||
console.log("File type:", fileType, typeof (fileType))
|
||||
if (utils.notNull(fileType) && fileType.startsWith("image")) {
|
||||
fileInfo.imgUrl = fileInfo.fileUrl
|
||||
}
|
||||
fileInfo.fileName = file.name
|
||||
console.log('Caching fileObj completed:', fileInfo)
|
||||
if (Array.isArray(fileObj)) {
|
||||
// Append to the end of the array after successful operation
|
||||
fileObj.push(fileInfo)
|
||||
}
|
||||
if ($event.type == 'change') {
|
||||
// Solve the problem of selecting the same file not triggering the change event, clean up at the end
|
||||
$event.target.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// Upload file
|
||||
async function upload(fileObj) {
|
||||
console.log("Preparing to upload file...", fileObj, fileObj.file, fileObj.fileId)
|
||||
// Current location handling
|
||||
if (utils.isNull(fileObj.file)) {
|
||||
if (utils.notNull(fileObj.fileId) && fileObj.remark != fileObj.remarkUpd) {
|
||||
let remark = null
|
||||
if (utils.notNull(fileObj.remarkUpd)) {
|
||||
remark = fileObj.remarkUpd
|
||||
}
|
||||
await updRemark(fileObj.fileId, remark)
|
||||
}
|
||||
return
|
||||
}
|
||||
console.log("Starting file upload...", fileObj, fileObj.file, fileObj.fileId)
|
||||
const url = '/common/file/upload'
|
||||
const formData = new FormData()
|
||||
formData.append('file', fileObj.file)
|
||||
if (utils.notNull(fileObj.remark)) {
|
||||
formData.append('remark', fileObj.remark)
|
||||
} else if (utils.notNull(fileObj.remarkUpd)) {
|
||||
formData.append('remark', fileObj.remarkUpd)
|
||||
}
|
||||
const data = await utils.awaitPost(url, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
Object.assign(fileObj, data)
|
||||
console.log("File upload processed successfully", fileObj)
|
||||
return fileObj
|
||||
}
|
||||
|
||||
// Update file remark
|
||||
async function updRemark(fileId, remarkUpd) {
|
||||
const param = {
|
||||
fileId: fileId,
|
||||
remark: remarkUpd
|
||||
}
|
||||
await utils.awaitPost('/common/file/updRemark', param)
|
||||
console.log("File remark updated successfully")
|
||||
}
|
||||
|
||||
// Batch upload files
|
||||
async function uploads(fileObjs) {
|
||||
if (utils.isEmpty(fileObjs)) {
|
||||
return
|
||||
}
|
||||
for (let index in fileObjs) {
|
||||
console.log('Processing file object:', fileObjs, index, fileObjs.length, fileObjs[index])
|
||||
await upload(fileObjs[index])
|
||||
console.log("uploads index:", index, "File upload completed", fileObjs[index])
|
||||
}
|
||||
}
|
||||
|
||||
// Handle file upload (onChange event)
|
||||
function upOnChg(fileObj, $event) {
|
||||
const file = $event.target.files[0] || $event.dataTransfer.files[0]
|
||||
// Current location
|
||||
let URL = window.URL || window.webkitURL
|
||||
// Convert to blob URL
|
||||
fileObj.fileUrl = URL.createObjectURL(file)
|
||||
const url = '/common/file/upload'
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('remark', fileObj.remark)
|
||||
utils.post(url, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}).then((data) => {
|
||||
console.log("File upload result:", data)
|
||||
Object.assign(fileObj, data)
|
||||
fileObj.remarkUpd = data.remark
|
||||
})
|
||||
}
|
||||
|
||||
// Add to component list
|
||||
function add(fileList) {
|
||||
const comp = {
|
||||
index: fileList.length,
|
||||
file: null,
|
||||
fileId: null,
|
||||
fileName: null,
|
||||
fileUrl: null,
|
||||
imgUrl: null,
|
||||
remark: null
|
||||
}
|
||||
fileList.push(comp)
|
||||
}
|
||||
|
||||
// Remove component from list
|
||||
function del(fileObj, index) {
|
||||
console.log("Deleting file object:", fileObj, index)
|
||||
if (Array.isArray(fileObj)) {
|
||||
fileObj.splice(index, 1)
|
||||
} else {
|
||||
utils.clearProps(fileObj)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert between Java and JS file objects
|
||||
function trans(javaFile, jsFile) {
|
||||
if (jsFile == undefined || jsFile == null) {
|
||||
return
|
||||
}
|
||||
// Clear array if present
|
||||
if (jsFile instanceof Array) {
|
||||
jsFile.splice(0, jsFile.length)
|
||||
} else {
|
||||
utils.clearProps(jsFile)
|
||||
}
|
||||
|
||||
if (javaFile == undefined || javaFile == null) {
|
||||
return
|
||||
}
|
||||
// Handle array type
|
||||
if (jsFile instanceof Array) {
|
||||
for (let java of javaFile) {
|
||||
const js = {}
|
||||
java.remarkUpd = java.remark
|
||||
Object.assign(js, java)
|
||||
jsFile.push(js)
|
||||
}
|
||||
} else {
|
||||
// Handle object type
|
||||
console.log("Object type conversion", jsFile instanceof Array)
|
||||
javaFile.remarkUpd = javaFile.remark
|
||||
Object.assign(jsFile, javaFile)
|
||||
}
|
||||
}
|
||||
|
||||
// Collect file IDs from components
|
||||
function fileIds(fileList) {
|
||||
return fileList.map(comp => comp.fileId).join(',')
|
||||
}
|
||||
|
||||
// Read file contents
|
||||
function readAll(filePath) {
|
||||
return ReadAll(filePath)
|
||||
}
|
||||
|
||||
export default {
|
||||
// Cache on onChange
|
||||
cache,
|
||||
// Upload file
|
||||
upload,
|
||||
// Upload files
|
||||
uploads,
|
||||
// Upload file
|
||||
upOnChg,
|
||||
// Upload on onChange
|
||||
upOnChg,
|
||||
// Add to component list
|
||||
add,
|
||||
// Delete component from component list
|
||||
del,
|
||||
// Convert between Java object and js object
|
||||
trans,
|
||||
// Collect fileId from Comps
|
||||
fileIds,
|
||||
// Read file
|
||||
readAll
|
||||
}
|
22
desktop/frontend/src/assets/js/shade.js
Normal file
22
desktop/frontend/src/assets/js/shade.js
Normal file
@ -0,0 +1,22 @@
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
/*
|
||||
* Show page shade
|
||||
*/
|
||||
export const showShade = function (closeCallBack) {
|
||||
const className = 'shade'
|
||||
const containerEl = document.querySelector('.layout-container')
|
||||
const shadeDiv = document.createElement('div')
|
||||
shadeDiv.setAttribute('class', 'layout-shade ' + className)
|
||||
containerEl.appendChild(shadeDiv)
|
||||
useEventListener(shadeDiv, 'click', () => closeShade(closeCallBack))
|
||||
}
|
||||
|
||||
/*
|
||||
* Hide page shade
|
||||
*/
|
||||
export const closeShade = function (closeCallBack = () => { }) {
|
||||
const shadeEl = document.querySelector('.layout-shade')
|
||||
shadeEl && shadeEl.remove()
|
||||
closeCallBack()
|
||||
}
|
690
desktop/frontend/src/assets/js/utils.js
Normal file
690
desktop/frontend/src/assets/js/utils.js
Normal file
@ -0,0 +1,690 @@
|
||||
import { Greet } from '@/../wailsjs/go/main/App.js'
|
||||
import axios from "axios"
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
/** axios start */
|
||||
// Create a new axios instance
|
||||
const $axios = axios.create({
|
||||
baseURL: "api",
|
||||
timeout: 12000
|
||||
})
|
||||
|
||||
// Request interceptors
|
||||
$axios.interceptors.request.use(
|
||||
(config) => {
|
||||
config.headers["token"] = ''
|
||||
if (config.method == "post" || config.method == "put") {
|
||||
delNullProperty(config.data)
|
||||
fomateDateProperty(config.data)
|
||||
} else if (config.method == "get" || config.method == "delete") {
|
||||
delNullProperty(config.params)
|
||||
fomateDateProperty(config.params)
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// Response interceptors
|
||||
$axios.interceptors.response.use(
|
||||
(response) => {
|
||||
// console.log("response:", response)
|
||||
if (response.status == 200) {
|
||||
return response.data
|
||||
} else {
|
||||
pop("Exception occurred in response:" + response.status)
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.log("error:" + JSON.stringify(error))
|
||||
if (error.response == undefined || error.response == null) {
|
||||
pop("Unknown request error!")
|
||||
pop("Unknown request error!")
|
||||
} else if (error.response.status == 500) {
|
||||
pop("Unable to communicate with backend, please retry later!")
|
||||
} else {
|
||||
pop("Request error:" + error)
|
||||
pop("Request error:" + error)
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
function get(url, param) {
|
||||
return $axios.get(url, { params: param })
|
||||
}
|
||||
|
||||
async function awaitGet(url, param) {
|
||||
return await $axios.get(url, { params: param })
|
||||
}
|
||||
|
||||
function post(url, param) {
|
||||
return $axios.post(url, param)
|
||||
}
|
||||
|
||||
async function awaitPost(url, param) {
|
||||
return await $axios.post(url, param)
|
||||
}
|
||||
|
||||
function del(url, param) {
|
||||
return $axios.delete(url, { params: param })
|
||||
}
|
||||
|
||||
async function awaitDel(url, param) {
|
||||
return await $axios.delete(url, { params: param })
|
||||
}
|
||||
|
||||
/**
|
||||
* demo call Go interfaces
|
||||
*/
|
||||
function greet(name) {
|
||||
return Greet(name).then(resp => {
|
||||
console.log("greet resp:", resp)
|
||||
return resp
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if object is null
|
||||
*/
|
||||
function isNull(obj) {
|
||||
return obj == undefined || obj == null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if object is not null
|
||||
*/
|
||||
function notNull(obj) {
|
||||
return obj != undefined && obj != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if string is blank
|
||||
*/
|
||||
function isBlank(str) {
|
||||
return str == undefined || str == null || /^s*$/.test(str)
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify a non-empty string
|
||||
*/
|
||||
function notBlank(str) {
|
||||
return !isBlank(str)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if array is empty
|
||||
*/
|
||||
function isEmpty(arr) {
|
||||
return arr == undefined || arr == null || (arr instanceof Array && arr.length == 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if array is not empty
|
||||
*/
|
||||
function notEmpty(arr) {
|
||||
return arr != undefined && arr != null && arr instanceof Array && arr.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if object is true
|
||||
*/
|
||||
function isTrue(obj) {
|
||||
return obj == true || obj == 'true'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if object is false
|
||||
*/
|
||||
function isFalse(obj) {
|
||||
return !isTrue(obj)
|
||||
}
|
||||
/**
|
||||
* Get count of a specific character in a string
|
||||
* @param {string} str - String to search
|
||||
* @param {string} char - Character to find
|
||||
* @returns {number} - Occurrence count
|
||||
*/
|
||||
function getCharCount(str, char) {
|
||||
// g=match globally
|
||||
var regex = new RegExp(char, 'g')
|
||||
// Search for all occurrences of the character in the string
|
||||
var result = str.match(regex)
|
||||
var count = !result ? 0 : result.length
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date with specified pattern
|
||||
* @param {Date|string} date - Date object or date string
|
||||
* @param {string} format - Target format pattern; by default, `yyyy-MM-dd HH:mm:ss`
|
||||
* @returns {string} - Formatted date string
|
||||
*/
|
||||
function dateFormat(date, format) {
|
||||
if (date == undefined || date == null || date == '') {
|
||||
return date
|
||||
}
|
||||
if (format == undefined || format == null
|
||||
|| format == '' || format == 0
|
||||
|| format == "datetime" || format == 'date_time'
|
||||
|| format == 'DATE_TIME' || format == 'DATETIME') {
|
||||
format = "yyyy-MM-dd HH:mm:ss"
|
||||
} else if (format == 'date' || format == 'DATE' || format == 1) {
|
||||
format = "yyyy-MM-dd"
|
||||
}
|
||||
date = new Date(date)
|
||||
const Y = date.getFullYear() + '',
|
||||
M = date.getMonth() + 1,
|
||||
D = date.getDate(),
|
||||
H = date.getHours(),
|
||||
m = date.getMinutes(),
|
||||
s = date.getSeconds()
|
||||
return format.replace(/YYYY|yyyy/g, Y)
|
||||
.replace(/YY|yy/g, Y.substring(2, 2))
|
||||
.replace(/MM/g, (M < 10 ? '0' : '') + M)
|
||||
.replace(/dd/g, (D < 10 ? '0' : '') + D)
|
||||
.replace(/HH|hh/g, (H < 10 ? '0' : '') + H)
|
||||
.replace(/mm/g, (m < 10 ? '0' : '') + m)
|
||||
.replace(/ss/g, (s < 10 ? '0' : '') + s)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively format Date properties in objects/arrays
|
||||
* @param {Object} obj - Target object to process
|
||||
*/
|
||||
function fomateDateProperty(obj) {
|
||||
for (let i in obj) {
|
||||
// Iterate through all properties of the object
|
||||
if (obj[i] == null) {
|
||||
continue
|
||||
} else if (obj[i] instanceof Date) {
|
||||
// Format as `yyyy-MM-dd HH:mm:ss`
|
||||
obj[i] = dateFormat(obj[i])
|
||||
} else if (obj[i].constructor === Object) {
|
||||
// Recursively format nested objects
|
||||
if (Object.keys(obj[i]).length > 0) {
|
||||
// Delete empty properties
|
||||
fomateDateProperty(obj[i])
|
||||
}
|
||||
} else if (obj[i].constructor === Array) {
|
||||
// Recursively clean nested arrays
|
||||
if (obj[i].length > 0) {
|
||||
for (let j = 0; j < obj[i].length; j++) {
|
||||
// Iterate through all array items
|
||||
fomateDateProperty(obj[i][j])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove null/empty properties recursively
|
||||
* @param {Object} obj - Target object to clean
|
||||
*/
|
||||
function delNullProperty(obj) {
|
||||
for (let i in obj) {
|
||||
// Iterate through all properties of the object
|
||||
if (obj[i] === undefined || obj[i] === null || obj[i] === "") {
|
||||
// Delete general null/empty properties
|
||||
delete obj[i]
|
||||
} else if (obj[i].constructor === Object) {
|
||||
// Recursively clean nested objects
|
||||
if (Object.keys(obj[i]).length === 0) delete obj[i]
|
||||
// Delete empty properties
|
||||
delNullProperty(obj[i])
|
||||
} else if (obj[i].constructor === Array) {
|
||||
// Recursively clean arrays
|
||||
if (obj[i].length === 0) {
|
||||
// Delete empty arrays
|
||||
delete obj[i]
|
||||
} else {
|
||||
for (let index = 0; index < obj[i].length; index++) {
|
||||
// Iterate through all array items
|
||||
if (obj[i][index] === undefined || obj[i][index] === null || obj[i][index] === "" || JSON.stringify(obj[i][index]) === "{}") {
|
||||
obj[i].splice(index, 1)
|
||||
// Delete null/empty array items
|
||||
index--
|
||||
// Do decrement to avoid skipping next item (index is now pointing to the next item)
|
||||
}
|
||||
if (obj[i].constructor === Object) {
|
||||
// Recursively clean nested objects in array items
|
||||
delNullProperty(obj[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display message notification
|
||||
* @param {string} msg - Message content
|
||||
* @param {string} type - Message type (success/warning/error/etc)
|
||||
*/
|
||||
function pop(msg, type) {
|
||||
ElMessage({ message: msg, type: type })
|
||||
}
|
||||
|
||||
/**
|
||||
* Show default message when no data available
|
||||
* @param {*} data - Data to check
|
||||
*/
|
||||
function popNoData(data) {
|
||||
if (data == undefined || data == null || (data instanceof Array && data.length == 0)) {
|
||||
ElMessage("No data available!")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current datetime as formatted string
|
||||
* @returns {string} Current datetime in yyyy-MM-dd HH:mm format
|
||||
*/
|
||||
function nowDatetimeStr() {
|
||||
const date = new Date()
|
||||
const datetimeStr = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
return datetimeStr
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination structure builder
|
||||
* @param {Object} source - Source pagination data
|
||||
* @param {Object} target - Target pagination object
|
||||
*/
|
||||
function buildPage(source, target) {
|
||||
target.pageNum = source.pageNum
|
||||
target.pageSize = source.pageSize
|
||||
target.total = source.total
|
||||
target.pages = source.pages
|
||||
copyArray(source.list, target.list)
|
||||
}
|
||||
/**
|
||||
* Clear array contents
|
||||
* @param {Array} arr - Array to clear
|
||||
*/
|
||||
function clearArray(arr) {
|
||||
if (arr == undefined || arr == null || arr.length == 0) {
|
||||
return
|
||||
}
|
||||
arr.splice(0, arr.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset object properties to null
|
||||
* @param {Object} obj - Target object
|
||||
*/
|
||||
function clearProps(obj) {
|
||||
if (obj == undefined || obj == null) {
|
||||
return
|
||||
}
|
||||
for (let i in obj) {
|
||||
obj[i] = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy properties between objects
|
||||
* @param {Object} source - Source object
|
||||
* @param {Object} target - Target object
|
||||
*/
|
||||
function copyProps(source, target = {}) {
|
||||
if (target == undefined || target == null) {
|
||||
target = {}
|
||||
}
|
||||
if (source == undefined || source == null) {
|
||||
source = new Object()
|
||||
}
|
||||
for (let i in target) {
|
||||
target[i] = (source[i] != undefined ? source[i] : null)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Clone array contents
|
||||
* @param {Array} source - Source array
|
||||
* @param {Array} target - Target array
|
||||
*/
|
||||
function copyArray(source, target) {
|
||||
if (target == undefined || target == null) {
|
||||
return
|
||||
}
|
||||
// Clear the array first
|
||||
if (target.length > 0) {
|
||||
target.splice(0, target.length)
|
||||
/* while (target.length > 0) {
|
||||
target.pop()
|
||||
} */
|
||||
}
|
||||
if (source == undefined || source == null) {
|
||||
return
|
||||
}
|
||||
for (let i of source) {
|
||||
target.push(i)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find changed properties between objects
|
||||
* @param {Object} origin - Original object
|
||||
* @param {Object} target - Modified object
|
||||
* @returns {Object} Changed properties
|
||||
*/
|
||||
function dfProps(origin, target) {
|
||||
if (origin == undefined || origin == null || target == undefined || target == null) {
|
||||
return target
|
||||
}
|
||||
var dfObj = {}
|
||||
for (let i in target) {
|
||||
if (target[i] != null && target[i] != origin[i]) {
|
||||
dfObj[i] = target[i]
|
||||
}
|
||||
}
|
||||
return dfObj
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for property differences
|
||||
* @param {Object} origin - Original object
|
||||
* @param {Object} target - Modified object
|
||||
* @returns {boolean} True if differences exist
|
||||
*/
|
||||
function hasDfProps(origin, target) {
|
||||
const df = dfProps(origin, target)
|
||||
for (let i in df) {
|
||||
if (df[i] != null) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all object properties are null
|
||||
* @param {Object} target - Object to check
|
||||
* @returns {boolean} True if all properties are null
|
||||
*/
|
||||
function isAllPropsNull(target) {
|
||||
if (target == undefined || target == null) {
|
||||
return true
|
||||
}
|
||||
for (let i in target) {
|
||||
if (target[i] != null) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function colorByLabel(label) {
|
||||
if ('ADD' == label) {
|
||||
return 'bg-success'
|
||||
}
|
||||
if ('UPD' == label) {
|
||||
return 'bg-primary'
|
||||
}
|
||||
if ('DEL' == label) {
|
||||
return 'bg-danger'
|
||||
}
|
||||
if ('step' == label) {
|
||||
return 'bg-primary'
|
||||
}
|
||||
if ('log' == label) {
|
||||
return 'bg-success'
|
||||
}
|
||||
if ('tool' == label) {
|
||||
return 'bg-primary'
|
||||
}
|
||||
if ('think' == label) {
|
||||
return 'bg-danger'
|
||||
}
|
||||
if ('run' == label) {
|
||||
return 'bg-success'
|
||||
}
|
||||
if ('message' == label) {
|
||||
return 'bg-success'
|
||||
}
|
||||
if ('act' == label) {
|
||||
return 'bg-danger'
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function descByLabel(label) {
|
||||
if ('ADD' == label) {
|
||||
return 'Add'
|
||||
}
|
||||
if ('UPD' == label) {
|
||||
return 'Update'
|
||||
}
|
||||
if ('DEL' == label) {
|
||||
return 'Delete'
|
||||
}
|
||||
return label
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry calls
|
||||
* @param {Function} method - Method to call
|
||||
* @param {any} params - Method parameters that are passed to the method
|
||||
*/
|
||||
function retry(method) {
|
||||
const params = []
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
params.push(arguments[i])
|
||||
}
|
||||
setTimeout(() => {
|
||||
method(params)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve label from options
|
||||
* @param {string|number} keyOrVal - Key or value to resolve
|
||||
* @param {Array} opts - Options array
|
||||
* @returns {string} Resolved label if found, or original keyOrVal if not found
|
||||
*/
|
||||
function resolveLabelFromOpts(keyOrVal, opts) {
|
||||
if (isEmpty(opts)) {
|
||||
return keyOrVal
|
||||
}
|
||||
for (let opt of opts) {
|
||||
if (opt.key == keyOrVal || opt.value == keyOrVal) {
|
||||
return opt.label
|
||||
}
|
||||
}
|
||||
return keyOrVal
|
||||
}
|
||||
|
||||
/**
|
||||
* Underscored string to camel case string
|
||||
* @param {String} underscore Underscored string
|
||||
* @returns Camel case string
|
||||
*/
|
||||
function underScoreToCamelCase(underscore) {
|
||||
if (isNull(underscore) || !underscore.includes('_')) {
|
||||
return underscore
|
||||
}
|
||||
const words = underscore.split('_')
|
||||
for (let i = 1; i < words.length; i++) {
|
||||
if (words[i] == "") {
|
||||
words[i] = ""
|
||||
continue
|
||||
}
|
||||
words[i] = words[i].substring(0, 1).toUpperCase() + words[i].substring(1, words[i].length)
|
||||
}
|
||||
return words.join("")
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce a function call
|
||||
* @param {Function} func Function to debounce
|
||||
* @param {Number} delay Delay in milliseconds
|
||||
* @returns Debounced function
|
||||
*/
|
||||
function debounce(func, delay) {
|
||||
let timer
|
||||
return function () {
|
||||
const context = this
|
||||
const args = arguments
|
||||
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
func.apply(context, args)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string to lines
|
||||
*/
|
||||
function stringToLines(str) {
|
||||
if (str == undefined || str == null) {
|
||||
return []
|
||||
}
|
||||
return str.split('\n')
|
||||
}
|
||||
export default {
|
||||
/**
|
||||
* Synchronous GET HTTP request
|
||||
*/
|
||||
get,
|
||||
|
||||
/**
|
||||
* Asynchronous GET HTTP request (async/await)
|
||||
*/
|
||||
awaitGet,
|
||||
|
||||
/**
|
||||
* Synchronous POST HTTP request
|
||||
*/
|
||||
post,
|
||||
|
||||
/**
|
||||
* Asynchronous POST HTTP request (async/await)
|
||||
*/
|
||||
awaitPost,
|
||||
|
||||
/**
|
||||
* Synchronous DELETE HTTP request
|
||||
*/
|
||||
del,
|
||||
|
||||
/**
|
||||
* Asynchronous DELETE HTTP request (async/await)
|
||||
*/
|
||||
awaitDel,
|
||||
|
||||
/**
|
||||
* Checks if a value is null/undefined
|
||||
*/
|
||||
isNull,
|
||||
|
||||
/**
|
||||
* Verifies a value is not null/undefined
|
||||
*/
|
||||
notNull,
|
||||
|
||||
isBlank,
|
||||
|
||||
notBlank,
|
||||
|
||||
/**
|
||||
* Checks if an array is empty
|
||||
*/
|
||||
isEmpty,
|
||||
|
||||
/**
|
||||
* Verifies an array contains elements
|
||||
*/
|
||||
notEmpty,
|
||||
|
||||
isTrue,
|
||||
|
||||
isFalse,
|
||||
|
||||
getCharCount,
|
||||
|
||||
/**
|
||||
* Displays a toast notification
|
||||
*/
|
||||
pop,
|
||||
|
||||
/**
|
||||
* Shows "No data" notification for empty datasets
|
||||
*/
|
||||
popNoData,
|
||||
|
||||
/**
|
||||
* Removes null/undefined properties from an object
|
||||
*/
|
||||
delNullProperty,
|
||||
|
||||
/**
|
||||
* Gets current datetime as formatted string (YYYY-MM-DD HH:mm:ss)
|
||||
*/
|
||||
nowDatetimeStr,
|
||||
|
||||
/**
|
||||
* Constructs pagination parameters
|
||||
*/
|
||||
buildPage,
|
||||
|
||||
/**
|
||||
* Clears all elements from an array
|
||||
*/
|
||||
clearArray,
|
||||
|
||||
/**
|
||||
* Resets object properties to null/undefined
|
||||
*/
|
||||
clearProps,
|
||||
|
||||
/**
|
||||
* Copies properties between objects
|
||||
*/
|
||||
copyProps,
|
||||
|
||||
/**
|
||||
* Creates a shallow array copy
|
||||
*/
|
||||
copyArray,
|
||||
|
||||
/**
|
||||
* Formats Date object to string (customizable format)
|
||||
*/
|
||||
dateFormat,
|
||||
|
||||
/**
|
||||
* Formats Date properties in objects to strings
|
||||
*/
|
||||
fomateDateProperty,
|
||||
|
||||
/**
|
||||
* Tracks changed properties between object states
|
||||
*/
|
||||
dfProps,
|
||||
|
||||
hasDfProps,
|
||||
|
||||
isAllPropsNull,
|
||||
|
||||
colorByLabel,
|
||||
|
||||
descByLabel,
|
||||
|
||||
/**
|
||||
* Retries failed operations with attempts
|
||||
*/
|
||||
retry,
|
||||
|
||||
resolveLabelFromOpts,
|
||||
|
||||
underScoreToCamelCase,
|
||||
|
||||
debounce,
|
||||
|
||||
stringToLines,
|
||||
|
||||
}
|
205
desktop/frontend/src/assets/js/verify.js
Normal file
205
desktop/frontend/src/assets/js/verify.js
Normal file
@ -0,0 +1,205 @@
|
||||
import utils from '@/assets/js/utils'
|
||||
|
||||
/** Regex for English letters, numbers, and underscores */
|
||||
const codeReg = /^[A-Za-z0-9_\-\.]+$/
|
||||
|
||||
/** Regex for mobile phone number in China (Mainland) */
|
||||
const mobileReg = /^1[3456789]\d{9}$/
|
||||
|
||||
/** Regex for ID card number in China (Mainland) */
|
||||
const idNoReg = /^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$/
|
||||
|
||||
/** Regex for email */
|
||||
const emailReg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/
|
||||
|
||||
const commonValidator = (rule, value, callback) => {
|
||||
if (utils.isNull(value)) {
|
||||
callback()
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const notBlankValidator = (rule, value, callback) => {
|
||||
if (utils.isBlank(value)) {
|
||||
callback(new Error('Input cannot be blank'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const nameValidator = (rule, value, callback) => {
|
||||
if (utils.isBlank(value)) {
|
||||
callback()
|
||||
} else if (value.length > 50) {
|
||||
callback(new Error('Name too long (max 50 characters)'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const mobileValidator = (rule, value, callback) => {
|
||||
if (utils.isNull(value)) {
|
||||
callback()
|
||||
} else if (!mobileReg.test(value)) {
|
||||
callback(new Error('Invalid mobile number'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const idNoValidator = (rule, value, callback) => {
|
||||
if (utils.isNull(value)) {
|
||||
callback()
|
||||
} else if (!idNoReg.test(value)) {
|
||||
callback(new Error('Invalid ID card number'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const emailValidator = (rule, value, callback) => {
|
||||
if (utils.isNull(value)) {
|
||||
callback()
|
||||
} else if (!emailReg.test(value)) {
|
||||
callback(new Error('Invalid email address'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const codeValidator = (rule, value, callback) => {
|
||||
if (utils.isBlank(value)) {
|
||||
callback()
|
||||
} else if (!codeReg.test(value)) {
|
||||
callback(new Error('Invalid code format'))
|
||||
callback(new Error('Invalid code format'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const intValidator = (rule, value, callback) => {
|
||||
if (utils.isBlank(value)) {
|
||||
callback()
|
||||
} else if (!Number.isInteger(value)) {
|
||||
callback(new Error('Input must be an integer'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
function validator() {
|
||||
console.log("arguments:", arguments)
|
||||
if (arguments.length <= 1) {
|
||||
const type = arguments[0]
|
||||
// Default validation logic, no special characters
|
||||
if (utils.isBlank(type)) {
|
||||
return commonValidator
|
||||
} else if (type == 'notBlank') {
|
||||
return notBlankValidator
|
||||
} else if (type == 'name') {
|
||||
return nameValidator
|
||||
} else if (type == 'mobile') {
|
||||
return mobileValidator
|
||||
} else if (type == 'idNo') {
|
||||
return idNoValidator
|
||||
} else if (type == 'email') {
|
||||
return emailValidator
|
||||
} else if (type == 'code') {
|
||||
return codeValidator
|
||||
} else if (type == 'int') {
|
||||
return intValidator
|
||||
} else {
|
||||
return commonValidator
|
||||
}
|
||||
}
|
||||
// Complex validators
|
||||
const complexValidator = (rule, value, callback) => {
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
const typeStr = arguments[i]
|
||||
if (typeStr == 'notBlank' && utils.isBlank(value)) {
|
||||
callback(new Error('Input cannot be blank'))
|
||||
break
|
||||
} else if (typeStr == 'code' && !codeReg.test(value)) {
|
||||
callback(new Error('Invalid code format'))
|
||||
break
|
||||
} else if (typeStr == 'int' && Number.isInteger(value)) {
|
||||
callback(new Error('Please enter an integer'))
|
||||
break
|
||||
}
|
||||
}
|
||||
// Ensure callback is called at least once
|
||||
callback()
|
||||
}
|
||||
return complexValidator
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
username: (username) => {
|
||||
if (typeof (username) == "undefined" || username == null) {
|
||||
return "Username cannot be blank"
|
||||
}
|
||||
username = username.trim()
|
||||
if (username.length < 4) {
|
||||
return "Username must be at least 4 characters long"
|
||||
}
|
||||
if (username.length > 20) {
|
||||
return "Username must be at most 20 characters long"
|
||||
}
|
||||
const reg = /^[A-Za-z0-9]+$/
|
||||
if (!reg.test(username)) {
|
||||
return "Username must be letters and numbers only"
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
password: (password) => {
|
||||
if (typeof (password) == "undefined" || password == null) {
|
||||
return "Password cannot be blank"
|
||||
}
|
||||
password = password.trim()
|
||||
if (password.length < 4) {
|
||||
return "Password must be at least 4 characters long"
|
||||
}
|
||||
if (password.length > 20) {
|
||||
return "Password must be at most 20 characters long"
|
||||
}
|
||||
const reg = /^[A-Za-z0-9\.\-\_\+]+$/
|
||||
if (!reg.test(password)) {
|
||||
return "Password must be letters, numbers, and special characters (.-_+) only"
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
email: (email) => {
|
||||
if (typeof (email) == "undefined" || email == null) {
|
||||
return "Email cannot be blank"
|
||||
}
|
||||
const reg = /^[A-Za-z0-9._%-]+@([A-Za-z0-9-]+\.)+[A-Za-z]{2,4}$/
|
||||
if (!reg.test(email)) {
|
||||
return "Invalid email address"
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
validCode: (validCode) => {
|
||||
if (typeof (validCode) == "undefined" || validCode == null) {
|
||||
return "Verification code cannot be blank"
|
||||
}
|
||||
validCode = validCode.trim()
|
||||
if (validCode.length != 6) {
|
||||
return "Verification code must be 6 characters long"
|
||||
}
|
||||
const reg = /^[A-Za-z0-9]{6}$/
|
||||
if (!reg.test(validCode)) {
|
||||
return "Invalid verification code format"
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
validator,
|
||||
|
||||
|
||||
}
|
8
desktop/frontend/src/assets/less/dark.css
Normal file
8
desktop/frontend/src/assets/less/dark.css
Normal file
@ -0,0 +1,8 @@
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
--el-fg-color: #1d1e1f;
|
||||
--el-bg-color: #141414;
|
||||
--el-vd-bg-color: rgb(20, 20, 20, 0.8);
|
||||
--el-vd-border: var(--el-border-color);
|
||||
--bg-color-overlay: #1d1e1f;
|
||||
}
|
9
desktop/frontend/src/assets/less/light.css
Normal file
9
desktop/frontend/src/assets/less/light.css
Normal file
@ -0,0 +1,9 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--el-fg-color:#ffffff;
|
||||
--el-bg-color: #f5f5f5;
|
||||
--el-vd-bg-color: rgb(255, 255, 255, 0.9);
|
||||
--el-vd-border: #cccccc;
|
||||
--bg-color-overlay: #f5f5f5;
|
||||
|
||||
}
|
54
desktop/frontend/src/assets/vite.config.js
Normal file
54
desktop/frontend/src/assets/vite.config.js
Normal file
@ -0,0 +1,54 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
terser()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8020',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/\/api/, ''),
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1500,
|
||||
// Split chunks, break large chunks into smaller ones
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules')) {
|
||||
// Let each plugin be packaged into an independent file
|
||||
return id.toString().split('node_modules/')[1].split('/')[0].toString()
|
||||
}
|
||||
// Unit b, merge smaller modules
|
||||
experimentalMinChunkSize: 10 * 1024
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
303
desktop/frontend/src/components/AsideMenu.vue
Normal file
303
desktop/frontend/src/components/AsideMenu.vue
Normal file
@ -0,0 +1,303 @@
|
||||
<template>
|
||||
<el-menu class="el-menu-custom" :default-active="activeMenu()" :collapse="menuCollapse" @open="handleOpen"
|
||||
@close="handleClose">
|
||||
|
||||
<el-menu-item index="M02" @click="routeTo('/task')">
|
||||
<el-icon>
|
||||
<List />
|
||||
</el-icon>
|
||||
<span>{{ getMenuNameByCode('M02') }}</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="M03" @click="routeTo('/history')">
|
||||
<el-icon>
|
||||
<Clock />
|
||||
</el-icon>
|
||||
<span>{{ getMenuNameByCode('M03') }}</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-sub-menu index="M99" v-if="hasMenuPerm('M99')">
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<setting />
|
||||
</el-icon>
|
||||
<span>{{ getMenuNameByCode('M99') }}</span>
|
||||
</template>
|
||||
<el-menu-item v-if="listSubMenu('M99') != null" v-for="secMenu in listSubMenu('M99')" :index="secMenu.index"
|
||||
@click="routeTo(secMenu.href)">
|
||||
{{ getMenuNameByCode(secMenu.index) }}
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ChatDotRound, List, Clock, Setting } from '@element-plus/icons-vue'
|
||||
import { ref, inject, onMounted, reactive, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const utils = inject('utils')
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const config = useConfig()
|
||||
const { menuCollapse } = storeToRefs(config)
|
||||
|
||||
const handleOpen = (key, keyPath) => {
|
||||
// console.log(key, keyPath)
|
||||
}
|
||||
const handleClose = (key, keyPath) => {
|
||||
// console.log(key, keyPath)
|
||||
}
|
||||
|
||||
// Menu List
|
||||
const menuList = [
|
||||
{
|
||||
index: "M02",
|
||||
menuName: "menu.task",
|
||||
href: "/task"
|
||||
},
|
||||
{
|
||||
index: "M03",
|
||||
menuName: "menu.history",
|
||||
href: "/history"
|
||||
},
|
||||
{
|
||||
index: "M99",
|
||||
menuName: "menu.config.settings",
|
||||
href: null,
|
||||
subMenuList: [
|
||||
{
|
||||
index: "M9901",
|
||||
menuName: "menu.config.general",
|
||||
href: "/config/general"
|
||||
},
|
||||
{
|
||||
index: "M9902",
|
||||
menuName: "menu.config.llm",
|
||||
href: "/config/llm"
|
||||
},
|
||||
{
|
||||
index: "M9903",
|
||||
menuName: "menu.config.theme",
|
||||
href: "/config/theme"
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
// Check menu position after refresh
|
||||
// activeMenu()
|
||||
})
|
||||
|
||||
function hasMenuPerm(menuCode) {
|
||||
return menuList.some(menuLv1 => menuLv1.index == menuCode)
|
||||
}
|
||||
|
||||
function listSubMenu(menuCode) {
|
||||
const matchedMenu = menuList.find(menuLv1 => menuLv1.index == menuCode)
|
||||
if (matchedMenu != null) {
|
||||
return matchedMenu.subMenuList
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
watch(() => router.currentRoute.value.path, (newValue, oldValue) => {
|
||||
// console.log('LeftMenu侦听到router.currentRoute.value.path发生更新', newValue, oldValue)
|
||||
// Check menu position after route change
|
||||
activeMenu()
|
||||
})
|
||||
|
||||
// Check activated menu position
|
||||
function activeMenu() {
|
||||
const currRoute = router.currentRoute
|
||||
const path = currRoute.value.path
|
||||
console.log("currRoute path:", path)
|
||||
let index = getIndexByPath(path)
|
||||
console.log("index:", index)
|
||||
if (utils.notNull(index)) {
|
||||
return index
|
||||
}
|
||||
// No match, try to find menu for parent path
|
||||
const lastIndex = path.lastIndexOf('/')
|
||||
if (lastIndex != -1) {
|
||||
const newPath = path.substring(0, lastIndex)
|
||||
console.log("newPath from parent path:", newPath)
|
||||
index = getIndexByPath(newPath)
|
||||
console.log("index from parent path:", index)
|
||||
if (utils.notNull(index)) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return "1"
|
||||
}
|
||||
|
||||
// Query menu index by path
|
||||
function getIndexByPath(path) {
|
||||
for (let fstMenu of menuList) {
|
||||
// console.log(fstMenu.index, fstMenu.href == path)
|
||||
if (fstMenu.href == path) {
|
||||
// console.log("完整1级菜单匹配上")
|
||||
return fstMenu.index
|
||||
}
|
||||
const secMenuList = fstMenu.subMenuList
|
||||
if (utils.notEmpty(secMenuList)) {
|
||||
for (let secMenu of secMenuList) {
|
||||
// console.log(secMenu.index, secMenu.href == path)
|
||||
if (secMenu.href == path) {
|
||||
return secMenu.index
|
||||
}
|
||||
const thdMenuList = secMenu.subMenuList
|
||||
if (utils.notEmpty(thdMenuList)) {
|
||||
for (let thdMenu of thdMenuList) {
|
||||
if (thdMenu.href == path) {
|
||||
return thdMenu.index
|
||||
}
|
||||
}
|
||||
// If no third-level menu path matches, use the 'to' from the route configuration to find a match
|
||||
for (let thdMenu of thdMenuList) {
|
||||
const nodeList = routeMap.get(path)
|
||||
if (utils.isEmpty(nodeList)) {
|
||||
continue
|
||||
}
|
||||
for (let node of nodeList) {
|
||||
if (node.to == thdMenu.href) {
|
||||
// console.log("A match was found for node.to:", node.to)
|
||||
return thdMenu.index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Iterate through each secondary menu item in the secMenuList
|
||||
for (let secMenu of secMenuList) {
|
||||
// console.log(secMenu.index, secMenu.href == path)
|
||||
const nodeList = routeMap.get(path)
|
||||
if (utils.isEmpty(nodeList)) {
|
||||
continue
|
||||
}
|
||||
for (let node of nodeList) {
|
||||
if (node.to == secMenu.href) {
|
||||
// console.log("匹配上node.to:", node.to)
|
||||
return secMenu.index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// None of the menu items match the path, try to find a match for the 'to' from the route configuration
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// get routes configuration
|
||||
const routes = router.options.routes
|
||||
// console.log("routes:", routes)
|
||||
const routeMap = new Map()
|
||||
routes.forEach(lv1 => {
|
||||
// console.log("lv1:", lv1)
|
||||
buildRoutePer(lv1)
|
||||
})
|
||||
// console.log(routeMap)
|
||||
|
||||
function buildRoutePer(lv1) {
|
||||
const lv2List = lv1.children
|
||||
if (utils.isEmpty(lv2List)) {
|
||||
const node1 = {
|
||||
title: lv1.meta.title
|
||||
}
|
||||
const nodeList = [node1]
|
||||
routeMap.set(lv1.path, nodeList)
|
||||
return
|
||||
}
|
||||
lv2List.forEach(lv2 => {
|
||||
// console.log("lv2:", lv2)
|
||||
const node1 = {
|
||||
title: lv1.meta.title
|
||||
}
|
||||
const node2 = {
|
||||
title: lv2.meta.title
|
||||
}
|
||||
const nodeList = [node1, node2]
|
||||
if (utils.notNull(lv2.meta.subTitle)) {
|
||||
const node3 = {
|
||||
title: lv2.meta.subTitle,
|
||||
to: null
|
||||
}
|
||||
nodeList.push(node3)
|
||||
}
|
||||
routeMap.set(lv1.path + '/' + lv2.path, nodeList)
|
||||
})
|
||||
}
|
||||
|
||||
function routeTo(href) {
|
||||
if (href == undefined || href == null || href == '') {
|
||||
return
|
||||
}
|
||||
router.push(href)
|
||||
}
|
||||
|
||||
function getMenuNameByCode(code) {
|
||||
for (let menu of menuList) {
|
||||
if (menu.index == code) {
|
||||
return t(menu.menuName)
|
||||
}
|
||||
if (menu.subMenuList != null) {
|
||||
for (let subMenu of menu.subMenuList) {
|
||||
if (subMenu.index == code) {
|
||||
return t(subMenu.menuName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return t(code)
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
span {
|
||||
/* Prevent text selection from double-clicking */
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
span {
|
||||
/* Font size */
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
li {
|
||||
/* Font size */
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/** When the menu is collapsed, redefine the hover menu height */
|
||||
.el-menu-item {
|
||||
min-width: 44px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.el-menu-custom {
|
||||
border-right: none;
|
||||
--el-menu-item-height: 40px;
|
||||
--el-menu-sub-item-height: 36px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.el-menu-custom .el-menu--collapse {
|
||||
width: 44px;
|
||||
}
|
||||
|
||||
.el-menu-custom:not(.el-menu--collapse) {
|
||||
width: 200px;
|
||||
}
|
||||
</style>
|
248
desktop/frontend/src/components/MainFrame.vue
Normal file
248
desktop/frontend/src/components/MainFrame.vue
Normal file
@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<el-aside width="collapse" class="layout-aside" :class="shrink ? 'shrink' : ''">
|
||||
<div :class="menuCollapse ? 'fixed-menu-collapse fxc' : 'fixed-menu-expand fxsb'">
|
||||
<div v-show="!menuCollapse" class="menu-logo">
|
||||
<el-link type="primary" @click="refresh" class="pl-20 pr-4">
|
||||
<img src="@/assets/img/logo-sm.png" class="fxc" height="34px" alt="logo" />
|
||||
</el-link>
|
||||
</div>
|
||||
<el-link class="plr-10 w-56" @click="menuToggle">
|
||||
<el-icon :size="20">
|
||||
<Fold v-show="!menuCollapse" />
|
||||
<Expand v-show="menuCollapse" />
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<el-scrollbar class="scrollbar-menu-wrapper" :class="shrink ? 'shrink' : ''">
|
||||
<AsideMenu />
|
||||
</el-scrollbar>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header>
|
||||
<TopHeader />
|
||||
</el-header>
|
||||
<el-main>
|
||||
<el-scrollbar style="width: 100%;">
|
||||
<!-- Router View Container -->
|
||||
<!-- { Component } = currently matched route component -->
|
||||
<RouterView v-slot="{ Component }">
|
||||
<!-- Cached Route Transition: Only keeps alive components with keepAlive meta flag
|
||||
Transition animation requires single root element in component Key ensures proper re-rendering on route path changes -->
|
||||
<transition :name="transitionName">
|
||||
<KeepAlive>
|
||||
<Component :is="Component" v-if="keepAlive" :key="$route.path" />
|
||||
</KeepAlive>
|
||||
</transition>
|
||||
<!-- Non-cached Route Transition: Fresh instance for other components
|
||||
Separate transition to prevent animation conflicts -->
|
||||
<transition :name="transitionName">
|
||||
<Component :is="Component" v-if="!keepAlive" :key="$route.path" />
|
||||
</transition>
|
||||
</RouterView>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
<div class="aside-menu-shade">
|
||||
|
||||
</div>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TopHeader from '@/components/TopHeader.vue'
|
||||
import AsideMenu from '@/components/AsideMenu.vue'
|
||||
import { ref, reactive, computed, watch, onBeforeMount } from 'vue'
|
||||
import { useRouter, RouterView } from 'vue-router'
|
||||
import { Expand, Fold } from '@element-plus/icons-vue'
|
||||
import { showShade, closeShade } from '@/assets/js/shade'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const router = useRouter()
|
||||
const config = useConfig()
|
||||
|
||||
const { shrink, menuCollapse } = storeToRefs(config)
|
||||
const currentRoute = reactive(router.currentRoute)
|
||||
|
||||
// Default transition effect, slide to the left
|
||||
let transitionName = 'slide-left'
|
||||
|
||||
const keepAlive = computed(() => {
|
||||
return currentRoute.value.meta.keepAlive
|
||||
})
|
||||
|
||||
/**
|
||||
* Set the menu animation duration to 0ms on page refresh to prevent the menu from expanding or collapsing
|
||||
* with an animation. This ensures that the menu state remains consistent after a page reload.
|
||||
*/
|
||||
const menuAnimationDuration = ref(0)
|
||||
|
||||
// Function to toggle the menu between expanded and collapsed states
|
||||
function menuToggle() {
|
||||
menuAnimationDuration.value = '300ms'
|
||||
|
||||
if (menuCollapse.value) {
|
||||
// console.log("Extend menu")
|
||||
if (shrink.value) {
|
||||
// Expend the shade if menu is collapsing
|
||||
showShade(() => {
|
||||
// Callback function to close the shade after the menu has collapsed
|
||||
config.setMenuCollapse(true)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// If the menu is in an expanded state, close the shade
|
||||
closeShade()
|
||||
}
|
||||
// Toggle the menu state
|
||||
config.setMenuCollapse(!menuCollapse.value)
|
||||
}
|
||||
|
||||
function onAdaptiveLayout() {
|
||||
// Get the current window width
|
||||
const clientWidth = document.body.clientWidth
|
||||
// console.log("menuCollapse:", menuCollapse.value, config.getMenuCollapse(), "clientWidth:", clientWidth)
|
||||
// Determine if the aside menu should be shrunk based on the window width
|
||||
if (clientWidth < 800) {
|
||||
config.setShrink(true)
|
||||
if (!menuCollapse.value) {
|
||||
// Collapse the menu if it is not already collapsed
|
||||
menuToggle()
|
||||
}
|
||||
} else {
|
||||
config.setShrink(false)
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
onAdaptiveLayout()
|
||||
useEventListener(window, 'resize', onAdaptiveLayout)
|
||||
})
|
||||
|
||||
watch(() => router.currentRoute.value.path, (newValue, oldValue) => {
|
||||
// If the layout is shrunk and the menu is expanded, collapse the menu
|
||||
if (shrink.value && !menuCollapse.value) {
|
||||
menuToggle()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
function refresh() {
|
||||
// Reload the page
|
||||
location.reload()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding: 0px;
|
||||
/* width: calc(100% -32px);
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
border-radius: 6px; */
|
||||
background-color: var(--el-fg-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
aside {
|
||||
background-color: var(--el-fg-color);
|
||||
}
|
||||
|
||||
aside.shrink {
|
||||
width: 44px;
|
||||
}
|
||||
|
||||
.layout-aside {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
transition: width .3s ease;
|
||||
}
|
||||
|
||||
main {
|
||||
height: calc(100vh - 44px);
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.menu-logo {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Keyframes for the menu collapse animation */
|
||||
@keyframes menuCollapse {
|
||||
0% {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
100% {
|
||||
width: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Keyframes for the menu expand animation */
|
||||
@keyframes menuExpand {
|
||||
0% {
|
||||
width: 44px;
|
||||
}
|
||||
|
||||
100% {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-menu-collapse {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
/* Reference to the keyframes */
|
||||
animation-name: menuCollapse;
|
||||
/* Duration of the animation */
|
||||
animation-duration: v-bind('menuAnimationDuration');
|
||||
animation-timing-function: ease-in-out;
|
||||
background-color: var(--el-fg-color);
|
||||
}
|
||||
|
||||
.fixed-menu-expand {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
height: 44px;
|
||||
width: 200px;
|
||||
/* Reference to the keyframes */
|
||||
animation-name: menuExpand;
|
||||
/* Duration of the animation */
|
||||
animation-duration: v-bind('menuAnimationDuration');
|
||||
animation-timing-function: ease-in-out;
|
||||
background-color: var(--el-fg-color);
|
||||
z-index: 9999999;
|
||||
}
|
||||
|
||||
.scrollbar-menu-wrapper {
|
||||
top: 44px;
|
||||
height: calc(100vh - 44px);
|
||||
background-color: var(--el-fg-color);
|
||||
}
|
||||
|
||||
.scrollbar-menu-wrapper.shrink {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
z-index: 9999999;
|
||||
}
|
||||
</style>
|
112
desktop/frontend/src/components/TableTools.vue
Normal file
112
desktop/frontend/src/components/TableTools.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="fxsb table-tools">
|
||||
<div v-show="!advSearch">
|
||||
<el-button type="default" @click="baseSearch">
|
||||
<el-icon :size="20">
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button type="danger" class="ml-10" @click="delSelected" :disabled="selectedRows.length == 0">
|
||||
<el-icon :size="20" class="pr-4">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
{{ t('delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-show="advSearch">
|
||||
<el-button @click="resetSearch"> {{ t('reset') }}</el-button>
|
||||
<el-button type="primary" @click="search"> {{ t('search') }}</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-input v-model="searchForm.kw" @input="baseSearch" clearable v-show="!advSearch" class="mr-8" />
|
||||
|
||||
<el-button-group>
|
||||
<el-button type="default" @click="advSearchSwitch">
|
||||
<el-icon :size="20">
|
||||
<Search />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button type="default">
|
||||
<el-dropdown :hide-on-click="false">
|
||||
<el-icon :size="20">
|
||||
<Grid />
|
||||
</el-icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="dropdown-max">
|
||||
<el-dropdown-item :command=item.prop v-for="(item, index) in tableColumns" :key="index">
|
||||
<el-checkbox :label="item.label" :value="item.isShow" :checked="item.isShow"
|
||||
@change="checkTableColumn($event, item.prop)" />
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Refresh, Search, Grid, Plus, Delete } from '@element-plus/icons-vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const props = defineProps({
|
||||
advSearch: {
|
||||
default: false
|
||||
},
|
||||
searchForm: {
|
||||
default: () => ({
|
||||
kw: null
|
||||
})
|
||||
},
|
||||
tableColumns: {
|
||||
default: () => []
|
||||
},
|
||||
selectedRows: {
|
||||
default: []
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits([
|
||||
'search',
|
||||
'baseSearch',
|
||||
'advSearchSwitch',
|
||||
'checkTableColumn',
|
||||
'delSelected',
|
||||
'resetSearch',
|
||||
])
|
||||
|
||||
const baseSearch = () => {
|
||||
console.log('baseSearch')
|
||||
emits('baseSearch')
|
||||
}
|
||||
|
||||
const search = () => {
|
||||
emits('search')
|
||||
}
|
||||
|
||||
const delSelected = () => {
|
||||
emits('delSelected')
|
||||
}
|
||||
|
||||
const advSearchSwitch = () => {
|
||||
emits('advSearchSwitch')
|
||||
}
|
||||
|
||||
const checkTableColumn = (isCheck, prop) => {
|
||||
console.log('checkTableColumn:', isCheck, prop)
|
||||
emits('checkTableColumn', isCheck, prop)
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
emits('resetSearch')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.table-tools {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
138
desktop/frontend/src/components/TopHeader.vue
Normal file
138
desktop/frontend/src/components/TopHeader.vue
Normal file
@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<!-- 导航栏 -->
|
||||
<div class="nav-bar">
|
||||
<div class="fxc">
|
||||
<!-- 左侧固定下拉 -->
|
||||
<el-dropdown trigger="click" @command="handleSwitchModel" class="fxc plr-16">
|
||||
<span class="el-dropdown-link">
|
||||
{{ selectedModel }}
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="mod in modelList" :key="mod" :command="mod">
|
||||
{{ mod }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<!-- 刷新 -->
|
||||
<el-link @click="refresh">
|
||||
<el-icon :size="20">
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<!-- 右侧 -->
|
||||
<div class="fxc">
|
||||
<div class="mlr-8">
|
||||
<el-switch v-model="isDark" :active-action-icon="Moon" :inactive-action-icon="Sunny" width="40"
|
||||
style="--el-switch-on-color: #4c4d4f; --el-switch-off-color: #f2f2f2;" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧固定下拉 -->
|
||||
<el-dropdown trigger="click" @command="handleSwitchLang" class="fxc plr-16">
|
||||
<span class="el-dropdown-link">
|
||||
{{ selectedLang.name }}
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="lang in langList" :key="lang" :command="lang">
|
||||
{{ lang.name }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ArrowDown, Refresh, Moon, Sunny } from '@element-plus/icons-vue'
|
||||
import { useConfig } from '@/store/config'
|
||||
/** 暗黑主题切换 */
|
||||
import { useDark } from '@vueuse/core'
|
||||
|
||||
const config = useConfig()
|
||||
const isDark = useDark()
|
||||
|
||||
const modelList = ref(config.modelList)
|
||||
|
||||
const selectedModel = ref(config.selectedModel != null ? config.selectedModel : modelList.value[0])
|
||||
|
||||
|
||||
function handleSwitchModel(mod) {
|
||||
// console.log("handleSwitchModel:", model)
|
||||
selectedModel.value = mod
|
||||
}
|
||||
|
||||
const langList = ref(config.langList)
|
||||
|
||||
const selectedLang = ref(config.selectedLang != null ? config.selectedLang : langList.value[0])
|
||||
|
||||
function handleSwitchLang(lang) {
|
||||
selectedLang.value = lang
|
||||
config.setSelectedLang(lang)
|
||||
// i18n.locale = lang.code
|
||||
location.reload()
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
location.reload()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
height: 44px;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.el-dropdown-link {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
min-width: 80px;
|
||||
color: var(--el-color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* 禁止双击选中文字 */
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
height: 100%;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-menu .item {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.nav-menu .profile {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.nav-menu .profile img {
|
||||
width: 40px;
|
||||
height: 30px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
</style>
|
279
desktop/frontend/src/components/Upload.vue
Normal file
279
desktop/frontend/src/components/Upload.vue
Normal file
@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<!-- 预览图片列表 -->
|
||||
<div class="wp-100" v-if="isImgType" v-for="(file, index) in fileList">
|
||||
<div class="center img-container" v-show="file.imgUrl != null">
|
||||
<img :src="file.imgUrl" alt="" class="imgSize max-wp-100" />
|
||||
<div class="edit" v-show="file.imgUrl != null">
|
||||
<el-text tag="label">
|
||||
<el-icon :size="32">
|
||||
<Edit />
|
||||
</el-icon>
|
||||
<input type="file" :accept="accept" @change="files.cache(file, $event)" />
|
||||
</el-text>
|
||||
<el-text tag="label">
|
||||
<el-icon :size="32">
|
||||
<Delete @click="files.del(fileList, index)" />
|
||||
</el-icon>
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wp-100 mlr-auto mt-0-5" v-if="desc">
|
||||
<el-input type="textarea" v-model="file.remarkUpd" placeholder="请添加描述" />
|
||||
</div>
|
||||
<el-divider v-if="index < fileList.length - 1" />
|
||||
</div>
|
||||
|
||||
<!-- 预览单个图片 -->
|
||||
<div class="wp-100" v-if="isImgType && file != null && file.imgUrl != null">
|
||||
<div class="center img-container">
|
||||
<img :src="file.imgUrl" alt="" class="imgSize max-wp-100" />
|
||||
<div class="edit">
|
||||
<el-text tag="label">
|
||||
<el-icon :size="32">
|
||||
<Edit />
|
||||
</el-icon>
|
||||
<input type="file" :accept="accept" @change="files.cache(file, $event)" />
|
||||
</el-text>
|
||||
<el-text tag="label" style="text-align: right;">
|
||||
<el-icon :size="32">
|
||||
<Delete @click="files.del(file)" />
|
||||
</el-icon>
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wp-100 mlr-auto mt-0-5" v-if="desc">
|
||||
<el-input type="textarea" v-model="file.remarkUpd" placeholder="请添加描述" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预览文件列表 -->
|
||||
<div class="file-preview" v-if="isFileType" v-for="(file, index) in fileList">
|
||||
<el-text type="primary" class="min-w-360" style="text-align: left;">{{ file.fileName }}</el-text>
|
||||
<el-text tag="label" style="text-align: right;">
|
||||
<el-icon c:size="16">
|
||||
<Delete @click="files.del(fileList, index)" />
|
||||
</el-icon>
|
||||
</el-text>
|
||||
</div>
|
||||
|
||||
<!-- 预览文件 -->
|
||||
<div class="file-preview" v-if="isFileType && file != null && file.fileUrl != null">
|
||||
<el-text type="primary" class="min-w-360" style="text-align: left;">{{ file.fileName }}</el-text>
|
||||
<el-text tag="label">
|
||||
<el-icon c:size="16">
|
||||
<Delete @click="files.del(file)" />
|
||||
</el-icon>
|
||||
</el-text>
|
||||
</div>
|
||||
|
||||
<!-- 添加文件列表 -->
|
||||
<div class="add" v-if="fileList != undefined && fileList != null" :class="isDragover ? 'is-dragover' : ''"
|
||||
@dragover.prevent="handleDragOver" @dragleave="handleDragLeave($event)"
|
||||
@drop.prevent="handleDrop(fileList, $event)">
|
||||
<el-text>点击按钮上传</el-text>
|
||||
<el-text tag="label">
|
||||
<el-icon :size="32">
|
||||
<UploadFilled />
|
||||
</el-icon>
|
||||
<input type="file" :accept="accept" @change="files.cache(fileList, $event)" />
|
||||
</el-text>
|
||||
<el-text>或将文件拖动到此处</el-text>
|
||||
</div>
|
||||
|
||||
<!-- 添加单个文件 -->
|
||||
<div class="add" v-if="file != undefined && file != null && file.fileUrl == null && file.imgUrl == null"
|
||||
:class="addCss, isDragover ? 'is-dragover' : ''" @dragover.prevent="handleDragOver"
|
||||
@dragleave="handleDragLeave($event)" @drop.prevent="handleDrop(file, $event)">
|
||||
<el-text>点击按钮上传</el-text>
|
||||
<el-text tag="label">
|
||||
<el-icon :size="32">
|
||||
<UploadFilled />
|
||||
</el-icon>
|
||||
<input type="file" :accept="accept" @change="files.cache(file, $event)" />
|
||||
</el-text>
|
||||
<el-text>或将文件拖动到此处</el-text>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, inject } from 'vue'
|
||||
import { Edit, Delete, UploadFilled } from '@element-plus/icons-vue'
|
||||
|
||||
|
||||
const utils = inject('utils')
|
||||
const files = inject('files')
|
||||
|
||||
const props = defineProps(['title', 'file', 'fileList', 'accept', 'type', 'desc', 'w', 'h', 'addCss'])
|
||||
|
||||
const title = computed(() => {
|
||||
return utils.notNull(props.title) ? props.title : "选择文件"
|
||||
})
|
||||
|
||||
const accept = computed(() => {
|
||||
return utils.notNull(props.accept) ? props.accept : "*"
|
||||
})
|
||||
|
||||
const type = computed(() => {
|
||||
return utils.notNull(props.type) ? props.type : 'file'
|
||||
})
|
||||
|
||||
const desc = computed(() => {
|
||||
return utils.notNull(props.desc) ? props.desc : false
|
||||
})
|
||||
|
||||
const w = computed(() => {
|
||||
if (utils.notNull(props.w)) {
|
||||
return props.w + 'px'
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const h = computed(() => {
|
||||
if (utils.notNull(props.h)) {
|
||||
return props.h + 'px'
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const addCss = computed(() => {
|
||||
return utils.notNull(props.addCss) ? props.addCss : ""
|
||||
})
|
||||
|
||||
const isImgType = computed(() => {
|
||||
return props.type == 'img'
|
||||
})
|
||||
|
||||
const isFileType = computed(() => {
|
||||
return props.type == undefined || props.type == null || props.type == 'file'
|
||||
})
|
||||
|
||||
const isDragover = ref(false)
|
||||
// 改变样式表示可以放置
|
||||
// event.target.style.backgroundColor = 'lightblue';
|
||||
function handleDragOver() {
|
||||
console.log("handleDragOver")
|
||||
isDragover.value = true
|
||||
}
|
||||
|
||||
function handleDragLeave($event) {
|
||||
console.log("handleDragLeave")
|
||||
// 防抖处理
|
||||
if ($event.currentTarget.contains($event.relatedTarget)) {
|
||||
isDragover.value = true
|
||||
} else {
|
||||
isDragover.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrop(fileObj, $event) {
|
||||
console.log("handleDrop:", fileObj)
|
||||
isDragover.value = false
|
||||
files.cache(fileObj, $event)
|
||||
}
|
||||
onMounted(() => {
|
||||
console.log("file,fileList:", props.file, props.fileList)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
input[type=file] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-text {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
color: var(--el-text-color);
|
||||
background-color: none;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.el-text:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
/* 图片hover按钮-start */
|
||||
.img-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
min-height: 60px;
|
||||
width: 100%;
|
||||
/*防止撑开父元素*/
|
||||
min-width: 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.img-container i {
|
||||
animation: blink 2s infinite;
|
||||
}
|
||||
|
||||
.img-container img {
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
opacity: 0.7;
|
||||
transition: 0.5s ease;
|
||||
}
|
||||
|
||||
.img-container div {
|
||||
min-height: 60px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.add {
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
margin: 9px 14px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div:hover.add {
|
||||
border: 1px dashed var(--el-color-primary);
|
||||
}
|
||||
|
||||
.is-dragover {
|
||||
padding: calc(var(--el-upload-dragger-padding-horizontal) - 1px) calc(var(--el-upload-dragger-padding-vertical) - 1px);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border: 2px dashed var(--el-color-primary)
|
||||
}
|
||||
|
||||
div.file-preview {
|
||||
display: flex;
|
||||
justify-content: space-between !important;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
padding: 0px;
|
||||
margin-top: 9px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
div:hover.file-preview {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
i {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.imgSize {
|
||||
width: v-bind(w);
|
||||
height: v-bind(h);
|
||||
}
|
||||
</style>
|
53
desktop/frontend/src/locales/en.js
Normal file
53
desktop/frontend/src/locales/en.js
Normal file
@ -0,0 +1,53 @@
|
||||
export default {
|
||||
add: "Add",
|
||||
edit: "Edit",
|
||||
delete: "Delete",
|
||||
search: "Search",
|
||||
reset: "Reset",
|
||||
confirm: "Confirm",
|
||||
cancel: "Cancel",
|
||||
save: "Save",
|
||||
submit: "Submit",
|
||||
export: "Export",
|
||||
import: "Import",
|
||||
copy: "Copy",
|
||||
paste: "Paste",
|
||||
cut: "Cut",
|
||||
baseInfo: "Base Info",
|
||||
|
||||
createdDt: "Created Date",
|
||||
updatedDt: "Updated Date",
|
||||
noData: "No Data",
|
||||
|
||||
menu: {
|
||||
task: "Task",
|
||||
history: "History",
|
||||
config: {
|
||||
settings: "Settings",
|
||||
general: "General Config",
|
||||
llm: "LLM Config",
|
||||
theme: "Theme Config"
|
||||
}
|
||||
},
|
||||
user: "User",
|
||||
switchModel: "Switch Model",
|
||||
step: "Step",
|
||||
promptInputPlaceHolder: "Please Input Task Prompt",
|
||||
promptInput: "Prompt Input",
|
||||
promptInputKw: "Prompt Input",
|
||||
clearCache: "Clear Cache",
|
||||
clearCacheSuccess: "Clear cache success",
|
||||
openManusAgiTips: "The above content is generated by OpenManus for reference only",
|
||||
taskStatus: {
|
||||
name: "Task Status",
|
||||
success: "Success",
|
||||
failed: "Failed",
|
||||
running: "Running",
|
||||
terminated: "Terminated",
|
||||
},
|
||||
newTask: "New Task",
|
||||
readConfigSuccess: "Read config success",
|
||||
readConfigFailed: "Read config failed",
|
||||
baseConfig: "Base Settings",
|
||||
serverConfig: "Server Config",
|
||||
}
|
22
desktop/frontend/src/locales/i18n.js
Normal file
22
desktop/frontend/src/locales/i18n.js
Normal file
@ -0,0 +1,22 @@
|
||||
// i18n配置
|
||||
import { createI18n } from "vue-i18n"
|
||||
import zhCn from "./zh-cn"
|
||||
import en from "./en"
|
||||
|
||||
const config = localStorage.getItem('config') ? JSON.parse(localStorage.getItem('config')) : {}
|
||||
|
||||
// 创建i18n
|
||||
const i18n = createI18n({
|
||||
// 语言标识
|
||||
locale: config.selectedLang ? config.selectedLang.code : 'zhCn',
|
||||
// 全局注入,可以直接使用$t
|
||||
globalInjection: true,
|
||||
// 处理报错: Uncaught (in promise) SyntaxError: Not available in legacy mode (at message-compiler.esm-bundler.js:54:19)
|
||||
legacy: false,
|
||||
messages: {
|
||||
zhCn,
|
||||
en
|
||||
}
|
||||
})
|
||||
|
||||
export default i18n
|
52
desktop/frontend/src/locales/zh-cn.js
Normal file
52
desktop/frontend/src/locales/zh-cn.js
Normal file
@ -0,0 +1,52 @@
|
||||
export default {
|
||||
add: "新增",
|
||||
edit: "编辑",
|
||||
delete: "删除",
|
||||
search: "搜索",
|
||||
reset: "重置",
|
||||
confirm: "确认",
|
||||
cancel: "取消",
|
||||
save: "保存",
|
||||
submit: "提交",
|
||||
export: "导出",
|
||||
import: "导入",
|
||||
copy: "复制",
|
||||
paste: "粘贴",
|
||||
cut: "剪切",
|
||||
baseInfo: "基本信息",
|
||||
|
||||
createdDt: "创建时间",
|
||||
updatedDt: "更新时间",
|
||||
noData: "暂无数据",
|
||||
|
||||
menu: {
|
||||
task: "任务",
|
||||
history: "历史记录",
|
||||
config: {
|
||||
settings: "设置",
|
||||
general: "通用设置",
|
||||
llm: "大模型设置",
|
||||
theme: "主题设置"
|
||||
}
|
||||
},
|
||||
user: '用户',
|
||||
step: "步骤",
|
||||
promptInputPlaceHolder: "请输入任务提示词",
|
||||
promptInput: "提示词输入",
|
||||
promptInputKw: "提示词关键字",
|
||||
clearCache: "清理缓存",
|
||||
clearCacheSuccess: "清理缓存成功",
|
||||
openManusAgiTips: "以上内容由OpenManus生成, 仅供参考和借鉴",
|
||||
taskStatus: {
|
||||
name: "任务状态",
|
||||
success: "成功",
|
||||
failed: "失败",
|
||||
running: "运行中",
|
||||
terminated: "终止",
|
||||
},
|
||||
newTask: "新任务",
|
||||
readConfigSuccess: "读取配置成功",
|
||||
readConfigFailed: "读取配置失败",
|
||||
baseConfig: "基础设置",
|
||||
serverConfig: "服务器配置",
|
||||
}
|
54
desktop/frontend/src/main.js
Normal file
54
desktop/frontend/src/main.js
Normal file
@ -0,0 +1,54 @@
|
||||
import './assets/css/main.css'
|
||||
|
||||
import files from '@/assets/js/files'
|
||||
import utils from '@/assets/js/utils'
|
||||
import verify from '@/assets/js/verify'
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import i18n from './locales/i18n'
|
||||
import router from './router'
|
||||
|
||||
// import ElementPlus from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
/* Dark theme configuration */
|
||||
import '@/assets/less/dark.css'
|
||||
import '@/assets/less/light.css'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
// Configure Vue production flags
|
||||
window.__VUE_PROD_DEVTOOLS__ = false
|
||||
window.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = false
|
||||
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(pinia)
|
||||
|
||||
// Globally reference router
|
||||
app.use(router)
|
||||
|
||||
app.use(i18n)
|
||||
|
||||
// Register Element Plus Message component globally (required for use in utils)
|
||||
app.use(ElMessage)
|
||||
|
||||
// Global configuration of ElementPlus
|
||||
// ElSelect.props.placeholder.default = '请选择'
|
||||
// When ElementPlus is imported, a global configuration object can be passed in which contains size and zIndex properties
|
||||
// size is used to set the default size of form components, and zIndex is used to set the layer level of pop-up components.
|
||||
// app.use(ElementPlus, { locale, size: 'default', zIndex: 2000 })
|
||||
|
||||
// Configure global providers for shared utilities
|
||||
app.provide('utils', utils)
|
||||
|
||||
|
||||
app.provide('files', files)
|
||||
app.provide('verify', verify)
|
||||
|
||||
/* app.provide('uuid', uuidv4) */
|
||||
|
||||
app.mount('#app')
|
83
desktop/frontend/src/router/index.js
Normal file
83
desktop/frontend/src/router/index.js
Normal file
@ -0,0 +1,83 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/components/MainFrame.vue'),
|
||||
meta: {
|
||||
title: "主页"
|
||||
},
|
||||
// 重定向到默认页面
|
||||
redirect: '/task',
|
||||
children: [
|
||||
{
|
||||
path: 'task',
|
||||
component: () => import('@/views/task/TaskIndex.vue'),
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
title: "任务列表",
|
||||
index: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'task/:id',
|
||||
component: () => import('@/views/task/TaskInfo.vue'),
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
title: "任务信息",
|
||||
index: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'history',
|
||||
component: () => import('@/views/task/HistoryIndex.vue'),
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
title: "历史记录",
|
||||
index: 0
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/config',
|
||||
component: () => import('@/components/MainFrame.vue'),
|
||||
meta: {
|
||||
title: "设置"
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'general',
|
||||
component: () => import('@/views/config/General.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
title: "常规设置",
|
||||
index: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'llm',
|
||||
component: () => import('@/views/config/Llm.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
title: "大模型配置",
|
||||
index: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'theme',
|
||||
component: () => import('@/views/config/Theme.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
title: "主题",
|
||||
index: 1
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
96
desktop/frontend/src/store/config.js
Normal file
96
desktop/frontend/src/store/config.js
Normal file
@ -0,0 +1,96 @@
|
||||
import { defineStore } from "pinia"
|
||||
|
||||
export const useConfig = defineStore("config", {
|
||||
state: () => {
|
||||
return {
|
||||
// 全局
|
||||
// aside是否收缩
|
||||
shrink: false,
|
||||
isDark: false,
|
||||
// 侧边栏
|
||||
// 菜单是否折叠
|
||||
menuCollapse: false,
|
||||
selectedModel: null,
|
||||
modelList: ['qwen2.5-7b', 'deepseek-r1-7b'],
|
||||
selectedLang: { code: 'en', name: 'English' },
|
||||
langList: [{ code: 'en', name: 'English' }, { code: 'zhCn', name: '简体中文' }],
|
||||
taskHistory: [
|
||||
// taskId, prompt, stepList, status, createdDt
|
||||
]
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
||||
getShrink() {
|
||||
return this.shrink
|
||||
},
|
||||
setShrink(shrink) {
|
||||
this.shrink = shrink
|
||||
},
|
||||
|
||||
getIsDark() {
|
||||
return this.isDark
|
||||
},
|
||||
|
||||
getMenuCollapse() {
|
||||
return this.menuCollapse
|
||||
},
|
||||
|
||||
setMenuCollapse(menuCollapse) {
|
||||
this.menuCollapse = menuCollapse
|
||||
},
|
||||
|
||||
getSelectedModel() {
|
||||
return this.selectedModel
|
||||
},
|
||||
|
||||
setSelectedModel(selectedModel) {
|
||||
this.selectedModel = selectedModel
|
||||
},
|
||||
|
||||
getModelList() {
|
||||
return this.modelList
|
||||
},
|
||||
|
||||
setModelList(modelList) {
|
||||
utils.copyArray(modelList, this.modelList)
|
||||
},
|
||||
|
||||
getSelectedLang() {
|
||||
return this.selectedLang
|
||||
},
|
||||
|
||||
setSelectedLang(selectedLang) {
|
||||
this.selectedLang = selectedLang
|
||||
},
|
||||
|
||||
getLangList() {
|
||||
return this.langList
|
||||
},
|
||||
|
||||
getTaskHistory() {
|
||||
return this.taskHistory
|
||||
},
|
||||
|
||||
setTaskHistory(taskHistory) {
|
||||
utils.copyArray(taskHistory, this.taskHistory)
|
||||
},
|
||||
|
||||
addTaskHistory(task) {
|
||||
// 添加到数组开头
|
||||
this.taskHistory.unshift(task)
|
||||
},
|
||||
|
||||
// 获取当前, 任务列表中第一个
|
||||
getCurrTask() {
|
||||
if (this.taskHistory.length == 0) {
|
||||
return {}
|
||||
}
|
||||
return this.taskHistory[0]
|
||||
},
|
||||
|
||||
},
|
||||
persist: {
|
||||
key: "config",
|
||||
}
|
||||
})
|
213
desktop/frontend/src/views/config/General.vue
Normal file
213
desktop/frontend/src/views/config/General.vue
Normal file
@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div class="main-content">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="title fxsb">
|
||||
<div>{{ t('baseConfig') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Show Data -->
|
||||
<div class="card-row-wrap" v-show="baseShow">
|
||||
<div class="card-row-aline fxsb">
|
||||
<el-text>{{ t('clearCache') }}:</el-text>
|
||||
<el-button type="danger" class="mlr-10" @click="clearCache">{{ t('clearCache') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="title fxsb">
|
||||
<div> {{ t('serverConfig') }}</div>
|
||||
<div>
|
||||
<el-link type="primary" class="no-select plr-6" @click="toEdit('server')" v-show="serverShow">
|
||||
{{ t('edit') }}
|
||||
</el-link>
|
||||
<el-link type="primary" class="no-select plr-6" @click="toShow('server')" v-show="serverEdit">
|
||||
{{ t('cancel') }}
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- No Data -->
|
||||
<div class="no-data" v-show="serverNoData">{{ t('noData') }}</div>
|
||||
|
||||
<!-- Show Data -->
|
||||
<div class="card-row-wrap" v-show="serverShow">
|
||||
<div class="card-row-item">
|
||||
<el-text>host:</el-text>
|
||||
<el-text>{{ serverConfig.host }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>port:</el-text>
|
||||
<el-text tag="p">{{ serverConfig.port }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Module -->
|
||||
<el-form ref="ruleFormRef" :model="serverConfigUpd" status-icon :rules="rules" v-show="serverEdit">
|
||||
<div class="card-row-wrap">
|
||||
<div class="card-row-item">
|
||||
<el-text>host:</el-text>
|
||||
<el-form-item prop="host">
|
||||
<el-input v-model="serverConfigUpd.host" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>port:</el-text>
|
||||
<el-form-item prop="port">
|
||||
<el-input v-model="serverConfigUpd.port" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-aline fxc" v-show="serverEdit">
|
||||
<el-button class="mlr-10" @click="toShow('server')">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" class="mlr-10" @click="submitForm">{{ t('submit') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const utils = inject('utils')
|
||||
const files = inject('files')
|
||||
const verify = inject('verify')
|
||||
const router = useRouter()
|
||||
const config = useConfig()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 视图模式
|
||||
const viewModel = reactive({
|
||||
base: 'show',
|
||||
server: 'show',
|
||||
})
|
||||
|
||||
function toShow(model) {
|
||||
console.log("toShow:" + model)
|
||||
viewModel[model] = 'show'
|
||||
}
|
||||
|
||||
function toEdit(model) {
|
||||
console.log("toEdit:" + model)
|
||||
viewModel[model] = 'edit'
|
||||
}
|
||||
|
||||
const baseShow = computed(() => {
|
||||
return viewModel.base == 'show'
|
||||
})
|
||||
|
||||
const baseEdit = computed(() => {
|
||||
return viewModel.base == 'edit'
|
||||
})
|
||||
|
||||
const baseNoData = computed(() => {
|
||||
return baseShow && serverConfig.model == null
|
||||
})
|
||||
|
||||
const serverShow = computed(() => {
|
||||
return viewModel.server == 'show'
|
||||
})
|
||||
|
||||
const serverEdit = computed(() => {
|
||||
return viewModel.server == 'edit'
|
||||
})
|
||||
|
||||
const readConfigSuccess = ref(false)
|
||||
|
||||
const serverNoData = computed(() => {
|
||||
return serverShow && !readConfigSuccess.value
|
||||
})
|
||||
|
||||
const serverConfig = reactive({
|
||||
host: null,
|
||||
port: null,
|
||||
})
|
||||
|
||||
const serverConfigUpd = reactive({
|
||||
host: null,
|
||||
port: null,
|
||||
})
|
||||
|
||||
function clearCache() {
|
||||
config.$reset()
|
||||
utils.pop(t('clearCacheSuccess'))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 读取配置文件config/config.toml
|
||||
files.readAll("@/../../config/config.toml").then((fileContent) => {
|
||||
console.log("config/config.toml: ", fileContent)
|
||||
if (utils.notBlank(fileContent)) {
|
||||
readConfigSuccess.value = true
|
||||
} else {
|
||||
utils.pop(t('readConfigFailed'))
|
||||
return
|
||||
}
|
||||
const lines = utils.stringToLines(fileContent)
|
||||
|
||||
// 读取[server]
|
||||
const serverStart = lines.findIndex((line) => {
|
||||
return line.includes("[server]")
|
||||
})
|
||||
for (let i = serverStart + 1; i < lines.length; i++) {
|
||||
console.log("line: ", lines[i])
|
||||
// 判定是否到了下个配置模块
|
||||
if (lines[i].startsWith("[")) {
|
||||
break
|
||||
}
|
||||
// 读取配置
|
||||
const line = lines[i]
|
||||
const lineArr = line.split("=")
|
||||
if (lineArr.length != 2) {
|
||||
continue
|
||||
}
|
||||
const key = lineArr[0].trim()
|
||||
const value = lineArr[1].trim()
|
||||
serverConfig[key] = value
|
||||
}
|
||||
console.log("serverConfig read from file: ", serverConfig)
|
||||
utils.copyProps(serverConfig, serverConfigUpd)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
await ruleFormRef.value.validate();
|
||||
if (!utils.hasDfProps(serverConfig, serverConfigUpd)) {
|
||||
ElMessage.success('未发生更改!');
|
||||
toShow('server')
|
||||
return
|
||||
}
|
||||
ElMessage.success('验证通过,提交表单');
|
||||
// update()
|
||||
} catch (error) {
|
||||
ElMessage.error('参数验证失败');
|
||||
}
|
||||
}
|
||||
|
||||
const rules = reactive({
|
||||
host: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
port: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
api_key: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
max_tokens: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
temperature: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
224
desktop/frontend/src/views/config/Llm.vue
Normal file
224
desktop/frontend/src/views/config/Llm.vue
Normal file
@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="main-content">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="title fxsb">
|
||||
<div>LLM Config</div>
|
||||
<div>
|
||||
<el-link type="primary" class="no-select plr-6" @click="toEdit('base')" v-show="baseShow">
|
||||
{{ t('edit') }}
|
||||
</el-link>
|
||||
<el-link type="primary" class="no-select plr-6" @click="toShow('base')" v-show="baseEdit">
|
||||
{{ t('cancel') }}
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- No Data -->
|
||||
<div class="no-data" v-show="baseNoData">{{ t('noData') }}</div>
|
||||
|
||||
<!-- Show Data -->
|
||||
<div class="card-row-wrap" v-show="baseShow">
|
||||
<div class="card-row-item">
|
||||
<el-text>model:</el-text>
|
||||
<el-text>{{ llmConfig.model }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>base_url:</el-text>
|
||||
<el-text tag="p">{{ llmConfig.base_url }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>api_key:</el-text>
|
||||
<el-text>{{ llmConfig.api_key }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>max_tokens:</el-text>
|
||||
<el-text>{{ llmConfig.max_tokens }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>temperature:</el-text>
|
||||
<el-text>{{ llmConfig.temperature }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Module -->
|
||||
<el-form ref="ruleFormRef" :model="llmConfigUpd" status-icon :rules="rules" v-show="baseEdit">
|
||||
<div class="card-row-wrap">
|
||||
<div class="card-row-item">
|
||||
<el-text>model:</el-text>
|
||||
<el-form-item prop="model">
|
||||
<el-input v-model="llmConfigUpd.model" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>base_url:</el-text>
|
||||
<el-form-item prop="base_url">
|
||||
<el-input v-model="llmConfigUpd.base_url" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>api_key:</el-text>
|
||||
<el-form-item prop="api_key">
|
||||
<el-input v-model="llmConfigUpd.api_key" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>max_tokens:</el-text>
|
||||
<el-form-item prop="max_tokens">
|
||||
<el-input v-model="llmConfigUpd.max_tokens" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>temperature:</el-text>
|
||||
<el-form-item prop="temperature">
|
||||
<el-input v-model="llmConfigUpd.temperature" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-aline fxc" v-show="baseEdit">
|
||||
<el-button class="mlr-10" @click="toShow('base')">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" class="mlr-10" @click="submitForm">{{ t('submit') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const utils = inject('utils')
|
||||
const files = inject('files')
|
||||
const verify = inject('verify')
|
||||
const router = useRouter()
|
||||
const config = useConfig()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 视图模式
|
||||
const viewModel = reactive({
|
||||
base: 'show',
|
||||
})
|
||||
|
||||
function toShow(model) {
|
||||
console.log("toShow:" + model)
|
||||
viewModel[model] = 'show'
|
||||
}
|
||||
|
||||
function toEdit(model) {
|
||||
console.log("toEdit:" + model)
|
||||
viewModel[model] = 'edit'
|
||||
}
|
||||
|
||||
const baseShow = computed(() => {
|
||||
return viewModel.base == 'show'
|
||||
})
|
||||
|
||||
const baseEdit = computed(() => {
|
||||
return viewModel.base == 'edit'
|
||||
})
|
||||
|
||||
const readConfigSuccess = ref(false)
|
||||
|
||||
const baseNoData = computed(() => {
|
||||
return baseShow && !readConfigSuccess.value
|
||||
})
|
||||
|
||||
const llmConfig = reactive({
|
||||
model: null,
|
||||
base_url: null,
|
||||
api_key: null,
|
||||
max_tokens: null,
|
||||
temperature: null,
|
||||
})
|
||||
|
||||
const llmConfigUpd = reactive({
|
||||
model: null,
|
||||
base_url: null,
|
||||
api_key: null,
|
||||
max_tokens: null,
|
||||
temperature: null,
|
||||
})
|
||||
|
||||
function clearCache() {
|
||||
config.$reset()
|
||||
utils.pop(t('clearCacheSuccess'))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 读取配置文件config/config.toml
|
||||
files.readAll("@/../../config/config.toml").then((fileContent) => {
|
||||
console.log("config/config.toml: ", fileContent)
|
||||
if (utils.notBlank(fileContent)) {
|
||||
readConfigSuccess.value = true
|
||||
} else {
|
||||
utils.pop(t('readConfigFailed'))
|
||||
return
|
||||
}
|
||||
const lines = utils.stringToLines(fileContent)
|
||||
|
||||
// 读取[llm]
|
||||
const llmStart = lines.findIndex((line) => {
|
||||
return line.includes("[llm]")
|
||||
})
|
||||
for (let i = llmStart + 1; i < lines.length; i++) {
|
||||
console.log("line: ", lines[i])
|
||||
// 判定是否到了下个配置模块
|
||||
if (lines[i].startsWith("[")) {
|
||||
break
|
||||
}
|
||||
// 读取配置
|
||||
const line = lines[i]
|
||||
const lineArr = line.split("=")
|
||||
if (lineArr.length != 2) {
|
||||
continue
|
||||
}
|
||||
const key = lineArr[0].trim()
|
||||
const value = lineArr[1].trim()
|
||||
llmConfig[key] = value
|
||||
}
|
||||
console.log("llmConfig read from file: ", llmConfig)
|
||||
utils.copyProps(llmConfig, llmConfigUpd)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
await ruleFormRef.value.validate();
|
||||
if (!utils.hasDfProps(llmConfig, llmConfigUpd)) {
|
||||
ElMessage.success('未发生更改!');
|
||||
toShow('base')
|
||||
return
|
||||
}
|
||||
ElMessage.success('验证通过,提交表单');
|
||||
// update()
|
||||
} catch (error) {
|
||||
ElMessage.error('参数验证失败');
|
||||
}
|
||||
}
|
||||
|
||||
const rules = reactive({
|
||||
model: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
base_url: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
api_key: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
max_tokens: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
temperature: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
32
desktop/frontend/src/views/config/Theme.vue
Normal file
32
desktop/frontend/src/views/config/Theme.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="main-content">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="title fxsb">
|
||||
<div>基本信息</div>
|
||||
<div>
|
||||
<el-link type="primary" class="no-select plr-6" @click="clearCache()">清理缓存</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {useConfig} from '@/store/config'
|
||||
|
||||
const utils = inject('utils')
|
||||
const router = useRouter()
|
||||
const config = useConfig()
|
||||
|
||||
function clearCache() {
|
||||
config.$reset()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
237
desktop/frontend/src/views/task/HistoryIndex.vue
Normal file
237
desktop/frontend/src/views/task/HistoryIndex.vue
Normal file
@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div class="main-content">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="adv-search" :class="advSearch ? 'expand' : ''">
|
||||
<div class="card-row-wrap">
|
||||
<div class="card-row-item">
|
||||
<el-text tag="label">taskId:</el-text>
|
||||
<el-input v-model="searchForm.taskId" placeholder="taskId" maxlength="50" show-word-limit />
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>{{ t('promptInputKw') }}:</el-text>
|
||||
<el-input v-model="searchForm.promptInput" :placeholder="t('promptInputKw')" />
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>{{ t('taskStatus.name') }}:</el-text>
|
||||
<el-select clearable v-model="searchForm.taskStatus">
|
||||
<el-option v-for="opt in taskStatusOpts" :key="opt.key" :value="opt.value" :label="t(opt.label)" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TableTools :advSearch="advSearch" :searchForm="searchForm" :tableColumns="tableColumns"
|
||||
:selectedRows="selectedRows" @baseSearch="baseSearch" @search="search" @advSearchSwitch="advSearchSwitch"
|
||||
@delSelected="delSelected" @resetSearch="resetSearch" @checkTableColumn="checkTableColumn" />
|
||||
</template>
|
||||
|
||||
<el-table ref="tableRef" @selection-change="handleSelectionChange" :data="pageInfo.list" stripe border
|
||||
style="width: 100%" highlight-current-row max-height="760" :cell-style="{ textAlign: 'center' }"
|
||||
:header-cell-style="{ textAlign: 'center' }">
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column type="index" label="#" width="50" />
|
||||
<el-table-column prop="taskId" label="TaskId" width="300">
|
||||
<template #default="scope">
|
||||
<el-link @click="toTaskInfo(scope.row.taskId)" type="primary" class="h-20">
|
||||
{{ scope.row.taskId }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-for="col in showTableColumns" :prop=col.prop :label="col.label" :width="col.width"
|
||||
:minWidth="col.minWidth" :showOverflowTooltip="col.showOverflowTooltip" />
|
||||
</el-table>
|
||||
|
||||
<el-pagination v-model:current-page="pageInfo.pageNum" v-model:page-size="pageInfo.pageSize"
|
||||
:total="pageInfo.total" layout="total, prev, pager, next" />
|
||||
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, computed, onMounted, onBeforeUnmount, onUnmounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const router = useRouter()
|
||||
const utils = inject('utils')
|
||||
const config = useConfig()
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
const tableRef = ref()
|
||||
const selectedRows = ref([])
|
||||
|
||||
// 高级搜索
|
||||
const advSearch = ref(false)
|
||||
|
||||
function advSearchSwitch() {
|
||||
advSearch.value = !advSearch.value
|
||||
}
|
||||
|
||||
const pageInfo = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
pages: 1,
|
||||
list: []
|
||||
})
|
||||
|
||||
watch(() => pageInfo.pageNum, () => {
|
||||
// 触发列表查询
|
||||
search()
|
||||
})
|
||||
|
||||
watch(() => pageInfo.pageSize, () => {
|
||||
// 触发列表查询
|
||||
search()
|
||||
})
|
||||
|
||||
|
||||
const searchForm = reactive({
|
||||
kw: null,
|
||||
taskId: null,
|
||||
promptInput: null,
|
||||
taskStatus: null
|
||||
})
|
||||
|
||||
const taskStatusOpts = reactive([
|
||||
{ key: "success", value: "success", label: "taskStatus.success" },
|
||||
{ key: "failed", value: "failed", label: "taskStatus.failed" },
|
||||
{ key: "running", value: "running", label: "taskStatus.running" },
|
||||
{ key: "terminated", value: "terminated", label: "taskStatus.terminated" }
|
||||
])
|
||||
|
||||
const tableColumns = ref([
|
||||
{ prop: "prompt", label: t('promptInput'), isShow: true, showOverflowTooltip: true, minWidth: 300, sortable: true },
|
||||
{ prop: "statusDesc", label: t('taskStatus.name'), isShow: true, width: 160 },
|
||||
{ prop: "createdDt", label: t('createdDt'), isShow: true, width: 160 }
|
||||
])
|
||||
|
||||
const showTableColumns = computed(() => {
|
||||
return tableColumns.value.filter(item => item.isShow)
|
||||
})
|
||||
|
||||
// 任务历史
|
||||
const taskHistory = computed(() => {
|
||||
return config.taskHistory
|
||||
})
|
||||
|
||||
// 基本搜索
|
||||
const baseSearch = utils.debounce(() => {
|
||||
const kw = searchForm.kw
|
||||
utils.clearProps(searchForm)
|
||||
searchForm.kw = kw
|
||||
search()
|
||||
}, 500)
|
||||
|
||||
// 搜索
|
||||
// 修改search方法
|
||||
function search() {
|
||||
searchForm.pageNum = pageInfo.pageNum
|
||||
searchForm.pageSize = pageInfo.pageSize
|
||||
console.log("search searchForm:", searchForm, pageInfo)
|
||||
|
||||
const filteredTaskList = taskHistory.value.filter(taskInfo => {
|
||||
if (utils.notBlank(searchForm.kw)) {
|
||||
if (!taskInfo.prompt.includes(searchForm.kw) && !taskInfo.taskId.includes(searchForm.kw)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (utils.notBlank(searchForm.taskId) && taskInfo.taskId != searchForm.taskId) {
|
||||
return false
|
||||
}
|
||||
if (utils.notBlank(searchForm.promptInput) && !taskInfo.prompt.includes(searchForm.promptInput)) {
|
||||
return false
|
||||
}
|
||||
if (utils.notBlank(searchForm.taskStatus) && taskInfo.status != searchForm.taskStatus) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// 计算总条数
|
||||
pageInfo.total = filteredTaskList.length
|
||||
|
||||
// 分页处理
|
||||
const startIndex = (pageInfo.pageNum - 1) * pageInfo.pageSize
|
||||
const endIndex = startIndex + pageInfo.pageSize
|
||||
pageInfo.list = filteredTaskList.slice(startIndex, endIndex)
|
||||
|
||||
// 任务状态处理
|
||||
pageInfo.list.forEach(item => {
|
||||
item.statusDesc = t('taskStatus.' + item.status)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
const handleSelectionChange = (val) => {
|
||||
selectedRows.value = val
|
||||
}
|
||||
|
||||
// 删除选中的数据
|
||||
function delSelected() {
|
||||
if (selectedRows.value.length == 0) {
|
||||
utils.pop("请选择要删除的数据!")
|
||||
return
|
||||
}
|
||||
selectedRows.value.forEach(item => {
|
||||
for (let i = 0; i < taskHistory.value.length; i++) {
|
||||
if (taskHistory.value[i].taskId == item.taskId) {
|
||||
taskHistory.value.splice(i, 1)
|
||||
i--
|
||||
}
|
||||
}
|
||||
})
|
||||
baseSearch()
|
||||
}
|
||||
|
||||
// 定义变量存储事件监听的引用(不能是常量)
|
||||
let listener = null
|
||||
|
||||
// 在组件挂载时添加事件监听
|
||||
onMounted(() => {
|
||||
listener = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
search()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keyup', listener)
|
||||
console.log("onMounted pageInfo:", pageInfo)
|
||||
search()
|
||||
})
|
||||
|
||||
// 在组件卸载前移除事件监听
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keyup', listener)
|
||||
})
|
||||
|
||||
function checkTableColumn(isCheck, prop) {
|
||||
console.log("checkTableColumn:", isCheck, prop)
|
||||
tableColumns.value.forEach(item => {
|
||||
if (item.prop == prop) {
|
||||
item.isShow = isCheck
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
utils.clearProps(searchForm)
|
||||
searchForm.openStatus = "OPEN"
|
||||
}
|
||||
|
||||
function toTaskInfo(taskId) {
|
||||
console.log("toTaskInfo:", taskId)
|
||||
router.push("/task/"+taskId)
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
407
desktop/frontend/src/views/task/Home.vue
Normal file
407
desktop/frontend/src/views/task/Home.vue
Normal file
@ -0,0 +1,407 @@
|
||||
<template>
|
||||
<div class="main-content fc">
|
||||
<el-scrollbar ref="scrollRef" style="width: 100%;">
|
||||
<div class="output-area" v-show="taskInfo.taskId != null">
|
||||
|
||||
<div class="dialog-user">
|
||||
<div class="blank"></div>
|
||||
<div class="content">
|
||||
<el-text class="title">
|
||||
{{ t('user') }}
|
||||
</el-text>
|
||||
<el-text class="prompt">
|
||||
{{ taskInfo.prompt }}
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-ai">
|
||||
<el-text class="title"> OpenManus-AI </el-text>
|
||||
|
||||
<div class="card-row-wrap">
|
||||
<div class="card-row-aline">
|
||||
<el-timeline class="wp-100">
|
||||
<el-timeline-item v-for="(step, index) in taskInfo.stepList" :key="index" :timestamp="step.createdDt"
|
||||
placement="top">
|
||||
<el-card>
|
||||
<div>
|
||||
<h4 class="color-label mr-10" :class="utils.colorByLabel('step')">
|
||||
STEP
|
||||
</h4>
|
||||
<el-text>{{ step.result }}</el-text>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div v-for="(subStep, subIndex) in step.subList">
|
||||
<div class="fxsb mtb-10">
|
||||
<el-text> {{ subStep.type }} </el-text>
|
||||
<el-text class="sub-step-time"> {{ subStep.createdDt }} </el-text>
|
||||
</div>
|
||||
<div>
|
||||
<el-text> {{ subStep.result }} </el-text>
|
||||
</div>
|
||||
<el-divider v-if="subIndex != step.subList.length - 1" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-text class="pr-10">任务状态:</el-text>
|
||||
<el-text>{{ taskInfo.status }}</el-text>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<div class="input-area">
|
||||
<div class="input-box">
|
||||
<el-icon @click="uploadFile" class="add-file-area" :size="24">
|
||||
<FolderAdd />
|
||||
</el-icon>
|
||||
<el-input ref="promptEle" type="textarea" v-model="prompt" class="input-style" style="border: none;"
|
||||
:autosize="{ minRows: 1, maxRows: 4 }" autofocus placeholder="请输入指令" @keydown.enter="handleInputEnter" />
|
||||
|
||||
<el-link class="send-area">
|
||||
<el-icon @click="sendPrompt" :size="24" v-show="!loading">
|
||||
<Promotion />
|
||||
</el-icon>
|
||||
<el-icon @click="stop" :size="24" v-show="loading">
|
||||
<CircleClose />
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-text class="tips">以上内容由OpenManus生成, 仅供参考和借鉴</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { FolderAdd, Promotion, Eleme, CircleClose } from '@element-plus/icons-vue'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import i18n from '@/locales/i18n'
|
||||
|
||||
const utils = inject('utils')
|
||||
const config = useConfig()
|
||||
const { t } = useI18n()
|
||||
|
||||
const prompt = ref('')
|
||||
const promptEle = ref(null)
|
||||
|
||||
const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message']
|
||||
const eventSource = ref(null)
|
||||
|
||||
const taskInfo = computed(() => {
|
||||
return config.getCurrTask()
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const scrollRef = ref(null)
|
||||
|
||||
// 建立EventSource连接
|
||||
const buildEventSource = (taskId) => {
|
||||
loading.value = true
|
||||
eventSource.value = new EventSource('http://localhost:5172/tasks/' + taskId + '/events')
|
||||
eventSource.value.onmessage = (event) => {
|
||||
console.log('Received data:', event.data)
|
||||
// 在这里处理接收到的数据 不起作用
|
||||
}
|
||||
|
||||
eventTypes.forEach(type => {
|
||||
eventSource.value.addEventListener(type, (event) => handleEvent(event, type))
|
||||
})
|
||||
|
||||
eventSource.value.onerror = (error) => {
|
||||
console.error('EventSource failed:', error)
|
||||
// 处理错误情况
|
||||
loading.value = false
|
||||
eventSource.value.close()
|
||||
taskInfo.value.status = "failed"
|
||||
utils.pop("任务执行失败", "error")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const handleEvent = (event, type) => {
|
||||
console.log('Received event, type:', type, event.data)
|
||||
// clearInterval(heartbeatTimer);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log("type:", type, "data:", data)
|
||||
if (eventSource.value.readyState === EventSource.CLOSED) {
|
||||
console.log('Connection is closed');
|
||||
}
|
||||
if (type == "complete" || data.status == "completed") {
|
||||
console.log('task completed');
|
||||
loading.value = false
|
||||
eventSource.value.close()
|
||||
taskInfo.value.status = "success"
|
||||
utils.pop("任务已完成", "success")
|
||||
return
|
||||
}
|
||||
// autoScroll(stepContainer);
|
||||
buildOutput(taskInfo.value.taskId)
|
||||
} catch (e) {
|
||||
console.error(`Error handling ${type} event:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildOutput(taskId) {
|
||||
// 同步执行,确保数据顺序
|
||||
await utils.awaitGet('http://localhost:5172/tasks/' + taskId).then(data => {
|
||||
console.log("task info resp:", data)
|
||||
buildStepList(data.steps)
|
||||
console.log("stepList:", taskInfo.value.stepList)
|
||||
// 滚动到底部
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
// 封装stepList
|
||||
const buildStepList = (steps) => {
|
||||
// stepList
|
||||
steps.forEach((step, idx) => {
|
||||
// 步骤
|
||||
if (step.type == "log" && step.result.startsWith("Executing step")) {
|
||||
const stepStr = step.result.replace("Executing step ", "").replace("\n", "")
|
||||
const stepNo = stepStr.split("/")[0]
|
||||
if (taskInfo.value.stepList.length < stepNo) {
|
||||
// 添加此step到stepList
|
||||
const parentStep = {
|
||||
type: "log",
|
||||
idx: idx,
|
||||
stepNo: stepNo,
|
||||
result: stepStr,
|
||||
subList: [],
|
||||
createdDt: utils.dateFormat(new Date())
|
||||
}
|
||||
taskInfo.value.stepList.push(parentStep)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 子步骤
|
||||
const subStep = {
|
||||
type: step.type,
|
||||
idx: idx,
|
||||
result: step.result,
|
||||
createdDt: utils.dateFormat(new Date())
|
||||
}
|
||||
// 判定添加到stepList中的哪个元素元素的subList中
|
||||
console.log("stepList:", taskInfo.value.stepList, "idx:", idx)
|
||||
let parentStep = null
|
||||
const pStepIndex = taskInfo.value.stepList.findIndex(parentStep => parentStep.idx > idx)
|
||||
console.log("pStepIndex:", pStepIndex)
|
||||
if (pStepIndex != -1) {
|
||||
// 取pStep的上一个元素
|
||||
parentStep = taskInfo.value.stepList[pStepIndex - 1]
|
||||
} else {
|
||||
// 不存在时, 添加到stepList最后一个元素末尾
|
||||
parentStep = taskInfo.value.stepList[taskInfo.value.stepList.length - 1]
|
||||
}
|
||||
console.log("parentStep:", parentStep)
|
||||
const existSubStep = parentStep.subList.find(existSubStep => existSubStep.idx == idx)
|
||||
if (!existSubStep) {
|
||||
// 不存在时, 添加到末尾
|
||||
parentStep.subList.push(subStep)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
// 组件卸载时关闭EventSource连接
|
||||
if (eventSource.value) {
|
||||
eventSource.value.close()
|
||||
}
|
||||
})
|
||||
|
||||
function handleInputEnter(event) {
|
||||
console.log("handleInputEnter:", event)
|
||||
event.preventDefault()
|
||||
sendPrompt()
|
||||
}
|
||||
|
||||
function uploadFile() {
|
||||
utils.pop("暂不支持,开发中", "warning")
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (scrollRef.value) {
|
||||
console.log("scrollRef:", scrollRef.value, scrollRef.value.wrapRef)
|
||||
const container = scrollRef.value.wrapRef
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送提示词
|
||||
function sendPrompt() {
|
||||
// 关闭之前的连接
|
||||
if (eventSource.value != null) {
|
||||
eventSource.value.close()
|
||||
}
|
||||
|
||||
if (utils.isBlank(prompt.value)) {
|
||||
utils.pop("Please enter a valid prompt", "error")
|
||||
promptEle.value.focus()
|
||||
return
|
||||
}
|
||||
|
||||
utils.post('http://localhost:5172/tasks', { prompt: prompt.value }).then(data => {
|
||||
if (!data.task_id) {
|
||||
throw new Error('Invalid task ID')
|
||||
}
|
||||
const newTask = {
|
||||
taskId: data.task_id,
|
||||
prompt: prompt.value,
|
||||
status: "running",
|
||||
createdDt: utils.dateFormat(new Date()),
|
||||
stepList: []
|
||||
}
|
||||
// 保存历史记录
|
||||
config.addTaskHistory(newTask)
|
||||
// 发送完成后清空输入框
|
||||
prompt.value = ''
|
||||
// 建立新的EventSource连接
|
||||
buildEventSource(data.task_id)
|
||||
|
||||
console.log("new task created:", newTask)
|
||||
}).catch(error => {
|
||||
console.error('Failed to create task:', error)
|
||||
})
|
||||
}
|
||||
|
||||
function stop() {
|
||||
console.log("stop")
|
||||
loading.value = false
|
||||
eventSource.value.close()
|
||||
taskInfo.value.status = "terminated"
|
||||
utils.pop("用户终止任务", "error")
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.output-area {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dialog-user {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dialog-user .blank {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dialog-user .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: end;
|
||||
border-radius: 12px;
|
||||
background-color: var(--el-fg-color);
|
||||
}
|
||||
|
||||
.dialog-user .title {
|
||||
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
|
||||
align-self: flex-end;
|
||||
margin: 6px 16px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.dialog-user .prompt {
|
||||
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
|
||||
align-self: flex-end;
|
||||
margin: 0px 16px 6px 16px;
|
||||
}
|
||||
|
||||
|
||||
.dialog {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.dialog-ai {
|
||||
margin-bottom: 16px;
|
||||
background-color: var(--el-fg-color);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.dialog-ai .title {
|
||||
margin: 6px 12px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
flex-grow: 0;
|
||||
width: 100%;
|
||||
max-height: 180px;
|
||||
padding-left: 80px;
|
||||
padding-right: 80px;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
background-color: var(--el-fg-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-style {
|
||||
width: 100%;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.input-style :deep(.el-textarea__inner) {
|
||||
outline: none;
|
||||
border: none;
|
||||
resize: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.add-file-area {
|
||||
margin-left: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.send-area {
|
||||
margin-left: 8px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.tips {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.sub-step-time {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
469
desktop/frontend/src/views/task/TaskIndex.vue
Normal file
469
desktop/frontend/src/views/task/TaskIndex.vue
Normal file
@ -0,0 +1,469 @@
|
||||
<template>
|
||||
<div class="main-content fc">
|
||||
<el-scrollbar ref="scrollRef">
|
||||
<div class="output-area" v-show="taskInfo.taskId != null">
|
||||
|
||||
<div class="dialog-user">
|
||||
<div class="blank"></div>
|
||||
<div class="content">
|
||||
<div class="title fxc">
|
||||
<img src="@/assets/img/user.png" class="user-img" />
|
||||
<el-text>
|
||||
{{ t('user') }}
|
||||
</el-text>
|
||||
</div>
|
||||
<el-text class="prompt">
|
||||
{{ taskInfo.prompt }}
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-ai">
|
||||
<el-text class="title"> OpenManus-AI </el-text>
|
||||
|
||||
<div class="card-row-wrap">
|
||||
<div class="card-row-aline">
|
||||
<el-timeline class="wp-100">
|
||||
<el-timeline-item v-for="(step, index) in taskInfo.stepList" :key="index" :timestamp="step.createdDt"
|
||||
placement="top">
|
||||
<el-card>
|
||||
<div>
|
||||
<h4 class="color-label mr-10" :class="utils.colorByLabel('step')">
|
||||
{{ t('step') }}
|
||||
</h4>
|
||||
<el-text>{{ step.result }}</el-text>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div v-for="(subStep, subIndex) in step.subList">
|
||||
<div class="fxsb mtb-10">
|
||||
<el-text> {{ subStep.type }} </el-text>
|
||||
<el-text class="sub-step-time"> {{ subStep.createdDt }} </el-text>
|
||||
</div>
|
||||
<div>
|
||||
<el-text> {{ subStep.result }} </el-text>
|
||||
</div>
|
||||
<el-divider v-if="subIndex != step.subList.length - 1" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<div class="input-area">
|
||||
|
||||
<div class="input-tools">
|
||||
<div class="new-task" v-show="!newTaskFlag">
|
||||
<el-button round @click="startNewTask">
|
||||
<el-icon :size="16">
|
||||
<CirclePlus />
|
||||
</el-icon>
|
||||
<span> {{ t('newTask') }} </span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="task-status" v-show="taskInfo.taskId != null">
|
||||
<el-text class="pr-10">{{ t('taskStatus.name') }}:</el-text>
|
||||
<el-text>{{ taskInfo.status }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-box">
|
||||
<el-icon @click="uploadFile" class="add-file-area" :size="24">
|
||||
<FolderAdd />
|
||||
</el-icon>
|
||||
<el-input ref="promptEle" type="textarea" v-model="prompt" class="input-style" style="border: none;"
|
||||
:autosize="{ minRows: 1, maxRows: 4 }" autofocus :placeholder="t('promptInputPlaceHolder')"
|
||||
@keydown.enter="handleInputEnter" />
|
||||
|
||||
<el-link class="send-area">
|
||||
<el-icon @click="sendPrompt" :size="24" v-show="!loading && taskInfo.status != 'running'">
|
||||
<Promotion />
|
||||
</el-icon>
|
||||
<el-icon @click="stop" :size="24" v-show="loading || taskInfo.status == 'running'">
|
||||
<CircleClose />
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-text class="tips">{{ t('openManusAgiTips') }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { FolderAdd, Promotion, CirclePlus, CircleClose } from '@element-plus/icons-vue'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const utils = inject('utils')
|
||||
const config = useConfig()
|
||||
const { t } = useI18n()
|
||||
|
||||
const prompt = ref('')
|
||||
const promptEle = ref(null)
|
||||
|
||||
const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message']
|
||||
const eventSource = ref(null)
|
||||
|
||||
const newTaskFlag = ref(false)
|
||||
|
||||
const taskInfo = computed(() => {
|
||||
if (newTaskFlag.value) {
|
||||
return {}
|
||||
}
|
||||
return config.getCurrTask()
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const scrollRef = ref(null)
|
||||
|
||||
// 建立EventSource连接
|
||||
const buildEventSource = (taskId) => {
|
||||
loading.value = true
|
||||
eventSource.value = new EventSource('http://localhost:5172/tasks/' + taskId + '/events')
|
||||
eventSource.value.onmessage = (event) => {
|
||||
console.log('Received data:', event.data)
|
||||
// 在这里处理接收到的数据 不起作用
|
||||
}
|
||||
|
||||
eventTypes.forEach(type => {
|
||||
eventSource.value.addEventListener(type, (event) => handleEvent(event, type))
|
||||
})
|
||||
|
||||
eventSource.value.onerror = (error) => {
|
||||
console.error('EventSource failed:', error)
|
||||
// 处理错误情况
|
||||
loading.value = false
|
||||
eventSource.value.close()
|
||||
taskInfo.value.status = "failed"
|
||||
utils.pop("任务执行失败", "error")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const handleEvent = (event, type) => {
|
||||
console.log('Received event, type:', type, event.data)
|
||||
// clearInterval(heartbeatTimer);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log("type:", type, "data:", data)
|
||||
if (eventSource.value.readyState === EventSource.CLOSED) {
|
||||
console.log('Connection is closed');
|
||||
}
|
||||
if (type == "complete" || data.status == "completed") {
|
||||
console.log('task completed');
|
||||
loading.value = false
|
||||
eventSource.value.close()
|
||||
taskInfo.value.status = "success"
|
||||
utils.pop("任务已完成", "success")
|
||||
return
|
||||
}
|
||||
// autoScroll(stepContainer);
|
||||
buildOutput(taskInfo.value.taskId)
|
||||
} catch (e) {
|
||||
console.error(`Error handling ${type} event:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildOutput(taskId) {
|
||||
// 同步执行,确保数据顺序
|
||||
await utils.awaitGet('http://localhost:5172/tasks/' + taskId).then(data => {
|
||||
console.log("task info resp:", data)
|
||||
buildStepList(data.steps)
|
||||
console.log("stepList:", taskInfo.value.stepList)
|
||||
// 滚动到底部
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
// 封装stepList
|
||||
const buildStepList = (steps) => {
|
||||
// stepList
|
||||
steps.forEach((step, idx) => {
|
||||
// 步骤
|
||||
if (step.type == "log" && step.result.startsWith("Executing step")) {
|
||||
const stepStr = step.result.replace("Executing step ", "").replace("\n", "")
|
||||
const stepNo = stepStr.split("/")[0]
|
||||
if (taskInfo.value.stepList.length < stepNo) {
|
||||
// 添加此step到stepList
|
||||
const parentStep = {
|
||||
type: "log",
|
||||
idx: idx,
|
||||
stepNo: stepNo,
|
||||
result: stepStr,
|
||||
subList: [],
|
||||
createdDt: utils.dateFormat(new Date())
|
||||
}
|
||||
taskInfo.value.stepList.push(parentStep)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 子步骤
|
||||
const subStep = {
|
||||
type: step.type,
|
||||
idx: idx,
|
||||
result: step.result,
|
||||
createdDt: utils.dateFormat(new Date())
|
||||
}
|
||||
// 判定添加到stepList中的哪个元素元素的subList中
|
||||
console.log("stepList:", taskInfo.value.stepList, "idx:", idx)
|
||||
let parentStep = null
|
||||
const pStepIndex = taskInfo.value.stepList.findIndex(parentStep => parentStep.idx > idx)
|
||||
console.log("pStepIndex:", pStepIndex)
|
||||
if (pStepIndex != -1) {
|
||||
// 取pStep的上一个元素
|
||||
parentStep = taskInfo.value.stepList[pStepIndex - 1]
|
||||
} else {
|
||||
// 不存在时, 添加到stepList最后一个元素末尾
|
||||
parentStep = taskInfo.value.stepList[taskInfo.value.stepList.length - 1]
|
||||
}
|
||||
console.log("parentStep:", parentStep)
|
||||
const existSubStep = parentStep.subList.find(existSubStep => existSubStep.idx == idx)
|
||||
if (!existSubStep) {
|
||||
// 不存在时, 添加到末尾
|
||||
parentStep.subList.push(subStep)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
// 组件卸载时关闭EventSource连接
|
||||
if (eventSource.value) {
|
||||
eventSource.value.close()
|
||||
}
|
||||
})
|
||||
|
||||
function handleInputEnter(event) {
|
||||
console.log("handleInputEnter:", event)
|
||||
event.preventDefault()
|
||||
sendPrompt()
|
||||
}
|
||||
|
||||
function uploadFile() {
|
||||
utils.pop("暂不支持,开发中", "warning")
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (scrollRef.value) {
|
||||
console.log("scrollRef:", scrollRef.value, scrollRef.value.wrapRef)
|
||||
const container = scrollRef.value.wrapRef
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送提示词
|
||||
function sendPrompt() {
|
||||
if (utils.isBlank(prompt.value)) {
|
||||
utils.pop("Please enter a valid prompt", "error")
|
||||
promptEle.value.focus()
|
||||
return
|
||||
}
|
||||
|
||||
if (taskInfo.value.status == "running") {
|
||||
utils.pop("请先终止当前任务", "error")
|
||||
return
|
||||
}
|
||||
|
||||
// 关闭之前的连接
|
||||
if (eventSource.value != null) {
|
||||
eventSource.value.close()
|
||||
}
|
||||
|
||||
utils.post('http://localhost:5172/tasks', { prompt: prompt.value }).then(data => {
|
||||
if (!data.task_id) {
|
||||
throw new Error('Invalid task ID')
|
||||
}
|
||||
const newTask = {
|
||||
taskId: data.task_id,
|
||||
prompt: prompt.value,
|
||||
status: "running",
|
||||
createdDt: utils.dateFormat(new Date()),
|
||||
stepList: []
|
||||
}
|
||||
// 保存历史记录
|
||||
config.addTaskHistory(newTask)
|
||||
newTaskFlag.value = false
|
||||
// 发送完成后清空输入框
|
||||
prompt.value = ''
|
||||
// 建立新的EventSource连接
|
||||
buildEventSource(data.task_id)
|
||||
|
||||
console.log("new task created:", newTask)
|
||||
}).catch(error => {
|
||||
console.error('Failed to create task:', error)
|
||||
})
|
||||
}
|
||||
|
||||
function stop() {
|
||||
console.log("stop")
|
||||
loading.value = false
|
||||
console.log("eventSource:", eventSource.value, "taskInfo:", taskInfo.value)
|
||||
if (eventSource.value != null) {
|
||||
eventSource.value.close()
|
||||
}
|
||||
|
||||
taskInfo.value.status = "terminated"
|
||||
utils.pop("用户终止任务", "error")
|
||||
}
|
||||
|
||||
function startNewTask() {
|
||||
console.log("startNewTask:", taskInfo.value)
|
||||
if (taskInfo.value.status == "running") {
|
||||
utils.pop("请先终止当前任务", "error")
|
||||
return
|
||||
}
|
||||
newTaskFlag.value = true
|
||||
prompt.value = ''
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.output-area {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dialog-user {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dialog-user .blank {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dialog-user .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: end;
|
||||
border-radius: 12px;
|
||||
background-color: var(--el-fg-color);
|
||||
}
|
||||
|
||||
.dialog-user .title {
|
||||
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
|
||||
align-self: flex-end;
|
||||
margin: 6px 16px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.dialog-user .prompt {
|
||||
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
|
||||
align-self: flex-end;
|
||||
margin: 0px 16px 6px 16px;
|
||||
}
|
||||
|
||||
.dialog-user .user-img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-ai {
|
||||
background-color: var(--el-fg-color);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.dialog-ai .title {
|
||||
margin: 6px 12px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
flex-grow: 0;
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
padding-left: 80px;
|
||||
padding-right: 80px;
|
||||
padding-top: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-area .input-tools {
|
||||
width: 100%;
|
||||
padding-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-area .input-tools .new-task {
|
||||
position: relative;
|
||||
left: -80px;
|
||||
}
|
||||
|
||||
.input-area .input-tools .task-status {
|
||||
position: relative;
|
||||
right: -80px;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
background-color: var(--el-fg-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-style {
|
||||
width: 100%;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.input-style :deep(.el-textarea__inner) {
|
||||
outline: none;
|
||||
border: none;
|
||||
resize: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.add-file-area {
|
||||
margin-left: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.send-area {
|
||||
margin-left: 8px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.tips {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.sub-step-time {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
167
desktop/frontend/src/views/task/TaskInfo.vue
Normal file
167
desktop/frontend/src/views/task/TaskInfo.vue
Normal file
@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div class="main-content fc">
|
||||
<!-- 展示模块-暂无数据 -->
|
||||
<div class="no-data" v-show="baseNoData">{{ t('noData') }}</div>
|
||||
|
||||
<!-- 展示模块 -->
|
||||
<div class="output-area" v-show="baseShow">
|
||||
|
||||
<div class="dialog-user">
|
||||
<div class="blank"></div>
|
||||
<div class="content">
|
||||
<div class="title fxc">
|
||||
<img src="@/assets/img/user.png" class="user-img" />
|
||||
<el-text>
|
||||
{{ t('user') }}
|
||||
</el-text>
|
||||
</div>
|
||||
<el-text class="prompt">
|
||||
{{ taskInfo.prompt }}
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-ai">
|
||||
<el-text class="title"> OpenManus-AI </el-text>
|
||||
|
||||
<div class="card-row-wrap">
|
||||
<div class="card-row-aline">
|
||||
<el-timeline class="wp-100">
|
||||
<el-timeline-item v-for="(step, index) in taskInfo.stepList" :key="index" :timestamp="step.createdDt"
|
||||
placement="top">
|
||||
<el-card>
|
||||
<div>
|
||||
<h4 class="color-label mr-10" :class="utils.colorByLabel('step')">
|
||||
{{ t('step') }}
|
||||
</h4>
|
||||
<el-text>{{ step.result }}</el-text>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div v-for="(subStep, subIndex) in step.subList">
|
||||
<div class="fxsb mtb-10">
|
||||
<el-text> {{ subStep.type }} </el-text>
|
||||
<el-text class="sub-step-time"> {{ subStep.createdDt }} </el-text>
|
||||
</div>
|
||||
<div>
|
||||
<el-text> {{ subStep.result }} </el-text>
|
||||
</div>
|
||||
<el-divider v-if="subIndex != step.subList.length - 1" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-status" v-show="taskInfo != null">
|
||||
<el-text class="pr-10">{{ t('taskStatus.name') }}:</el-text>
|
||||
<el-text>{{ taskInfo.status }}</el-text>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { User } from '@element-plus/icons-vue'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const utils = inject('utils')
|
||||
const config = useConfig()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 视图模式
|
||||
const viewModel = reactive({
|
||||
base: 'show'
|
||||
})
|
||||
|
||||
const baseShow = computed(() => {
|
||||
return viewModel.base == 'show'
|
||||
})
|
||||
|
||||
const baseNoData = computed(() => {
|
||||
return baseShow && taskInfo.value == null
|
||||
})
|
||||
|
||||
const taskInfo = computed(() => {
|
||||
return config.getCurrTask()
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.output-area {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dialog-user {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dialog-user .blank {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dialog-user .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: end;
|
||||
border-radius: 12px;
|
||||
background-color: var(--el-fg-color);
|
||||
}
|
||||
|
||||
.dialog-user .title {
|
||||
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
|
||||
align-self: flex-end;
|
||||
margin: 6px 16px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.dialog-user .prompt {
|
||||
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
|
||||
align-self: flex-end;
|
||||
margin: 0px 16px 6px 16px;
|
||||
}
|
||||
|
||||
.dialog-user .user-img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-ai {
|
||||
background-color: var(--el-fg-color);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.dialog-ai .title {
|
||||
margin: 6px 12px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.task-status {
|
||||
align-self: self-start;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.sub-step-time {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
54
desktop/frontend/vite.config.js
Normal file
54
desktop/frontend/vite.config.js
Normal file
@ -0,0 +1,54 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
terser()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8020',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/\/api/, ''),
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1500,
|
||||
// Fine-tune bundling strategy
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules')) {
|
||||
// Extract package name from module path to create separate chunks
|
||||
return id.toString().split('node_modules/')[1].split('/')[0].toString()
|
||||
}
|
||||
},
|
||||
// Attempt to merge chunks smaller than 10KB (in bytes)
|
||||
experimentalMinChunkSize: 10 * 1024,
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
6
desktop/frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
6
desktop/frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function Greet(arg1:string):Promise<string>;
|
||||
|
||||
export function ReadAll(arg1:string):Promise<string>;
|
11
desktop/frontend/wailsjs/go/main/App.js
Normal file
11
desktop/frontend/wailsjs/go/main/App.js
Normal file
@ -0,0 +1,11 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
||||
export function ReadAll(arg1) {
|
||||
return window['go']['main']['App']['ReadAll'](arg1);
|
||||
}
|
24
desktop/frontend/wailsjs/runtime/package.json
Normal file
24
desktop/frontend/wailsjs/runtime/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
249
desktop/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
249
desktop/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@ -0,0 +1,249 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
238
desktop/frontend/wailsjs/runtime/runtime.js
Normal file
238
desktop/frontend/wailsjs/runtime/runtime.js
Normal file
@ -0,0 +1,238 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
38
desktop/go.mod
Normal file
38
desktop/go.mod
Normal file
@ -0,0 +1,38 @@
|
||||
module OpenManus
|
||||
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require github.com/wailsapp/wails/v2 v2.10.1
|
||||
|
||||
require (
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.19 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
|
||||
// replace github.com/wailsapp/wails/v2 v2.9.2 => C:\Users\aylvn\go\pkg\mod
|
79
desktop/go.sum
Normal file
79
desktop/go.sum
Normal file
@ -0,0 +1,79 @@
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
|
||||
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
|
||||
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
45
desktop/main.go
Normal file
45
desktop/main.go
Normal file
@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
// Create an instance of the app structure
|
||||
app := NewApp()
|
||||
|
||||
/* AppMenu := menu.NewMenu()
|
||||
FileMenu := AppMenu.AddSubmenu("File")
|
||||
FileMenu.AddText("&Open", keys.CmdOrCtrl("o"), nil)
|
||||
FileMenu.AddSeparator()
|
||||
FileMenu.AddText("Quit", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
|
||||
runtime.Quit(app.ctx)
|
||||
}) */
|
||||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "OpenManus",
|
||||
Width: 1024,
|
||||
Height: 768,
|
||||
// Menu: AppMenu,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: app.startup,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
29
desktop/src/utils/file.go
Normal file
29
desktop/src/utils/file.go
Normal file
@ -0,0 +1,29 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// 打开文件
|
||||
func ReadAll(filePath string) []byte {
|
||||
if IsBlank(filePath) {
|
||||
fmt.Println("File path is nil")
|
||||
return nil
|
||||
}
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
fmt.Println("Error opening file:", err)
|
||||
return nil
|
||||
}
|
||||
// 确保文件最后被关闭
|
||||
defer file.Close()
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
fmt.Println("Read file error:", err)
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
232
desktop/src/utils/http.go
Normal file
232
desktop/src/utils/http.go
Normal file
@ -0,0 +1,232 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var host = "http://localhost:8020"
|
||||
|
||||
type Body struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// 结构体中的变量命名首字母大写,不然不能被外部访问到
|
||||
type Resp struct {
|
||||
Code int `json:"code"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func buildResp(resp *http.Response, err error) Resp {
|
||||
// 确保在函数退出时关闭resp的主体
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 打印请求结果
|
||||
Logf("Http Resp: %v, err: %v\n", resp, err)
|
||||
if err != nil {
|
||||
// 网络请求处理错误
|
||||
Log("Network Error: ", err)
|
||||
return Resp{resp.StatusCode, err.Error()}
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
// 网络请求状态码异常
|
||||
Log("Http Resp Status Code Error: ", resp)
|
||||
return Resp{resp.StatusCode, resp.Status}
|
||||
}
|
||||
|
||||
// 读取响应体
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
Log("Error Reading Response Body: ", err)
|
||||
return Resp{202, err.Error()}
|
||||
}
|
||||
|
||||
// 打印响应内容
|
||||
Log("Http Response Body: ", string(body))
|
||||
// 判断返回内容类型
|
||||
// Log(resp)
|
||||
Logf("Content-Type=%v", resp.Header.Get("Content-Type"))
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
// 处理返回响应状态和内容
|
||||
var bodySt Body
|
||||
if !strings.HasPrefix(contentType, "application/json") {
|
||||
// 非json,直接返回
|
||||
return Resp{resp.StatusCode, string(body)}
|
||||
}
|
||||
|
||||
// 序列化后返回
|
||||
err = json.Unmarshal(body, &bodySt)
|
||||
if err != nil {
|
||||
Log("Parse Body Json Faild: ", err)
|
||||
return Resp{202, err.Error()}
|
||||
}
|
||||
if bodySt.Code != 200 {
|
||||
return Resp{bodySt.Code, bodySt.Msg}
|
||||
}
|
||||
return Resp{bodySt.Code, bodySt.Data}
|
||||
}
|
||||
|
||||
func buildReqUrl(uri string) string {
|
||||
if strings.HasPrefix(uri, "http") {
|
||||
return uri
|
||||
}
|
||||
return host + uri
|
||||
}
|
||||
|
||||
// Go http请求 https://www.cnblogs.com/Xinenhui/p/17496684.html
|
||||
// Get
|
||||
func Get(uri string, param map[string]interface{}, header map[string]string) Resp {
|
||||
Logf("Get Uri: %s, Param: %s, Header: %s\n", uri, param, header)
|
||||
apiUrl := buildReqUrl(uri)
|
||||
|
||||
//新建一个GET请求
|
||||
req, err := http.NewRequest("GET", apiUrl, nil)
|
||||
if err != nil {
|
||||
return Resp{202, err.Error()}
|
||||
}
|
||||
|
||||
// 请求头部信息
|
||||
// Set时候,如果原来这一项已存在,后面的就修改已有的
|
||||
// Add时候,如果原本不存在,则添加,如果已存在,就不做任何修改
|
||||
for k, v := range header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
// url参数处理
|
||||
q := req.URL.Query()
|
||||
for k, v := range param {
|
||||
strOfV, err := AnyToStr(v)
|
||||
if err != nil {
|
||||
return Resp{202, err.Error()}
|
||||
}
|
||||
q.Set(k, strOfV)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
// 发送请求给服务端,实例化一个客户端
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return Resp{202, err.Error()}
|
||||
}
|
||||
return buildResp(resp, err)
|
||||
}
|
||||
|
||||
// Post Json
|
||||
func Post(uri string, param map[string]interface{}, header map[string]string) Resp {
|
||||
Logf("Post Json Uri: %s, Param: %s, Header: %s\n", uri, param, header)
|
||||
apiUrl := buildReqUrl(uri)
|
||||
|
||||
// Json参数处理
|
||||
jsonStr, err := json.Marshal(param)
|
||||
if err != nil {
|
||||
Log("Error Marshalling Map To JSON: ", err)
|
||||
return Resp{202, err.Error()}
|
||||
}
|
||||
Logf("Post Json Body Payload: %s\n", string(jsonStr))
|
||||
|
||||
// 新建一个POST请求
|
||||
req, err := http.NewRequest("POST", apiUrl, strings.NewReader(string(jsonStr)))
|
||||
if err != nil {
|
||||
return Resp{202, err.Error()}
|
||||
}
|
||||
// 请求头部信息
|
||||
// Set时候,如果原来这一项已存在,后面的就修改已有的
|
||||
// Add时候,如果原本不存在,则添加,如果已存在,就不做任何修改
|
||||
for k, v := range header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
// Post Json表单请求头
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
//发送请求给服务端,实例化一个客户端
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return Resp{202, err.Error()}
|
||||
}
|
||||
return buildResp(resp, err)
|
||||
}
|
||||
|
||||
// Post Form
|
||||
func PostForm(uri string, param map[string]interface{}, header map[string]string) Resp {
|
||||
Logf("Post Form Uri: %s, Param: %s\n", uri, param)
|
||||
apiUrl := buildReqUrl(uri)
|
||||
|
||||
// PostForm参数处理
|
||||
urlMap := url.Values{}
|
||||
for k, v := range param {
|
||||
strOfV, err := AnyToStr(v)
|
||||
if err != nil {
|
||||
return Resp{202, err.Error()}
|
||||
}
|
||||
urlMap.Set(k, strOfV)
|
||||
}
|
||||
|
||||
Logf("Post Form Body Payload: %s\n", urlMap.Encode())
|
||||
|
||||
// 新建一个POST请求
|
||||
req, err := http.NewRequest("POST", apiUrl, strings.NewReader(urlMap.Encode()))
|
||||
if err != nil {
|
||||
return Resp{202, err.Error()}
|
||||
}
|
||||
// 请求头部信息
|
||||
// Set时候,如果原来这一项已存在,后面的就修改已有的
|
||||
// Add时候,如果原本不存在,则添加,如果已存在,就不做任何修改
|
||||
for k, v := range header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
// Post FormData表单请求头
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
//发送请求给服务端,实例化一个客户端
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return Resp{202, err.Error()}
|
||||
}
|
||||
return buildResp(resp, err)
|
||||
}
|
||||
|
||||
// Del
|
||||
func Del(uri string, param map[string]interface{}, header map[string]string) Resp {
|
||||
Logf("Del Uri: %s, Param: %s\n", uri, param)
|
||||
apiUrl := buildReqUrl(uri)
|
||||
|
||||
//新建一个Del请求
|
||||
req, err := http.NewRequest("DELETE", apiUrl, nil)
|
||||
if err != nil {
|
||||
return Resp{202, err.Error()}
|
||||
}
|
||||
|
||||
// 请求头部信息
|
||||
// Set时候,如果原来这一项已存在,后面的就修改已有的
|
||||
// Add时候,如果原本不存在,则添加,如果已存在,就不做任何修改
|
||||
for k, v := range header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
// url参数处理
|
||||
q := req.URL.Query()
|
||||
for k, v := range param {
|
||||
strOfV, err := AnyToStr(v)
|
||||
if err != nil {
|
||||
return Resp{202, err.Error()}
|
||||
}
|
||||
q.Set(k, strOfV)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
// 发送请求给服务端,实例化一个客户端
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return Resp{202, err.Error()}
|
||||
}
|
||||
return buildResp(resp, err)
|
||||
}
|
40
desktop/src/utils/log.go
Normal file
40
desktop/src/utils/log.go
Normal file
@ -0,0 +1,40 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 保存日志到文件
|
||||
func Log(v ...any) {
|
||||
// 打开文件
|
||||
file, err := os.OpenFile("wails.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
// 设置logger
|
||||
logToFile := log.New(file, "WailsLog: ", log.LstdFlags)
|
||||
log.Println(v...)
|
||||
// 写入日志
|
||||
logToFile.Println(v...)
|
||||
}
|
||||
|
||||
// 保存日志到文件
|
||||
func Logf(format string, v ...any) {
|
||||
if !strings.HasSuffix(format, "\n") {
|
||||
format = format + "\n"
|
||||
}
|
||||
// 打开文件
|
||||
file, err := os.OpenFile("wails.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
// 设置logger
|
||||
logToFile := log.New(file, "WailsLog: ", log.LstdFlags)
|
||||
log.Printf(format, v...)
|
||||
// 写入日志
|
||||
logToFile.Printf(format, v...)
|
||||
}
|
64
desktop/src/utils/str.go
Normal file
64
desktop/src/utils/str.go
Normal file
@ -0,0 +1,64 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AnyToStr 任意类型数据转string
|
||||
func AnyToStr(i interface{}) (string, error) {
|
||||
if i == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(i)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return "", nil
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.String:
|
||||
return v.String(), nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return strconv.FormatInt(v.Int(), 10), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return strconv.FormatUint(v.Uint(), 10), nil
|
||||
case reflect.Float32:
|
||||
return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil
|
||||
case reflect.Float64:
|
||||
return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil
|
||||
case reflect.Complex64:
|
||||
return fmt.Sprintf("(%g+%gi)", real(v.Complex()), imag(v.Complex())), nil
|
||||
case reflect.Complex128:
|
||||
return fmt.Sprintf("(%g+%gi)", real(v.Complex()), imag(v.Complex())), nil
|
||||
case reflect.Bool:
|
||||
return strconv.FormatBool(v.Bool()), nil
|
||||
case reflect.Slice, reflect.Map, reflect.Struct, reflect.Array:
|
||||
str, _ := json.Marshal(i)
|
||||
return string(str), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unable to cast %#v of type %T to string", i, i)
|
||||
}
|
||||
}
|
||||
|
||||
func IsEmpty(s string) bool {
|
||||
return len(s) == 0
|
||||
}
|
||||
|
||||
func IsNotEmpty(s string) bool {
|
||||
return len(s) > 0
|
||||
}
|
||||
|
||||
func IsBlank(s string) bool {
|
||||
return len(s) == 0 || strings.TrimSpace(s) == ""
|
||||
}
|
||||
|
||||
func IsNotBlank(s string) bool {
|
||||
return len(s) > 0 && strings.TrimSpace(s) != ""
|
||||
}
|
33
desktop/wails-build.txt
Normal file
33
desktop/wails-build.txt
Normal file
@ -0,0 +1,33 @@
|
||||
wails官网: https://wails.io/
|
||||
go环境下载: https://go.dev/dl/
|
||||
|
||||
https://blog.csdn.net/jankin6/article/details/140087959
|
||||
|
||||
go env -w GOPROXY=https://goproxy.cn
|
||||
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
|
||||
wails doctor
|
||||
|
||||
D:
|
||||
mkdir VsCodeProjects
|
||||
cd VsCodeProjects
|
||||
// 初始化创建项目
|
||||
wails init -n OpenManus -t vue
|
||||
|
||||
// 进入项目并运行
|
||||
cd .\OpenManus
|
||||
wails dev
|
||||
|
||||
// 构建应用
|
||||
wails build
|
||||
|
||||
|
||||
// 安装工具库 frontend包下
|
||||
|
||||
cd D:\VsCodeProjects\OpenManus\frontend
|
||||
npm install vue-router@latest
|
||||
npm i pinia
|
||||
pnpm i pinia-plugin-persistedstate
|
||||
npm install axios
|
||||
npm install qs
|
||||
npm i --save-dev @types/qs
|
13
desktop/wails.json
Normal file
13
desktop/wails.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "OpenManus",
|
||||
"outputfilename": "OpenManus",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
"frontend:dev:watcher": "npm run dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"author": {
|
||||
"name": "aylvn",
|
||||
"email": "aylvn@sina.com"
|
||||
}
|
||||
}
|
41
run.bat
41
run.bat
@ -1,3 +1,42 @@
|
||||
@echo off
|
||||
venv\Scripts\python.exe app.py
|
||||
setlocal
|
||||
cd /d %~dp0
|
||||
|
||||
set "VENV_DIR=%~dp0venv"
|
||||
set "PYTHON_PATH=%VENV_DIR%\python.exe"
|
||||
|
||||
where git >nul 2>&1
|
||||
if %errorlevel% == 0 (
|
||||
echo Trying to sync with GitHub repository...
|
||||
git pull origin front-end 2>&1 || echo Failed to sync with GitHub, skipping update...
|
||||
) else (
|
||||
echo Git not detected, skipping code synchronization
|
||||
)
|
||||
|
||||
if not exist "%VENV_DIR%\" (
|
||||
echo Virtual environment not found, initializing installation...
|
||||
python -m venv "%VENV_DIR%" || (
|
||||
echo Failed to create virtual environment, please install Python 3.12 first
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
call "%VENV_DIR%\Scripts\activate.bat"
|
||||
pip install -r requirements.txt || (
|
||||
echo Dependency installation failed, please check requirements. txt
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo Starting Python application...
|
||||
if not exist "%PYTHON_PATH%" (
|
||||
echo Error: Python executable file does not exist in %PYTHON_PATH%
|
||||
echo Please try deleting the venv folder and running the script again
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
"%PYTHON_PATH%" "%~dp0app.py"
|
||||
|
||||
pause
|
||||
endlocal
|
||||
|
327
static/main.js
327
static/main.js
@ -38,6 +38,7 @@ function createTask() {
|
||||
}
|
||||
setupSSE(data.task_id);
|
||||
loadHistory();
|
||||
promptInput.value = '';
|
||||
})
|
||||
.catch(error => {
|
||||
container.innerHTML = `<div class="error">Error: ${error.message}</div>`;
|
||||
@ -49,6 +50,7 @@ function setupSSE(taskId) {
|
||||
let retryCount = 0;
|
||||
const maxRetries = 3;
|
||||
const retryDelay = 2000;
|
||||
let lastResultContent = '';
|
||||
|
||||
const container = document.getElementById('task-container');
|
||||
|
||||
@ -60,16 +62,15 @@ function setupSSE(taskId) {
|
||||
container.innerHTML += '<div class="ping">·</div>';
|
||||
}, 5000);
|
||||
|
||||
const pollInterval = setInterval(() => {
|
||||
fetch(`/tasks/${taskId}`)
|
||||
.then(response => response.json())
|
||||
.then(task => {
|
||||
updateTaskStatus(task);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Polling failed:', error);
|
||||
});
|
||||
}, 10000);
|
||||
// Initial polling
|
||||
fetch(`/tasks/${taskId}`)
|
||||
.then(response => response.json())
|
||||
.then(task => {
|
||||
updateTaskStatus(task);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Initial status fetch failed:', error);
|
||||
});
|
||||
|
||||
const handleEvent = (event, type) => {
|
||||
clearInterval(heartbeatTimer);
|
||||
@ -105,20 +106,35 @@ function setupSSE(taskId) {
|
||||
|
||||
eventSource.addEventListener('complete', (event) => {
|
||||
clearInterval(heartbeatTimer);
|
||||
clearInterval(pollInterval);
|
||||
container.innerHTML += `
|
||||
<div class="complete">
|
||||
<div>✅ Task completed</div>
|
||||
<pre>${lastResultContent}</pre>
|
||||
</div>
|
||||
`;
|
||||
eventSource.close();
|
||||
currentEventSource = null;
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
lastResultContent = data.result || '';
|
||||
|
||||
container.innerHTML += `
|
||||
<div class="complete">
|
||||
<div>✅ Task completed</div>
|
||||
<pre>${lastResultContent}</pre>
|
||||
</div>
|
||||
`;
|
||||
|
||||
fetch(`/tasks/${taskId}`)
|
||||
.then(response => response.json())
|
||||
.then(task => {
|
||||
updateTaskStatus(task);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Final status update failed:', error);
|
||||
});
|
||||
|
||||
eventSource.close();
|
||||
currentEventSource = null;
|
||||
} catch (e) {
|
||||
console.error('Error handling complete event:', e);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener('error', (event) => {
|
||||
clearInterval(heartbeatTimer);
|
||||
clearInterval(pollInterval);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
container.innerHTML += `
|
||||
@ -138,24 +154,49 @@ function setupSSE(taskId) {
|
||||
|
||||
console.error('SSE connection error:', err);
|
||||
clearInterval(heartbeatTimer);
|
||||
clearInterval(pollInterval);
|
||||
eventSource.close();
|
||||
|
||||
if (retryCount < maxRetries) {
|
||||
retryCount++;
|
||||
container.innerHTML += `
|
||||
<div class="warning">
|
||||
⚠ Connection lost, retrying in ${retryDelay/1000} seconds (${retryCount}/${maxRetries})...
|
||||
</div>
|
||||
`;
|
||||
setTimeout(connect, retryDelay);
|
||||
} else {
|
||||
container.innerHTML += `
|
||||
<div class="error">
|
||||
⚠ Connection lost, please try refreshing the page
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
fetch(`/tasks/${taskId}`)
|
||||
.then(response => response.json())
|
||||
.then(task => {
|
||||
if (task.status === 'completed' || task.status === 'failed') {
|
||||
updateTaskStatus(task);
|
||||
if (task.status === 'completed') {
|
||||
container.innerHTML += `
|
||||
<div class="complete">
|
||||
<div>✅ Task completed</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
container.innerHTML += `
|
||||
<div class="error">
|
||||
❌ Error: ${task.error || 'Task failed'}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else if (retryCount < maxRetries) {
|
||||
retryCount++;
|
||||
container.innerHTML += `
|
||||
<div class="warning">
|
||||
⚠ Connection lost, retrying in ${retryDelay/1000} seconds (${retryCount}/${maxRetries})...
|
||||
</div>
|
||||
`;
|
||||
setTimeout(connect, retryDelay);
|
||||
} else {
|
||||
container.innerHTML += `
|
||||
<div class="error">
|
||||
⚠ Connection lost, please try refreshing the page
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Task status check failed:', error);
|
||||
if (retryCount < maxRetries) {
|
||||
retryCount++;
|
||||
setTimeout(connect, retryDelay);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@ -167,7 +208,7 @@ function loadHistory() {
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error(`请求失败: ${response.status} - ${text.substring(0, 100)}`);
|
||||
throw new Error(`request failure: ${response.status} - ${text.substring(0, 100)}`);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
@ -180,16 +221,16 @@ function loadHistory() {
|
||||
<div class="task-meta">
|
||||
${new Date(task.created_at).toLocaleString()} -
|
||||
<span class="status status-${task.status ? task.status.toLowerCase() : 'unknown'}">
|
||||
${task.status || '未知状态'}
|
||||
${task.status || 'Unknown state'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载历史记录失败:', error);
|
||||
console.error('Failed to load history records:', error);
|
||||
const listContainer = document.getElementById('task-list');
|
||||
listContainer.innerHTML = `<div class="error">加载失败: ${error.message}</div>`;
|
||||
listContainer.innerHTML = `<div class="error">Load Fail: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
@ -212,13 +253,88 @@ function formatStepContent(data, eventType) {
|
||||
|
||||
function createStepElement(type, content, timestamp) {
|
||||
const step = document.createElement('div');
|
||||
step.className = `step-item ${type}`;
|
||||
step.innerHTML = `
|
||||
<div class="log-line">
|
||||
<span class="log-prefix">${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:</span>
|
||||
<pre>${content}</pre>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Executing step
|
||||
const stepRegex = /Executing step (\d+)\/(\d+)/;
|
||||
if (type === 'log' && stepRegex.test(content)) {
|
||||
const match = content.match(stepRegex);
|
||||
const currentStep = parseInt(match[1]);
|
||||
const totalSteps = parseInt(match[2]);
|
||||
|
||||
step.className = 'step-divider';
|
||||
step.innerHTML = `
|
||||
<div class="step-circle">${currentStep}</div>
|
||||
<div class="step-line"></div>
|
||||
<div class="step-info">${currentStep}/${totalSteps}</div>
|
||||
`;
|
||||
} else if (type === 'act') {
|
||||
// Check if it contains information about file saving
|
||||
const saveRegex = /Content successfully saved to (.+)/;
|
||||
const match = content.match(saveRegex);
|
||||
|
||||
step.className = `step-item ${type}`;
|
||||
|
||||
if (match && match[1]) {
|
||||
const filePath = match[1].trim();
|
||||
const fileName = filePath.split('/').pop();
|
||||
const fileExtension = fileName.split('.').pop().toLowerCase();
|
||||
|
||||
// Handling different types of files
|
||||
let fileInteractionHtml = '';
|
||||
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'].includes(fileExtension)) {
|
||||
fileInteractionHtml = `
|
||||
<div class="file-interaction image-preview">
|
||||
<img src="${filePath}" alt="${fileName}" class="preview-image" onclick="showFullImage('${filePath}')">
|
||||
<a href="/download?file_path=${filePath}" download="${fileName}" class="download-link">⬇️ 下载图片</a>
|
||||
</div>
|
||||
`;
|
||||
} else if (['mp3', 'wav', 'ogg'].includes(fileExtension)) {
|
||||
fileInteractionHtml = `
|
||||
<div class="file-interaction audio-player">
|
||||
<audio controls src="${filePath}"></audio>
|
||||
<a href="/download?file_path=${filePath}" download="${fileName}" class="download-link">⬇️ 下载音频</a>
|
||||
</div>
|
||||
`;
|
||||
} else if (['html', 'js', 'py'].includes(fileExtension)) {
|
||||
fileInteractionHtml = `
|
||||
<div class="file-interaction code-file">
|
||||
<button onclick="simulateRunPython('${filePath}')" class="run-button">▶️ 模拟运行</button>
|
||||
<a href="/download?file_path=${filePath}" download="${fileName}" class="download-link">⬇️ 下载文件</a>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
fileInteractionHtml = `
|
||||
<div class="file-interaction">
|
||||
<a href="/download?file_path=${filePath}" download="${fileName}" class="download-link">⬇️ 下载文件: ${fileName}</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
step.innerHTML = `
|
||||
<div class="log-line">
|
||||
<span class="log-prefix">${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:</span>
|
||||
<pre>${content}</pre>
|
||||
${fileInteractionHtml}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
step.innerHTML = `
|
||||
<div class="log-line">
|
||||
<span class="log-prefix">${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:</span>
|
||||
<pre>${content}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
step.className = `step-item ${type}`;
|
||||
step.innerHTML = `
|
||||
<div class="log-line">
|
||||
<span class="log-prefix">${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:</span>
|
||||
<pre>${content}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return step;
|
||||
}
|
||||
|
||||
@ -269,13 +385,115 @@ function updateTaskStatus(task) {
|
||||
|
||||
if (task.status === 'completed') {
|
||||
statusBar.innerHTML = `<span class="status-complete">✅ Task completed</span>`;
|
||||
|
||||
if (currentEventSource) {
|
||||
currentEventSource.close();
|
||||
currentEventSource = null;
|
||||
}
|
||||
} else if (task.status === 'failed') {
|
||||
statusBar.innerHTML = `<span class="status-error">❌ Task failed: ${task.error || 'Unknown error'}</span>`;
|
||||
|
||||
if (currentEventSource) {
|
||||
currentEventSource.close();
|
||||
currentEventSource = null;
|
||||
}
|
||||
} else {
|
||||
statusBar.innerHTML = `<span class="status-running">⚙️ Task running: ${task.status}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Display full screen image
|
||||
function showFullImage(imageSrc) {
|
||||
const modal = document.getElementById('image-modal');
|
||||
if (!modal) {
|
||||
const modalDiv = document.createElement('div');
|
||||
modalDiv.id = 'image-modal';
|
||||
modalDiv.className = 'image-modal';
|
||||
modalDiv.innerHTML = `
|
||||
<span class="close-modal">×</span>
|
||||
<img src="${imageSrc}" class="modal-content" id="full-image">
|
||||
`;
|
||||
document.body.appendChild(modalDiv);
|
||||
|
||||
const closeBtn = modalDiv.querySelector('.close-modal');
|
||||
closeBtn.addEventListener('click', () => {
|
||||
modalDiv.classList.remove('active');
|
||||
});
|
||||
|
||||
modalDiv.addEventListener('click', (e) => {
|
||||
if (e.target === modalDiv) {
|
||||
modalDiv.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => modalDiv.classList.add('active'), 10);
|
||||
} else {
|
||||
document.getElementById('full-image').src = imageSrc;
|
||||
modal.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate running Python files
|
||||
function simulateRunPython(filePath) {
|
||||
let modal = document.getElementById('python-modal');
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'python-modal';
|
||||
modal.className = 'python-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="python-console">
|
||||
<div class="close-modal">×</div>
|
||||
<div class="python-output">Loading Python file contents...</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const closeBtn = modal.querySelector('.close-modal');
|
||||
closeBtn.addEventListener('click', () => {
|
||||
modal.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
modal.classList.add('active');
|
||||
|
||||
// Load Python file content
|
||||
fetch(filePath)
|
||||
.then(response => response.text())
|
||||
.then(code => {
|
||||
const outputDiv = modal.querySelector('.python-output');
|
||||
outputDiv.innerHTML = '';
|
||||
|
||||
const codeElement = document.createElement('pre');
|
||||
codeElement.textContent = code;
|
||||
codeElement.style.marginBottom = '20px';
|
||||
codeElement.style.padding = '10px';
|
||||
codeElement.style.borderBottom = '1px solid #444';
|
||||
outputDiv.appendChild(codeElement);
|
||||
|
||||
// Add simulation run results
|
||||
const resultElement = document.createElement('div');
|
||||
resultElement.innerHTML = `
|
||||
<div style="color: #4CAF50; margin-top: 10px; margin-bottom: 10px;">
|
||||
> Simulated operation output:</div>
|
||||
<pre style="color: #f8f8f8;">
|
||||
#This is the result of Python code simulation run
|
||||
#The actual operational results may vary
|
||||
|
||||
# Running ${filePath.split('/').pop()}...
|
||||
print("Hello from Python Simulated environment!")
|
||||
|
||||
# Code execution completed
|
||||
</pre>
|
||||
`;
|
||||
outputDiv.appendChild(resultElement);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading Python file:', error);
|
||||
const outputDiv = modal.querySelector('.python-output');
|
||||
outputDiv.innerHTML = `Error loading file: ${error.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadHistory();
|
||||
|
||||
@ -304,4 +522,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('prompt-input').focus();
|
||||
});
|
||||
}
|
||||
|
||||
// Add keyboard event listener to close modal boxes
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const imageModal = document.getElementById('image-modal');
|
||||
if (imageModal && imageModal.classList.contains('active')) {
|
||||
imageModal.classList.remove('active');
|
||||
}
|
||||
|
||||
const pythonModal = document.getElementById('python-modal');
|
||||
if (pythonModal && pythonModal.classList.contains('active')) {
|
||||
pythonModal.classList.remove('active');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
173
static/style.css
173
static/style.css
@ -181,7 +181,6 @@ button:hover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 200px);
|
||||
overflow-y: auto;
|
||||
@ -270,7 +269,7 @@ button:hover {
|
||||
.step-item pre {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
margin-left: 20px;
|
||||
overflow-x: hidden;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
@ -300,6 +299,176 @@ button:hover {
|
||||
}
|
||||
}
|
||||
|
||||
/* Step division line style */
|
||||
.step-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 15px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
z-index: 2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* File interaction style */
|
||||
.file-interaction {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
background-color: #f5f7fa;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.download-link {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.download-link:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 200px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.preview-image:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.audio-player audio {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.run-button {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
background-color: var(--success-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
margin-right: 10px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.run-button:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
/* Full screen image modal box */
|
||||
.image-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Python runs simulation modal boxes */
|
||||
.python-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.python-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.python-console {
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
background-color: #1e1e1e;
|
||||
color: #f8f8f8;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
font-family: 'Courier New', monospace;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.python-output {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
flex-grow: 1;
|
||||
height: 2px;
|
||||
background-color: var(--border-color);
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.step-info {
|
||||
margin-left: 15px;
|
||||
font-weight: bold;
|
||||
color: var(--text-light);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.step-item strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
|
Loading…
x
Reference in New Issue
Block a user