feat: 优化UI并增加代码编辑器

This commit is contained in:
ViperEkura 2026-03-27 00:42:38 +08:00
parent 1a010bb8ce
commit 917feb1c8c
14 changed files with 752 additions and 289 deletions

View File

@ -10,6 +10,7 @@
- 📁 **工作目录** - 项目级文件隔离,安全操作 - 📁 **工作目录** - 项目级文件隔离,安全操作
- 📊 **Token 统计** - 按日/周/月统计使用量 - 📊 **Token 统计** - 按日/周/月统计使用量
- 🔄 **流式响应** - 实时 SSE 流式输出 - 🔄 **流式响应** - 实时 SSE 流式输出
- 📝 **代码编辑器** - 基于 CodeMirror 6支持 15+ 语言语法高亮和暗色主题
- 💾 **多数据库** - 支持 MySQL、SQLite、PostgreSQL - 💾 **多数据库** - 支持 MySQL、SQLite、PostgreSQL
## 快速开始 ## 快速开始
@ -157,5 +158,5 @@ frontend/
## 技术栈 ## 技术栈
- **后端**: Python 3.11+, Flask, SQLAlchemy - **后端**: Python 3.11+, Flask, SQLAlchemy
- **前端**: Vue 3, Vite - **前端**: Vue 3, Vite, CodeMirror 6
- **LLM**: 支持 GLM 等大语言模型 - **LLM**: 支持 GLM 等大语言模型

View File

@ -85,6 +85,8 @@ def delete_message(conv_id, msg_id):
conv = _get_conv(conv_id) conv = _get_conv(conv_id)
if not conv: if not conv:
return err(404, "conversation not found") return err(404, "conversation not found")
if msg_id.startswith("temp_"):
return ok(message="deleted")
msg = db.session.get(Message, msg_id) msg = db.session.get(Message, msg_id)
if not msg or msg.conversation_id != conv_id: if not msg or msg.conversation_id != conv_id:
return err(404, "message not found") return err(404, "message not found")

View File

