Compare commits

...

25 Commits

Author SHA1 Message Date
Isaac
7e18c97a2f
Merge pull request #821 from aylvn/desktop
Desktop has implemented the large model configuration page and settings page functionality
2025-03-19 15:44:17 +08:00
Sheng Fan
e190e1484e merge: manually check translations 2025-03-19 12:54:13 +08:00
Sheng Fan
e3202ade12 refactor: rewrite comments in English 2025-03-19 12:44:27 +08:00
aylvn
ec35d42936 resolved some pre-commit check problems 2025-03-18 22:33:32 +08:00
aylvn
da607e31b2 Desktop version has added a llm configuration page and a general configuration page 2025-03-18 22:20:37 +08:00
aylvn
4a0c3c6231
Merge branch 'mannaandpoem:desktop' into desktop 2025-03-17 23:19:12 +08:00
aylvn
2d507c0bcf Desktop client reads llm configuration from the config/config.toml file 2025-03-17 23:15:14 +08:00
aylvn
7e42e4ccd2 优化任务页面布局和交互控制 2025-03-16 18:57:30 +08:00
aylvn
bcada6ead5 增加深色/浅色主题切换功能 2025-03-16 16:22:22 +08:00
aylvn
d08db8a76a 新增历史任务详情页 2025-03-16 16:02:03 +08:00
aylvn
94cf2be101 任务状态展示优化 2025-03-16 15:04:04 +08:00
aylvn
00bf070dbd 历史任务页面开发 2025-03-16 14:58:07 +08:00
mannaandpoem
77b42aaf47
Merge pull request #679 from aylvn/desktop
Desktop
2025-03-16 12:43:49 +08:00
aylvn
ae053d6e27 更新desktop README.md 2025-03-16 12:26:07 +08:00
aylvn
0710570811 桌面客户端desktop代码初始化提交 2025-03-16 12:13:27 +08:00
Isaac
38e34219d3
Merge pull request #565 from Feige-cn/front-end
fix download bug
2025-03-13 16:34:57 +08:00
Feige-cn
693f6a90ad fix download bug 2025-03-13 11:06:49 +08:00
xiangjinyu
d847b550d6 add max_steps 2025-03-12 19:22:32 +08:00
xiangjinyu
5bba31db3e update terminate prompt 2025-03-12 19:21:13 +08:00
xiangjinyu
548c402316 add max_observe in toolcall and manus 2025-03-12 17:23:08 +08:00
xiangjinyu
c96c016feb update browser-use window size 2025-03-12 17:21:06 +08:00
Isaac
728fdaffd6
Merge pull request #520 from Feige-cn/front-end
Optimize front-end display
2025-03-12 15:53:22 +08:00
Feige-cn
fa2b05b658 Optimize front-end display 2025-03-12 15:45:02 +08:00
Isaac
7b96e12858
Merge pull request #456 from Feige-cn/front-end
Synchronize built-in environment
2025-03-11 13:58:00 +08:00
Feige-cn
62f937c1ca Synchronize built-in environment 2025-03-11 13:48:00 +08:00
66 changed files with 13726 additions and 60 deletions

3
.gitignore vendored
View File

@ -178,3 +178,6 @@ data/
# Workspace
workspace/
# Private Config
config/config.toml

50
app.py
View File

@ -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"])

View File

@ -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(

View File

@ -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

View File

@ -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())

View File

@ -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):

View File

@ -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
View File

@ -0,0 +1,5 @@
build
node_modules
frontend/dist
frontend/package.json.md5
*.log

71
desktop/README.md Normal file
View 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 projects dist directory.

71
desktop/README_zh.md Normal file
View 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
View 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
}

View 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)

View 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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View 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>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 B

View 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
}

View 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()
}

View 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,
}

View 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,
}

View 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;
}

View 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;
}

View 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
}
}
}
},
})

View 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('LeftMenurouter.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>

View 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>

View 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>

View 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>

View 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>

View 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",
}

View 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

View 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: "服务器配置",
}

View 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')

View 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

View 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",
}
})

View 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>

View 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>

View 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>

View 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>

View 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) {
// stepstepList
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())
}
// stepListsubList
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>

View 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) {
// stepstepList
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())
}
// stepListsubList
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>

View 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>

View 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,
}
},
}
})

View 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>;

View 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);
}

View 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"
}

View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -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

View File

@ -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">&times;</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">&times;</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');
}
}
});
});

View File

@ -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;