@ -8,6 +8,21 @@
"name": "nano-claw", "name": "nano-claw",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-java": "^6.0.1",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.2",
"@codemirror/lang-python": "^6.1.7",
"@codemirror/lang-rust": "^6.0.1",
"@codemirror/lang-sql": "^6.8.0",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/theme-one-dark": "^6.1.2",
"codemirror": "^6.0.1",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"katex": "^0.16.40", "katex": "^0.16.40",
"marked": "^15.0.12", "marked": "^15.0.12",
@ -66,6 +81,268 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@codemirror/autocomplete": {
"version": "6.20.1",
"resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz",
"integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.10.3",
"resolved": "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.10.3.tgz",
"integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-cpp": {
"version": "6.0.3",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz",
"integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/cpp": "^1.0.0"
}
},
"node_modules/@codemirror/lang-css": {
"version": "6.3.1",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.2",
"@lezer/css": "^1.1.7"
}
},
"node_modules/@codemirror/lang-go": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-go/-/lang-go-6.0.1.tgz",
"integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/go": "^1.0.0"
}
},
"node_modules/@codemirror/lang-html": {
"version": "6.4.11",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/css": "^1.1.0",
"@lezer/html": "^1.3.12"
}
},
"node_modules/@codemirror/lang-java": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-java/-/lang-java-6.0.2.tgz",
"integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/java": "^1.0.0"
}
},
"node_modules/@codemirror/lang-javascript": {
"version": "6.2.5",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/javascript": "^1.0.0"
}
},
"node_modules/@codemirror/lang-json": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
"integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/lang-markdown": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz",
"integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.7.1",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.3.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.2.1",
"@lezer/markdown": "^1.0.0"
}
},
"node_modules/@codemirror/lang-python": {
"version": "6.2.1",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-python/-/lang-python-6.2.1.tgz",
"integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.3.2",
"@codemirror/language": "^6.8.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.1",
"@lezer/python": "^1.1.4"
}
},
"node_modules/@codemirror/lang-rust": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz",
"integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/rust": "^1.0.0"
}
},
"node_modules/@codemirror/lang-sql": {
"version": "6.10.0",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz",
"integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@codemirror/lang-xml": {
"version": "6.1.0",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
"integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/xml": "^1.0.0"
}
},
"node_modules/@codemirror/lang-yaml": {
"version": "6.1.3",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-yaml/-/lang-yaml-6.1.3.tgz",
"integrity": "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.0.0",
"@lezer/yaml": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.12.3",
"resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.12.3.tgz",
"integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.9.5",
"resolved": "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.9.5.tgz",
"integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.6.0",
"resolved": "https://registry.npmmirror.com/@codemirror/search/-/search-6.6.0.tgz",
"integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.37.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.6.0",
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.6.0.tgz",
"integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/theme-one-dark": {
"version": "6.1.3",
"resolved": "https://registry.npmmirror.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.40.0",
"resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.40.0.tgz",
"integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.6.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@ -514,6 +791,167 @@
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@lezer/common": {
"version": "1.5.1",
"resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.5.1.tgz",
"integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==",
"license": "MIT"
},
"node_modules/@lezer/cpp": {
"version": "1.1.5",
"resolved": "https://registry.npmmirror.com/@lezer/cpp/-/cpp-1.1.5.tgz",
"integrity": "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/css": {
"version": "1.3.3",
"resolved": "https://registry.npmmirror.com/@lezer/css/-/css-1.3.3.tgz",
"integrity": "sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@lezer/go": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/@lezer/go/-/go-1.0.1.tgz",
"integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@lezer/highlight": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.3.0"
}
},
"node_modules/@lezer/html": {
"version": "1.3.13",
"resolved": "https://registry.npmmirror.com/@lezer/html/-/html-1.3.13.tgz",
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/java": {
"version": "1.1.3",
"resolved": "https://registry.npmmirror.com/@lezer/java/-/java-1.1.3.tgz",
"integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/javascript": {
"version": "1.5.4",
"resolved": "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.5.4.tgz",
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@lezer/json": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@lezer/json/-/json-1.0.3.tgz",
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.8",
"resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.8.tgz",
"integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/markdown": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@lezer/markdown/-/markdown-1.6.3.tgz",
"integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@lezer/python": {
"version": "1.1.18",
"resolved": "https://registry.npmmirror.com/@lezer/python/-/python-1.1.18.tgz",
"integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/rust": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/@lezer/rust/-/rust-1.0.2.tgz",
"integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/xml": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/@lezer/xml/-/xml-1.0.6.tgz",
"integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/yaml": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/@lezer/yaml/-/yaml-1.0.4.tgz",
"integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.4.0"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.60.0", "version": "4.60.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz",
@ -992,6 +1430,21 @@
"dev": true, "dev": true,
"license": "Python-2.0" "license": "Python-2.0"
}, },
"node_modules/codemirror": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "8.3.0", "version": "8.3.0",
"resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz",
@ -1001,6 +1454,12 @@
"node": ">= 12" "node": ">= 12"
} }
}, },
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
@ -1287,6 +1746,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"license": "MIT"
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
@ -1399,6 +1864,12 @@
"optional": true "optional": true
} }
} }
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
} }
} }
} }

View File

@ -9,6 +9,21 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"codemirror": "^6.0.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/lang-markdown": "^6.3.2",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-python": "^6.1.7",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/lang-java": "^6.0.1",
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-rust": "^6.0.1",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-sql": "^6.8.0",
"@codemirror/lang-xml": "^6.1.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"katex": "^0.16.40", "katex": "^0.16.40",
"marked": "^15.0.12", "marked": "^15.0.12",

View File

@ -83,12 +83,7 @@
<div class="create-modal"> <div class="create-modal">
<div class="modal-header"> <div class="modal-header">
<h3>创建项目</h3> <h3>创建项目</h3>
<button class="btn-icon" @click="showCreateModal = false"> <CloseButton @click="showCreateModal = false" />
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
@ -116,6 +111,7 @@ import { ref, shallowRef, computed, onMounted, defineAsyncComponent } from 'vue'
import Sidebar from './components/Sidebar.vue' import Sidebar from './components/Sidebar.vue'
import ChatView from './components/ChatView.vue' import ChatView from './components/ChatView.vue'
import FileExplorer from './components/FileExplorer.vue' import FileExplorer from './components/FileExplorer.vue'
import CloseButton from './components/CloseButton.vue'
const SettingsPanel = defineAsyncComponent(() => import('./components/SettingsPanel.vue')) const SettingsPanel = defineAsyncComponent(() => import('./components/SettingsPanel.vue'))
const StatsPanel = defineAsyncComponent(() => import('./components/StatsPanel.vue')) const StatsPanel = defineAsyncComponent(() => import('./components/StatsPanel.vue'))
@ -181,13 +177,10 @@ const newProjectDesc = ref('')
const creatingProject = ref(false) const creatingProject = ref(false)
function togglePanel(panel) { function togglePanel(panel) {
if (panel === 'settings') { const ref = panel === 'settings' ? showSettings : showStats
showSettings.value = !showSettings.value const other = panel === 'settings' ? showStats : showSettings
if (showSettings.value) showStats.value = false ref.value = !ref.value
} else { if (ref.value) other.value = false
showStats.value = !showStats.value
if (showStats.value) showSettings.value = false
}
} }
const currentConv = computed(() => const currentConv = computed(() =>
@ -629,31 +622,8 @@ onMounted(() => {
font-size: 14px; font-size: 14px;
} }
.modal-overlay { /* modal-overlay, modal-content, btn-icon, form-group, modal-footer, btn-secondary, btn-primary now in global.css */
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal-content {
border-radius: 16px;
width: 90%;
max-width: 520px;
max-height: 80vh;
overflow-y: auto;
padding: 24px;
background: color-mix(in srgb, var(--bg-primary) 75%, transparent);
backdrop-filter: blur(40px);
-webkit-backdrop-filter: blur(40px);
border: 1px solid var(--border-medium);
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.2);
}
/* -- Create project modal -- */
.create-modal { .create-modal {
background: var(--bg-primary); background: var(--bg-primary);
border: 1px solid var(--border-medium); border: 1px solid var(--border-medium);
@ -662,104 +632,4 @@ onMounted(() => {
max-width: 440px; max-width: 440px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
} }
.btn-icon {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
background: none;
color: var(--text-tertiary);
cursor: pointer;
border-radius: 6px;
transition: all 0.15s;
}
.btn-icon:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 6px;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 8px 12px;
background: var(--bg-secondary);
border: 1px solid var(--border-light);
border-radius: 8px;
color: var(--text-primary);
font-size: 14px;
outline: none;
box-sizing: border-box;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group textarea:focus {
border-color: var(--accent-primary);
}
.form-group textarea {
resize: vertical;
min-height: 60px;
font-family: inherit;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 20px;
border-top: 1px solid var(--border-light);
}
.btn-secondary {
padding: 8px 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-light);
border-radius: 8px;
color: var(--text-secondary);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.btn-primary {
padding: 8px 16px;
background: var(--accent-primary);
border: none;
border-radius: 8px;
color: #fff;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary:hover {
opacity: 0.9;
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style> </style>

View File

@ -260,12 +260,14 @@ watch(() => props.conversation?.id, () => {
} }
.thinking-badge { .thinking-badge {
background: var(--success-bg); background: rgba(245, 158, 11, 0.12);
color: var(--success-color); color: #d97706;
} }
[data-theme="dark"] .thinking-badge {
background: rgba(245, 158, 11, 0.18);
color: #fbbf24;
}
.messages-container { .messages-container {
flex: 1 1 auto; flex: 1 1 auto;
@ -309,7 +311,7 @@ watch(() => props.conversation?.id, () => {
padding: 0 16px; padding: 0 16px;
} }
/* .message-bubble, .avatar, .message-body now in global.css */

View File

@ -0,0 +1,168 @@
<template>
<div ref="container" class="code-editor-wrap"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { EditorView, keymap } from '@codemirror/view'
import { EditorState, Compartment } from '@codemirror/state'
import { basicSetup } from 'codemirror'
import { indentWithTab } from '@codemirror/commands'
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark'
import { syntaxHighlighting } from '@codemirror/language'
import { markdown } from '@codemirror/lang-markdown'
import { javascript } from '@codemirror/lang-javascript'
import { python } from '@codemirror/lang-python'
import { html } from '@codemirror/lang-html'
import { css } from '@codemirror/lang-css'
import { json } from '@codemirror/lang-json'
import { yaml } from '@codemirror/lang-yaml'
import { java } from '@codemirror/lang-java'
import { cpp } from '@codemirror/lang-cpp'
import { rust } from '@codemirror/lang-rust'
import { go } from '@codemirror/lang-go'
import { sql } from '@codemirror/lang-sql'
import { xml } from '@codemirror/lang-xml'
const EXT_MAP = {
md: markdown, markdown: markdown, mdx: markdown,
js: () => javascript(), jsx: () => javascript({ jsx: true }),
ts: () => javascript({ typescript: true }),
tsx: () => javascript({ jsx: true, typescript: true }),
py: python, pyw: python,
html: html, htm: html, vue: html, svelte: html,
css: css, scss: css, less: css,
json: json, jsonc: json,
yaml: yaml, yml: yaml,
java: java,
c: cpp, h: cpp, cpp: cpp, cc: cpp, cxx: cpp, hpp: cpp,
rs: rust,
go: go,
sql: sql,
xml: xml, svg: xml, xsl: xml,
}
function langForFile(filename) {
if (!filename) return []
const ext = filename.split('.').pop().toLowerCase()
const fn = EXT_MAP[ext]
if (!fn) return []
const result = typeof fn === 'function' ? fn() : fn
return Array.isArray(result) ? result : [result]
}
const props = defineProps({
modelValue: { type: String, default: '' },
filename: { type: String, default: '' },
dark: { type: Boolean, default: false },
})
const emit = defineEmits(['update:modelValue', 'save'])
const container = ref(null)
let view = null
let skipEmit = false
const themeComp = new Compartment()
const langComp = new Compartment()
// Custom dark theme matching app's --bg-primary / --bg-secondary
const appDarkTheme = EditorView.theme({
'&': { backgroundColor: '#1a1a1a', color: '#f0f0f0' },
'.cm-gutters': {
backgroundColor: '#141414',
color: '#606060',
border: 'none',
borderRight: '1px solid rgba(255,255,255,0.06)',
},
'.cm-activeLineGutter': { backgroundColor: 'rgba(255,255,255,0.04)', color: '#a0a0a0' },
'.cm-activeLine': { backgroundColor: 'rgba(255,255,255,0.03)' },
'&.cm-focused .cm-cursor': { borderLeftColor: '#3b82f6', borderLeftWidth: '2px' },
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, &.cm-focused .cm-content ::selection': {
backgroundColor: 'rgba(59,130,246,0.25) !important',
},
'.cm-searchMatch': { backgroundColor: 'rgba(250,204,21,0.3)' },
'&.cm-focused .cm-searchMatch': { backgroundColor: 'rgba(250,204,21,0.45)' },
'.cm-panels': { backgroundColor: '#1a1a1a', borderColor: 'rgba(255,255,255,0.08)' },
'.cm-panels input, .cm-panels button, .cm-panels select': {
backgroundColor: '#141414',
color: '#f0f0f0',
border: '1px solid rgba(255,255,255,0.1)',
},
'.cm-tooltip': {
backgroundColor: '#141414',
border: '1px solid rgba(255,255,255,0.1)',
color: '#f0f0f0',
},
'.cm-tooltip-autocomplete > ul > li': { padding: '4px 8px' },
'.cm-tooltip-autocomplete > ul > li[aria-selected]': {
backgroundColor: 'rgba(59,130,246,0.2)',
color: '#f0f0f0',
},
}, { dark: true })
const editorTheme = EditorView.theme({
'&': { height: '100%' },
'.cm-scroller': { overflow: 'auto', height: '100%' },
'.cm-gutters': { minWidth: '40px' },
'.cm-content': {
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
fontSize: '14px',
lineHeight: '1.7',
},
})
onMounted(() => {
view = new EditorView({
state: EditorState.create({
doc: props.modelValue,
extensions: [
basicSetup,
editorTheme,
keymap.of([
indentWithTab,
{ key: 'Mod-s', run: () => { emit('save'); return true } },
]),
EditorView.updateListener.of(update => {
if (update.docChanged && !skipEmit) {
emit('update:modelValue', update.state.doc.toString())
}
}),
EditorView.lineWrapping,
themeComp.of(props.dark ? [appDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)] : []),
langComp.of(langForFile(props.filename)),
],
}),
parent: container.value,
})
})
watch(() => props.modelValue, val => {
if (!view || val === view.state.doc.toString()) return
skipEmit = true
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: val } })
skipEmit = false
})
watch(() => props.dark, isDark => {
view?.dispatch({
effects: themeComp.reconfigure(
isDark ? [appDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)] : []
),
})
})
watch(() => props.filename, () => {
view?.dispatch({ effects: langComp.reconfigure(langForFile(props.filename)) })
})
onUnmounted(() => view?.destroy())
</script>
<style scoped>
.code-editor-wrap {
height: 100%;
width: 100%;
overflow: hidden;
}
</style>

View File

@ -54,9 +54,9 @@
<div v-if="activeFile" class="file-viewer"> <div v-if="activeFile" class="file-viewer">
<div class="viewer-header"> <div class="viewer-header">
<div class="viewer-breadcrumb"> <div class="viewer-breadcrumb">
<span v-for="(seg, i) in activeFile.split('/')" :key="i" class="breadcrumb-seg"> <span v-for="(seg, i) in breadcrumbSegments" :key="i" class="breadcrumb-seg">
{{ seg }} {{ seg }}
<span v-if="i < activeFile.split('/').length - 1" class="breadcrumb-sep">/</span> <span v-if="i < breadcrumbSegments.length - 1" class="breadcrumb-sep">/</span>
</span> </span>
</div> </div>
<div class="viewer-actions"> <div class="viewer-actions">
@ -93,17 +93,14 @@
<span>{{ fileError }}</span> <span>{{ fileError }}</span>
</div> </div>
<!-- Text / code editor (default mode) --> <!-- Text / code editor (all non-image files including .md) -->
<div v-else-if="fileType !== 'image'" class="editor-container"> <div v-else-if="fileType !== 'image'" class="code-pane">
<div class="editor-highlight" aria-hidden="true" v-html="editorHighlighted"></div> <CodeEditor
<textarea
ref="editorRef"
v-model="editContent" v-model="editContent"
class="file-editor" :filename="activeFile"
spellcheck="false" :dark="isDark"
@keydown="onEditorKeydown" @save="saveFile"
@scroll="syncScroll" />
></textarea>
</div> </div>
<!-- Image viewer --> <!-- Image viewer -->
@ -130,19 +127,19 @@
import { ref, computed, watch, onMounted, onUnmounted } from 'vue' import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { projectApi } from '../api' import { projectApi } from '../api'
import FileTreeItem from './FileTreeItem.vue' import FileTreeItem from './FileTreeItem.vue'
import { renderMarkdown } from '../utils/markdown' import CodeEditor from './CodeEditor.vue'
import { normalizeFileTree } from '../utils/fileTree' import { normalizeFileTree } from '../utils/fileTree'
import { useTheme } from '../composables/useTheme'
const IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'ico']) const IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'ico'])
// Treat all text/code files as markdown (code blocks in ``` get auto-highlighted)
// For pure code files without markdown, wrap in code fence
const props = defineProps({ const props = defineProps({
projectId: { type: String, required: true }, projectId: { type: String, required: true },
projectName: { type: String, default: '' }, projectName: { type: String, default: '' },
}) })
const { isDark } = useTheme()
// -- Tree state -- // -- Tree state --
const treeItems = ref([]) const treeItems = ref([])
const loadingTree = ref(false) const loadingTree = ref(false)
@ -153,13 +150,14 @@ const fileError = ref('')
const loadingFile = ref(false) const loadingFile = ref(false)
const editContent = ref('') const editContent = ref('')
const saving = ref(false) const saving = ref(false)
const editorRef = ref(null)
const imageUrl = ref('') const imageUrl = ref('')
// -- File type detection -- function releaseImageUrl() {
const isMarkdownFile = computed(() => { if (imageUrl.value) {
return ['md', 'markdown', 'mdx'].includes(fileExt.value) URL.revokeObjectURL(imageUrl.value)
}) imageUrl.value = ''
}
}
const fileExt = computed(() => { const fileExt = computed(() => {
if (!activeFile.value) return '' if (!activeFile.value) return ''
@ -167,28 +165,16 @@ const fileExt = computed(() => {
return parts.length > 1 ? parts.pop().toLowerCase() : '' return parts.length > 1 ? parts.pop().toLowerCase() : ''
}) })
const breadcrumbSegments = computed(() => {
if (!activeFile.value) return []
return activeFile.value.split('/')
})
const fileType = computed(() => { const fileType = computed(() => {
if (IMAGE_EXTS.has(fileExt.value)) return 'image' if (IMAGE_EXTS.has(fileExt.value)) return 'image'
return 'text' return 'text'
}) })
// -- Content rendering --
const editorHighlighted = computed(() => {
if (!editContent.value) return ''
if (isMarkdownFile.value) return renderMarkdown(editContent.value)
const lang = fileExt.value || ''
return renderMarkdown('```' + lang + '\n' + editContent.value + '\n```')
})
function syncScroll() {
const ta = editorRef.value
const pre = ta?.previousElementSibling
if (pre) {
pre.scrollTop = ta.scrollTop
pre.scrollLeft = ta.scrollLeft
}
}
async function loadTree(path = '') { async function loadTree(path = '') {
loadingTree.value = true loadingTree.value = true
try { try {
@ -205,7 +191,7 @@ async function openFile(filepath) {
activeFile.value = filepath activeFile.value = filepath
fileError.value = '' fileError.value = ''
editContent.value = '' editContent.value = ''
imageUrl.value = '' releaseImageUrl()
loadingFile.value = true loadingFile.value = true
const ext = filepath.split('.').pop().toLowerCase() const ext = filepath.split('.').pop().toLowerCase()
@ -281,13 +267,6 @@ async function createNewFolder() {
} }
} }
function onEditorKeydown(e) {
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
saveFile()
}
}
// Ctrl+S global shortcut // Ctrl+S global shortcut
function onGlobalKeydown(e) { function onGlobalKeydown(e) {
if (e.key === 's' && (e.ctrlKey || e.metaKey) && activeFile.value) { if (e.key === 's' && (e.ctrlKey || e.metaKey) && activeFile.value) {
@ -299,7 +278,7 @@ function onGlobalKeydown(e) {
watch(() => props.projectId, () => { watch(() => props.projectId, () => {
activeFile.value = null activeFile.value = null
editContent.value = '' editContent.value = ''
imageUrl.value = '' releaseImageUrl()
loadTree() loadTree()
}) })
@ -310,6 +289,7 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('keydown', onGlobalKeydown) document.removeEventListener('keydown', onGlobalKeydown)
releaseImageUrl()
}) })
</script> </script>
@ -459,6 +439,7 @@ onUnmounted(() => {
display: flex; display: flex;
gap: 2px; gap: 2px;
flex-shrink: 0; flex-shrink: 0;
align-items: center;
} }
.viewer-loading { .viewer-loading {
@ -480,31 +461,6 @@ onUnmounted(() => {
font-size: 13px; font-size: 13px;
} }
.file-editor {
flex: 1;
resize: none;
border: none;
outline: none;
color: var(--text-primary);
background: var(--bg-code);
tab-size: 4;
white-space: pre;
overflow: auto;
}
.file-editor::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.file-editor::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 3px;
}
.viewer-placeholder { .viewer-placeholder {
flex: 1; flex: 1;
display: flex; display: flex;
@ -516,60 +472,12 @@ onUnmounted(() => {
font-size: 13px; font-size: 13px;
} }
/* -- Editor (highlighted textarea overlay) -- */ /* -- Code pane -- */
.editor-container { .code-pane {
flex: 1; flex: 1;
display: flex;
position: relative;
overflow: hidden; overflow: hidden;
} }
.editor-highlight {
position: absolute;
inset: 0;
margin: 0;
padding: 20px 24px;
overflow: auto;
pointer-events: none;
color: var(--text-primary);
background: transparent;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 14px;
line-height: 1.7;
white-space: pre;
tab-size: 4;
}
/* Strip wrapper styles from markdown-rendered <pre><code> so text aligns with textarea */
.editor-highlight :deep(pre),
.editor-highlight :deep(code) {
margin: 0;
padding: 0;
background: transparent !important;
border: none;
border-radius: 0;
font-size: inherit;
font-family: inherit;
line-height: inherit;
white-space: inherit;
tab-size: inherit;
word-wrap: normal;
}
.editor-container .file-editor {
position: relative;
z-index: 1;
flex: 1;
border: none;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 14px;
line-height: 1.7;
padding: 20px 24px;
color: transparent;
caret-color: var(--text-primary);
background: transparent;
}
/* -- Image viewer -- */ /* -- Image viewer -- */
.image-viewer { .image-viewer {
flex: 1; flex: 1;

View File

@ -161,10 +161,6 @@ async function onClick() {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.tree-children {
/* no extra indent, tree-item handles it via tree-indent */
}
.tree-loading { .tree-loading {
padding: 4px 0 4px 40px; padding: 4px 0 4px 40px;
color: var(--text-tertiary); color: var(--text-tertiary);

View File

@ -92,8 +92,6 @@ function copyContent() {
</script> </script>
<style scoped> <style scoped>
/* .message-bubble, .avatar, .message-body now in global.css */
.attachments-list { .attachments-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -115,8 +113,8 @@ function copyContent() {
} }
.attachment-icon { .attachment-icon {
background: rgba(139, 92, 246, 0.15); background: var(--attachment-bg);
color: #8b5cf6; color: var(--attachment-color);
padding: 2px 6px; padding: 2px 6px;
border-radius: 4px; border-radius: 4px;
font-size: 11px; font-size: 11px;

View File

@ -306,8 +306,8 @@ textarea::placeholder {
height: 36px; height: 36px;
border-radius: 8px; border-radius: 8px;
border: none; border: none;
background: rgba(139, 92, 246, 0.12); background: var(--attachment-bg);
color: #8b5cf6; color: var(--attachment-color);
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
@ -322,23 +322,23 @@ textarea::placeholder {
} }
.btn-tool.active { .btn-tool.active {
background: var(--success-bg); background: var(--tool-bg);
color: var(--success-color); color: var(--tool-color);
} }
.btn-tool.active:hover:not(:disabled) { .btn-tool.active:hover:not(:disabled) {
background: var(--success-color); background: var(--tool-color);
color: white; color: white;
} }
.btn-upload:hover:not(:disabled) { .btn-upload:hover:not(:disabled) {
background: #8b5cf6; background: var(--attachment-color);
color: white; color: white;
transform: translateY(-1px); transform: translateY(-1px);
} }
.btn-upload:active:not(:disabled) { .btn-upload:active:not(:disabled) {
background: #7c3aed; background: var(--attachment-color-hover);
transform: translateY(0); transform: translateY(0);
} }

View File

@ -236,7 +236,7 @@ watch(() => props.streamingContent?.length, () => {
50% { opacity: 1; } 50% { opacity: 1; }
} }
/* Thinking and tool call step headers */ /* Step header (shared by thinking and tool_call) */
.thinking .step-header, .thinking .step-header,
.tool_call .step-header { .tool_call .step-header {
display: flex; display: flex;
@ -261,7 +261,7 @@ watch(() => props.streamingContent?.length, () => {
} }
.tool_call .step-header svg:first-child { .tool_call .step-header svg:first-child {
color: #a855f7; color: var(--tool-color);
} }
.step-label { .step-label {
@ -317,7 +317,7 @@ watch(() => props.streamingContent?.length, () => {
.loading-dots { .loading-dots {
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
color: var(--accent-primary); color: var(--tool-color);
animation: pulse 1s ease-in-out infinite; animation: pulse 1s ease-in-out infinite;
} }

View File

@ -217,14 +217,11 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { statsApi } from '../api' import { statsApi } from '../api'
import { useTheme } from '../composables/useTheme'
import { formatNumber } from '../utils/format' import { formatNumber } from '../utils/format'
import CloseButton from './CloseButton.vue' import CloseButton from './CloseButton.vue'
defineEmits(['close']) defineEmits(['close'])
const { isDark } = useTheme()
const periods = [ const periods = [
{ value: 'daily', label: '今日' }, { value: 'daily', label: '今日' },
{ value: 'weekly', label: '本周' }, { value: 'weekly', label: '本周' },
@ -236,7 +233,9 @@ const stats = ref(null)
const loading = ref(false) const loading = ref(false)
const hoveredPoint = ref(null) const hoveredPoint = ref(null)
const accentColor = computed(() => isDark.value ? '#60a5fa' : '#2563eb') const accentColor = computed(() => {
return getComputedStyle(document.documentElement).getPropertyValue('--accent-primary').trim() || '#2563eb'
})
const chartWidth = 320 const chartWidth = 320
const chartHeight = 140 const chartHeight = 140

View File

@ -22,6 +22,16 @@
--accent-primary-light: rgba(37, 99, 235, 0.08); --accent-primary-light: rgba(37, 99, 235, 0.08);
--accent-primary-medium: rgba(37, 99, 235, 0.15); --accent-primary-medium: rgba(37, 99, 235, 0.15);
--tool-color: #5478FF;
--tool-color-hover: #3d5ce0;
--tool-bg: rgba(84, 120, 255, 0.18);
--tool-bg-hover: rgba(84, 120, 255, 0.28);
--tool-border: rgba(84, 120, 255, 0.22);
--attachment-color: #ca8a04;
--attachment-color-hover: #a16207;
--attachment-bg: rgba(202, 138, 4, 0.15);
--success-color: #059669; --success-color: #059669;
--success-bg: rgba(16, 185, 129, 0.1); --success-bg: rgba(16, 185, 129, 0.1);
--danger-color: #ef4444; --danger-color: #ef4444;
@ -57,6 +67,16 @@
--accent-primary-light: rgba(59, 130, 246, 0.15); --accent-primary-light: rgba(59, 130, 246, 0.15);
--accent-primary-medium: rgba(59, 130, 246, 0.25); --accent-primary-medium: rgba(59, 130, 246, 0.25);
--tool-color: #5478FF;
--tool-color-hover: #7a96ff;
--tool-bg: rgba(84, 120, 255, 0.28);
--tool-bg-hover: rgba(84, 120, 255, 0.40);
--tool-border: rgba(84, 120, 255, 0.32);
--attachment-color: #facc15;
--attachment-color-hover: #fde047;
--attachment-bg: rgba(250, 204, 21, 0.22);
--success-color: #34d399; --success-color: #34d399;
--success-bg: rgba(52, 211, 153, 0.15); --success-bg: rgba(52, 211, 153, 0.15);
--danger-color: #f87171; --danger-color: #f87171;
@ -215,8 +235,6 @@ body {
overflow-x: auto; overflow-x: auto;
} }
/* (merged above with .md-content pre selectors) */
.code-header { .code-header {
display: flex; display: flex;
align-items: center; align-items: center;
@ -417,6 +435,20 @@ input[type="range"]::-moz-range-thumb {
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
} }
.modal-content {
border-radius: 16px;
width: 90%;
max-width: 520px;
max-height: 80vh;
overflow-y: auto;
padding: 24px;
background: color-mix(in srgb, var(--bg-primary) 75%, transparent);
backdrop-filter: blur(40px);
-webkit-backdrop-filter: blur(40px);
border: 1px solid var(--border-medium);
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.2);
}
.modal-header { .modal-header {
display: flex; display: flex;
align-items: center; align-items: center;
@ -665,6 +697,7 @@ input[type="range"]::-moz-range-thumb {
padding-right: 40px; padding-right: 40px;
} }
.form-group select:hover { .form-group select:hover {
border-color: var(--accent-primary); border-color: var(--accent-primary);
} }