This commit is contained in:
aaron 2026-04-07 21:37:44 +08:00
parent 4e1c7f2e8e
commit 41e75fd0ce
56 changed files with 501 additions and 1329 deletions

78
.gitignore vendored Normal file
View File

@ -0,0 +1,78 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
*.egg
dist/
build/
.eggs/
*.whl
# Python virtual environment
backend/venv/
venv/
.venv/
env/
# Environment variables
.env
.env.local
.env.*.local
# SQLite database
*.db
*.db-journal
*.sqlite3
# Node.js
node_modules/
frontend/node_modules/
# Next.js
frontend/.next/
frontend/out/
# Build / production
*.tsbuildinfo
.next/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.project
.classpath
.settings/
# OS files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Thumbs.db
ehthumbs.db
Desktop.ini
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Coverage / test
coverage/
htmlcov/
.coverage
.coverage.*
.pytest_cache/
.tox/
.nox/
# Misc
*.bak
*.tmp
*.temp

View File

@ -25,6 +25,10 @@ async def get_latest():
"up_count": mt.up_count if mt else 0,
"down_count": mt.down_count if mt else 0,
"limit_up_count": mt.limit_up_count if mt else 0,
"limit_down_count": mt.limit_down_count if mt else 0,
"max_streak": mt.max_streak if mt else 0,
"broken_rate": mt.broken_rate if mt else 0,
"index_above_ma20": mt.index_above_ma20 if mt else False,
} if mt else None,
"recommendations": [
{

View File

@ -29,6 +29,8 @@ async def init_db():
"ALTER TABLE recommendations ADD COLUMN position_score REAL",
"ALTER TABLE recommendations ADD COLUMN valuation_score REAL",
"ALTER TABLE recommendations ADD COLUMN llm_analysis TEXT DEFAULT ''",
"ALTER TABLE market_temperature ADD COLUMN max_streak INTEGER",
"ALTER TABLE market_temperature ADD COLUMN broken_rate REAL",
]:
try:
await conn.execute(

View File

@ -81,20 +81,23 @@ async def _save_to_db(result: dict):
# 保存市场温度
mt = result.get("market_temp")
if mt:
stmt = tables.market_temperature_table.insert().values(
trade_date=mt.trade_date,
up_count=mt.up_count,
down_count=mt.down_count,
limit_up_count=mt.limit_up_count,
limit_down_count=mt.limit_down_count,
max_streak=mt.max_streak,
broken_rate=mt.broken_rate,
temperature=mt.temperature,
# 使用 INSERT OR REPLACE 确保重复扫描能更新数据
stmt = text(
"INSERT OR REPLACE INTO market_temperature "
"(trade_date, up_count, down_count, limit_up_count, limit_down_count, "
"max_streak, broken_rate, temperature) "
"VALUES (:td, :up, :down, :lu, :ld, :ms, :br, :temp)"
)
try:
await db.execute(stmt)
except Exception:
pass # 可能已存在UNIQUE 约束)
await db.execute(stmt, {
"td": mt.trade_date,
"up": mt.up_count,
"down": mt.down_count,
"lu": mt.limit_up_count,
"ld": mt.limit_down_count,
"ms": mt.max_streak,
"br": mt.broken_rate,
"temp": mt.temperature,
})
# 保存板块热度(先清除同一 trade_date 的旧数据,避免重复)
trade_date_val = mt.trade_date if mt else ""

Binary file not shown.

View File

@ -1,25 +1,25 @@
{
"pages": {
"/chat/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/chat/page.js"
],
"/layout": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/css/app/layout.css",
"static/chunks/app/layout.js"
],
"/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/page.js"
],
"/recommendations/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/recommendations/page.js"
],
"/sectors/page": [
"/stock/[code]/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/sectors/page.js"
"static/chunks/app/stock/[code]/page.js"
]
}
}

File diff suppressed because one or more lines are too long

View File

@ -1 +1,20 @@
{}
{
"components/capital-flow.tsx -> echarts": {
"id": "components/capital-flow.tsx -> echarts",
"files": [
"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js"
]
},
"components/kline-chart.tsx -> echarts": {
"id": "components/kline-chart.tsx -> echarts",
"files": [
"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js"
]
},
"components/score-radar.tsx -> echarts": {
"id": "components/score-radar.tsx -> echarts",
"files": [
"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js"
]
}
}

View File

@ -1,5 +1,6 @@
{
"/page": "app/page.js",
"/chat/page": "app/chat/page.js",
"/recommendations/page": "app/recommendations/page.js",
"/sectors/page": "app/sectors/page.js"
"/api/chat/stream/route": "app/api/chat/stream/route.js",
"/stock/[code]/page": "app/stock/[code]/page.js"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
self.__REACT_LOADABLE_MANIFEST="{}"
self.__REACT_LOADABLE_MANIFEST="{\"components/capital-flow.tsx -> echarts\":{\"id\":\"components/capital-flow.tsx -> echarts\",\"files\":[\"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js\"]},\"components/kline-chart.tsx -> echarts\":{\"id\":\"components/kline-chart.tsx -> echarts\",\"files\":[\"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js\"]},\"components/score-radar.tsx -> echarts\":{\"id\":\"components/score-radar.tsx -> echarts\",\"files\":[\"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js\"]}}"

View File

@ -1,5 +1,5 @@
{
"node": {},
"edge": {},
"encryptionKey": "rmrljjdyNTjhpyAHQzkci4dGEXtuEnS7slbszZTpG4E="
"encryptionKey": "YesDqzwu7U3Zt5BbI/COWZPvCGzAI5FFxbDk16RGGxQ="
}

View File

@ -10,6 +10,17 @@ exports.id = "vendor-chunks/next";
exports.ids = ["vendor-chunks/next"];
exports.modules = {
/***/ "(ssr)/./node_modules/next/dist/api/navigation.js":
/*!**************************************************!*\
!*** ./node_modules/next/dist/api/navigation.js ***!
\**************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _client_components_navigation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../client/components/navigation */ \"(ssr)/./node_modules/next/dist/client/components/navigation.js\");\n/* harmony import */ var _client_components_navigation__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_client_components_navigation__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony reexport (unknown) */ var __WEBPACK_REEXPORT_OBJECT__ = {};\n/* harmony reexport (unknown) */ for(const __WEBPACK_IMPORT_KEY__ in _client_components_navigation__WEBPACK_IMPORTED_MODULE_0__) if(__WEBPACK_IMPORT_KEY__ !== \"default\") __WEBPACK_REEXPORT_OBJECT__[__WEBPACK_IMPORT_KEY__] = () => _client_components_navigation__WEBPACK_IMPORTED_MODULE_0__[__WEBPACK_IMPORT_KEY__]\n/* harmony reexport (unknown) */ __webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__);\n\n\n//# sourceMappingURL=navigation.js.map//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvbmV4dC9kaXN0L2FwaS9uYXZpZ2F0aW9uLmpzIiwibWFwcGluZ3MiOiI7Ozs7OztBQUFnRDs7QUFFaEQiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9hc3RvY2stYWdlbnQtZnJvbnRlbmQvLi9ub2RlX21vZHVsZXMvbmV4dC9kaXN0L2FwaS9uYXZpZ2F0aW9uLmpzP2MyNzIiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0ICogZnJvbSBcIi4uL2NsaWVudC9jb21wb25lbnRzL25hdmlnYXRpb25cIjtcblxuLy8jIHNvdXJjZU1hcHBpbmdVUkw9bmF2aWdhdGlvbi5qcy5tYXAiXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/next/dist/api/navigation.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/next/dist/client/add-base-path.js":
/*!********************************************************!*\
!*** ./node_modules/next/dist/client/add-base-path.js ***!
@ -2097,6 +2108,17 @@ eval("\nmodule.exports = __webpack_require__(/*! ../../module.compiled */ \"(ssr
/***/ }),
/***/ "(rsc)/./node_modules/next/dist/server/future/route-modules/app-route/module.compiled.js":
/*!*****************************************************************************************!*\
!*** ./node_modules/next/dist/server/future/route-modules/app-route/module.compiled.js ***!
\*****************************************************************************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
"use strict";
eval("\nif (false) {} else {\n if (false) {} else {\n if (true) {\n module.exports = __webpack_require__(/*! next/dist/compiled/next-server/app-route.runtime.dev.js */ \"next/dist/compiled/next-server/app-route.runtime.dev.js\");\n } else {}\n }\n}\n\n//# sourceMappingURL=module.compiled.js.map//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHJzYykvLi9ub2RlX21vZHVsZXMvbmV4dC9kaXN0L3NlcnZlci9mdXR1cmUvcm91dGUtbW9kdWxlcy9hcHAtcm91dGUvbW9kdWxlLmNvbXBpbGVkLmpzIiwibWFwcGluZ3MiOiJBQUFhO0FBQ2IsSUFBSSxLQUFtQyxFQUFFLEVBRXhDLENBQUM7QUFDRixRQUFRLEtBQXFDLEVBQUUsRUFRMUMsQ0FBQztBQUNOLFlBQVksSUFBc0M7QUFDbEQsWUFBWSw4SkFBbUY7QUFDL0YsVUFBVSxLQUFLLEVBSU47QUFDVDtBQUNBOztBQUVBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vYXN0b2NrLWFnZW50LWZyb250ZW5kLy4vbm9kZV9tb2R1bGVzL25leHQvZGlzdC9zZXJ2ZXIvZnV0dXJlL3JvdXRlLW1vZHVsZXMvYXBwLXJvdXRlL21vZHVsZS5jb21waWxlZC5qcz8zYmM3Il0sInNvdXJjZXNDb250ZW50IjpbIlwidXNlIHN0cmljdFwiO1xuaWYgKHByb2Nlc3MuZW52Lk5FWFRfUlVOVElNRSA9PT0gXCJlZGdlXCIpIHtcbiAgICBtb2R1bGUuZXhwb3J0cyA9IHJlcXVpcmUoXCJuZXh0L2Rpc3Qvc2VydmVyL2Z1dHVyZS9yb3V0ZS1tb2R1bGVzL2FwcC1yb3V0ZS9tb2R1bGUuanNcIik7XG59IGVsc2Uge1xuICAgIGlmIChwcm9jZXNzLmVudi5fX05FWFRfRVhQRVJJTUVOVEFMX1JFQUNUKSB7XG4gICAgICAgIGlmIChwcm9jZXNzLmVudi5OT0RFX0VOViA9PT0gXCJkZXZlbG9wbWVudFwiKSB7XG4gICAgICAgICAgICBtb2R1bGUuZXhwb3J0cyA9IHJlcXVpcmUoXCJuZXh0L2Rpc3QvY29tcGlsZWQvbmV4dC1zZXJ2ZXIvYXBwLXJvdXRlLWV4cGVyaW1lbnRhbC5ydW50aW1lLmRldi5qc1wiKTtcbiAgICAgICAgfSBlbHNlIGlmIChwcm9jZXNzLmVudi5UVVJCT1BBQ0spIHtcbiAgICAgICAgICAgIG1vZHVsZS5leHBvcnRzID0gcmVxdWlyZShcIm5leHQvZGlzdC9jb21waWxlZC9uZXh0LXNlcnZlci9hcHAtcm91dGUtdHVyYm8tZXhwZXJpbWVudGFsLnJ1bnRpbWUucHJvZC5qc1wiKTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIG1vZHVsZS5leHBvcnRzID0gcmVxdWlyZShcIm5leHQvZGlzdC9jb21waWxlZC9uZXh0LXNlcnZlci9hcHAtcm91dGUtZXhwZXJpbWVudGFsLnJ1bnRpbWUucHJvZC5qc1wiKTtcbiAgICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICAgIGlmIChwcm9jZXNzLmVudi5OT0RFX0VOViA9PT0gXCJkZXZlbG9wbWVudFwiKSB7XG4gICAgICAgICAgICBtb2R1bGUuZXhwb3J0cyA9IHJlcXVpcmUoXCJuZXh0L2Rpc3QvY29tcGlsZWQvbmV4dC1zZXJ2ZXIvYXBwLXJvdXRlLnJ1bnRpbWUuZGV2LmpzXCIpO1xuICAgICAgICB9IGVsc2UgaWYgKHByb2Nlc3MuZW52LlRVUkJPUEFDSykge1xuICAgICAgICAgICAgbW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKFwibmV4dC9kaXN0L2NvbXBpbGVkL25leHQtc2VydmVyL2FwcC1yb3V0ZS10dXJiby5ydW50aW1lLnByb2QuanNcIik7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICBtb2R1bGUuZXhwb3J0cyA9IHJlcXVpcmUoXCJuZXh0L2Rpc3QvY29tcGlsZWQvbmV4dC1zZXJ2ZXIvYXBwLXJvdXRlLnJ1bnRpbWUucHJvZC5qc1wiKTtcbiAgICAgICAgfVxuICAgIH1cbn1cblxuLy8jIHNvdXJjZU1hcHBpbmdVUkw9bW9kdWxlLmNvbXBpbGVkLmpzLm1hcCJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(rsc)/./node_modules/next/dist/server/future/route-modules/app-route/module.compiled.js\n");
/***/ }),
/***/ "(rsc)/./node_modules/next/dist/server/lib/clone-response.js":
/*!*************************************************************!*\
!*** ./node_modules/next/dist/server/lib/clone-response.js ***!

View File

@ -125,7 +125,7 @@
/******/
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("1c3e3006bc57475f")
/******/ __webpack_require__.h = () => ("c8994b8599cfdbb2")
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */

File diff suppressed because one or more lines are too long

View File

@ -25,7 +25,7 @@ eval(__webpack_require__.ts("Promise.resolve(/*! import() eager */).then(__webpa
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (\"66e5cf568d04\");\nif (true) { module.hot.accept() }\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKGFwcC1wYWdlcy1icm93c2VyKS8uL3NyYy9hcHAvZ2xvYmFscy5jc3MiLCJtYXBwaW5ncyI6IjtBQUFBLCtEQUFlLGNBQWM7QUFDN0IsSUFBSSxJQUFVLElBQUksaUJBQWlCIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vX05fRS8uL3NyYy9hcHAvZ2xvYmFscy5jc3M/OTc1OSJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZGVmYXVsdCBcIjY2ZTVjZjU2OGQwNFwiXG5pZiAobW9kdWxlLmhvdCkgeyBtb2R1bGUuaG90LmFjY2VwdCgpIH1cbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(app-pages-browser)/./src/app/globals.css\n"));
eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (\"41402df187ff\");\nif (true) { module.hot.accept() }\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKGFwcC1wYWdlcy1icm93c2VyKS8uL3NyYy9hcHAvZ2xvYmFscy5jc3MiLCJtYXBwaW5ncyI6IjtBQUFBLCtEQUFlLGNBQWM7QUFDN0IsSUFBSSxJQUFVLElBQUksaUJBQWlCIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vX05fRS8uL3NyYy9hcHAvZ2xvYmFscy5jc3M/OTc1OSJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZGVmYXVsdCBcIjQxNDAyZGYxODdmZlwiXG5pZiAobW9kdWxlLmhvdCkgeyBtb2R1bGUuaG90LmFjY2VwdCgpIH1cbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(app-pages-browser)/./src/app/globals.css\n"));
/***/ })

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -163,7 +163,7 @@
/******/ // This function allow to reference async chunks
/******/ __webpack_require__.u = function(chunkId) {
/******/ // return url for filenames based on template
/******/ return undefined;
/******/ return "static/chunks/" + chunkId + ".js";
/******/ };
/******/ }();
/******/
@ -192,7 +192,7 @@
/******/
/******/ /* webpack/runtime/getFullHash */
/******/ !function() {
/******/ __webpack_require__.h = function() { return "12b9b356febc7fba"; }
/******/ __webpack_require__.h = function() { return "18bfcaa4d79c56aa"; }
/******/ }();
/******/
/******/ /* webpack/runtime/global */

View File

@ -562,9 +562,6 @@ video {
.right-0{
right: 0px;
}
.top-0{
top: 0px;
}
.z-40{
z-index: 40;
}
@ -610,18 +607,21 @@ video {
.ml-0\.5{
margin-left: 0.125rem;
}
.ml-1{
margin-left: 0.25rem;
}
.ml-1\.5{
margin-left: 0.375rem;
}
.ml-2{
margin-left: 0.5rem;
}
.mt-0\.5{
margin-top: 0.125rem;
}
.mt-1{
margin-top: 0.25rem;
}
.mt-1\.5{
margin-top: 0.375rem;
}
.mt-2{
margin-top: 0.5rem;
}
@ -682,6 +682,9 @@ video {
.h-56{
height: 14rem;
}
.h-6{
height: 1.5rem;
}
.h-64{
height: 16rem;
}
@ -700,9 +703,6 @@ video {
.min-h-screen{
min-height: 100vh;
}
.w-0\.5{
width: 0.125rem;
}
.w-1{
width: 0.25rem;
}
@ -721,6 +721,9 @@ video {
.w-3{
width: 0.75rem;
}
.w-6{
width: 1.5rem;
}
.w-60{
width: 15rem;
}
@ -733,6 +736,12 @@ video {
.w-full{
width: 100%;
}
.min-w-\[36px\]{
min-width: 36px;
}
.min-w-\[60px\]{
min-width: 60px;
}
.max-w-3xl{
max-width: 48rem;
}
@ -828,6 +837,9 @@ video {
.grid-cols-2{
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-3{
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-cols-4{
grid-template-columns: repeat(4, minmax(0, 1fr));
}
@ -870,9 +882,6 @@ video {
.gap-2{
gap: 0.5rem;
}
.gap-2\.5{
gap: 0.625rem;
}
.gap-3{
gap: 0.75rem;
}
@ -892,6 +901,11 @@ video {
margin-top: calc(0.375rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.375rem * var(--tw-space-y-reverse));
}
.space-y-2 > :not([hidden]) ~ :not([hidden]){
--tw-space-y-reverse: 0;
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
}
.space-y-4 > :not([hidden]) ~ :not([hidden]){
--tw-space-y-reverse: 0;
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
@ -919,9 +933,6 @@ video {
.whitespace-nowrap{
white-space: nowrap;
}
.rounded{
border-radius: 0.25rem;
}
.rounded-2xl{
border-radius: 16px;
}
@ -955,6 +966,9 @@ video {
.border-accent-indigo\/\[0\.12\]{
border-color: rgb(129 140 248 / 0.12);
}
.border-amber-500\/20{
border-color: rgb(245 158 11 / 0.2);
}
.border-amber-500\/\[0\.08\]{
border-color: rgb(245 158 11 / 0.08);
}
@ -967,9 +981,15 @@ video {
.border-orange-500\/15{
border-color: rgb(249 115 22 / 0.15);
}
.border-orange-600\/15{
border-color: rgb(234 88 12 / 0.15);
}
.border-red-500\/10{
border-color: rgb(239 68 68 / 0.1);
}
.border-slate-400\/15{
border-color: rgb(148 163 184 / 0.15);
}
.border-slate-800\/50{
border-color: rgb(30 41 59 / 0.5);
}
@ -1031,11 +1051,15 @@ video {
--tw-bg-opacity: 1;
background-color: rgb(251 146 60 / var(--tw-bg-opacity, 1));
}
.bg-orange-500\/20{
background-color: rgb(249 115 22 / 0.2);
}
.bg-orange-500\/60{
background-color: rgb(249 115 22 / 0.6);
}
.bg-orange-500\/\[0\.08\]{
background-color: rgb(249 115 22 / 0.08);
.bg-red-400{
--tw-bg-opacity: 1;
background-color: rgb(248 113 113 / var(--tw-bg-opacity, 1));
}
.bg-red-500\/\[0\.08\]{
background-color: rgb(239 68 68 / 0.08);
@ -1073,6 +1097,11 @@ video {
--tw-gradient-to: rgb(129 140 248 / 0) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.from-amber-500\/30{
--tw-gradient-from: rgb(245 158 11 / 0.3) var(--tw-gradient-from-position);
--tw-gradient-to: rgb(245 158 11 / 0) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.from-orange-500{
--tw-gradient-from: #f97316 var(--tw-gradient-from-position);
--tw-gradient-to: rgb(249 115 22 / 0) var(--tw-gradient-to-position);
@ -1088,6 +1117,16 @@ video {
--tw-gradient-to: rgb(249 115 22 / 0) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.from-orange-700\/20{
--tw-gradient-from: rgb(194 65 12 / 0.2) var(--tw-gradient-from-position);
--tw-gradient-to: rgb(194 65 12 / 0) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.from-slate-400\/20{
--tw-gradient-from: rgb(148 163 184 / 0.2) var(--tw-gradient-from-position);
--tw-gradient-to: rgb(148 163 184 / 0) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.from-transparent{
--tw-gradient-from: transparent var(--tw-gradient-from-position);
--tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);
@ -1115,6 +1154,15 @@ video {
.to-amber-600{
--tw-gradient-to: #d97706 var(--tw-gradient-to-position);
}
.to-amber-600\/20{
--tw-gradient-to: rgb(217 119 6 / 0.2) var(--tw-gradient-to-position);
}
.to-orange-800\/15{
--tw-gradient-to: rgb(154 52 18 / 0.15) var(--tw-gradient-to-position);
}
.to-slate-500\/15{
--tw-gradient-to: rgb(100 116 139 / 0.15) var(--tw-gradient-to-position);
}
.to-transparent{
--tw-gradient-to: transparent var(--tw-gradient-to-position);
}
@ -1130,10 +1178,6 @@ video {
.p-5{
padding: 1.25rem;
}
.px-1\.5{
padding-left: 0.375rem;
padding-right: 0.375rem;
}
.px-2{
padding-left: 0.5rem;
padding-right: 0.5rem;
@ -1158,6 +1202,10 @@ video {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
}
.py-1{
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.py-1\.5{
padding-top: 0.375rem;
padding-bottom: 0.375rem;
@ -1232,18 +1280,9 @@ video {
font-size: 1.875rem;
line-height: 2.25rem;
}
.text-\[10px\]{
font-size: 10px;
}
.text-\[11px\]{
font-size: 11px;
}
.text-\[13px\]{
font-size: 13px;
}
.text-\[9px\]{
font-size: 9px;
}
.text-base{
font-size: 1rem;
line-height: 1.5rem;
@ -1308,6 +1347,10 @@ video {
.text-accent-indigo\/80{
color: rgb(129 140 248 / 0.8);
}
.text-amber-400{
--tw-text-opacity: 1;
color: rgb(251 191 36 / var(--tw-text-opacity, 1));
}
.text-amber-500\/60{
color: rgb(245 158 11 / 0.6);
}
@ -1315,6 +1358,9 @@ video {
--tw-text-opacity: 1;
color: rgb(52 211 153 / var(--tw-text-opacity, 1));
}
.text-emerald-400\/60{
color: rgb(52 211 153 / 0.6);
}
.text-emerald-400\/80{
color: rgb(52 211 153 / 0.8);
}
@ -1326,13 +1372,14 @@ video {
--tw-text-opacity: 1;
color: rgb(251 146 60 / var(--tw-text-opacity, 1));
}
.text-orange-400\/80{
color: rgb(251 146 60 / 0.8);
}
.text-red-400{
--tw-text-opacity: 1;
color: rgb(248 113 113 / var(--tw-text-opacity, 1));
}
.text-slate-300{
--tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.text-text-muted{
--tw-text-opacity: 1;
color: rgb(84 99 128 / var(--tw-text-opacity, 1));
@ -1419,6 +1466,9 @@ video {
.duration-300{
transition-duration: 300ms;
}
.duration-500{
transition-duration: 500ms;
}
.duration-700{
transition-duration: 700ms;
}

View File

@ -1 +0,0 @@
{"c":["webpack"],"r":[],"m":[]}

View File

@ -1 +0,0 @@
{"c":["app/layout","app/page","app/recommendations/page","webpack"],"r":[],"m":[]}

View File

@ -1 +0,0 @@
{"c":["app/layout","webpack"],"r":["app/_not-found/page"],"m":["(app-pages-browser)/./node_modules/next/dist/build/webpack/loaders/next-client-pages-loader.js?absolutePagePath=%2FUsers%2Faaron%2Fsource_code%2Fastock-agent%2Ffrontend%2Fnode_modules%2Fnext%2Fdist%2Fclient%2Fcomponents%2Fnot-found-error.js&page=%2F_not-found%2Fpage!","(app-pages-browser)/./node_modules/next/dist/client/components/not-found-error.js"]}

View File

@ -1,22 +0,0 @@
"use strict";
/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
self["webpackHotUpdate_N_E"]("app/layout",{
/***/ "(app-pages-browser)/./src/app/globals.css":
/*!*****************************!*\
!*** ./src/app/globals.css ***!
\*****************************/
/***/ (function(module, __webpack_exports__, __webpack_require__) {
eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (\"66e5cf568d04\");\nif (true) { module.hot.accept() }\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKGFwcC1wYWdlcy1icm93c2VyKS8uL3NyYy9hcHAvZ2xvYmFscy5jc3MiLCJtYXBwaW5ncyI6IjtBQUFBLCtEQUFlLGNBQWM7QUFDN0IsSUFBSSxJQUFVLElBQUksaUJBQWlCIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vX05fRS8uL3NyYy9hcHAvZ2xvYmFscy5jc3M/OTc1OSJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZGVmYXVsdCBcIjY2ZTVjZjU2OGQwNFwiXG5pZiAobW9kdWxlLmhvdCkgeyBtb2R1bGUuaG90LmFjY2VwdCgpIH1cbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(app-pages-browser)/./src/app/globals.css\n"));
/***/ })
});

View File

@ -1,22 +0,0 @@
"use strict";
/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
self["webpackHotUpdate_N_E"]("app/layout",{
/***/ "(app-pages-browser)/./src/app/globals.css":
/*!*****************************!*\
!*** ./src/app/globals.css ***!
\*****************************/
/***/ (function(module, __webpack_exports__, __webpack_require__) {
eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (\"02fabde643f0\");\nif (true) { module.hot.accept() }\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKGFwcC1wYWdlcy1icm93c2VyKS8uL3NyYy9hcHAvZ2xvYmFscy5jc3MiLCJtYXBwaW5ncyI6IjtBQUFBLCtEQUFlLGNBQWM7QUFDN0IsSUFBSSxJQUFVLElBQUksaUJBQWlCIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vX05fRS8uL3NyYy9hcHAvZ2xvYmFscy5jc3M/OTc1OSJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZGVmYXVsdCBcIjAyZmFiZGU2NDNmMFwiXG5pZiAobW9kdWxlLmhvdCkgeyBtb2R1bGUuaG90LmFjY2VwdCgpIH1cbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(app-pages-browser)/./src/app/globals.css\n"));
/***/ })
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"c":["webpack"],"r":[],"m":[]}

View File

@ -1,18 +0,0 @@
"use strict";
/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
self["webpackHotUpdate_N_E"]("webpack",{},
/******/ function(__webpack_require__) { // webpackRuntimeModules
/******/ /* webpack/runtime/getFullHash */
/******/ !function() {
/******/ __webpack_require__.h = function() { return "1b777160410063de"; }
/******/ }();
/******/
/******/ }
);

View File

@ -1,18 +0,0 @@
"use strict";
/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
self["webpackHotUpdate_N_E"]("webpack",{},
/******/ function(__webpack_require__) { // webpackRuntimeModules
/******/ /* webpack/runtime/getFullHash */
/******/ !function() {
/******/ __webpack_require__.h = function() { return "12b9b356febc7fba"; }
/******/ }();
/******/
/******/ }
);

View File

@ -1,18 +0,0 @@
"use strict";
/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
self["webpackHotUpdate_N_E"]("webpack",{},
/******/ function(__webpack_require__) { // webpackRuntimeModules
/******/ /* webpack/runtime/getFullHash */
/******/ !function() {
/******/ __webpack_require__.h = function() { return "0102527ee05174f3"; }
/******/ }();
/******/
/******/ }
);

View File

@ -1,60 +0,0 @@
"use strict";
/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
self["webpackHotUpdate_N_E"]("webpack",{},
/******/ function(__webpack_require__) { // webpackRuntimeModules
/******/ /* webpack/runtime/compat get default export */
/******/ !function() {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function() { return module['default']; } :
/******/ function() { return module; };
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/create fake namespace object */
/******/ !function() {
/******/ var getProto = Object.getPrototypeOf ? function(obj) { return Object.getPrototypeOf(obj); } : function(obj) { return obj.__proto__; };
/******/ var leafPrototypes;
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 16: return value when it's Promise-like
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = this(value);
/******/ if(mode & 8) return value;
/******/ if(typeof value === 'object' && value) {
/******/ if((mode & 4) && value.__esModule) return value;
/******/ if((mode & 16) && typeof value.then === 'function') return value;
/******/ }
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ var def = {};
/******/ leafPrototypes = leafPrototypes || [null, getProto({}), getProto([]), getProto(getProto)];
/******/ for(var current = mode & 2 && value; typeof current == 'object' && !~leafPrototypes.indexOf(current); current = getProto(current)) {
/******/ Object.getOwnPropertyNames(current).forEach(function(key) { def[key] = function() { return value[key]; }; });
/******/ }
/******/ def['default'] = function() { return value; };
/******/ __webpack_require__.d(ns, def);
/******/ return ns;
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/getFullHash */
/******/ !function() {
/******/ __webpack_require__.h = function() { return "2eda11c00b2dd330"; }
/******/ }();
/******/
/******/ }
);

File diff suppressed because one or more lines are too long

View File

@ -1,79 +0,0 @@
// File: /Users/aaron/source_code/astock-agent/frontend/src/app/page.tsx
import * as entry from '../../../src/app/page.js'
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
type TEntry = typeof import('../../../src/app/page.js')
// Check that the entry is a valid entry
checkFields<Diff<{
default: Function
config?: {}
generateStaticParams?: Function
revalidate?: RevalidateRange<TEntry> | false
dynamic?: 'auto' | 'force-dynamic' | 'error' | 'force-static'
dynamicParams?: boolean
fetchCache?: 'auto' | 'force-no-store' | 'only-no-store' | 'default-no-store' | 'default-cache' | 'only-cache' | 'force-cache'
preferredRegion?: 'auto' | 'global' | 'home' | string | string[]
runtime?: 'nodejs' | 'experimental-edge' | 'edge'
maxDuration?: number
metadata?: any
generateMetadata?: Function
viewport?: any
generateViewport?: Function
}, TEntry, ''>>()
// Check the prop type of the entry function
checkFields<Diff<PageProps, FirstArg<TEntry['default']>, 'default'>>()
// Check the arguments and return type of the generateMetadata function
if ('generateMetadata' in entry) {
checkFields<Diff<PageProps, FirstArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
checkFields<Diff<ResolvingMetadata, SecondArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
}
// Check the arguments and return type of the generateViewport function
if ('generateViewport' in entry) {
checkFields<Diff<PageProps, FirstArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
checkFields<Diff<ResolvingViewport, SecondArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
}
// Check the arguments and return type of the generateStaticParams function
if ('generateStaticParams' in entry) {
checkFields<Diff<{ params: PageParams }, FirstArg<MaybeField<TEntry, 'generateStaticParams'>>, 'generateStaticParams'>>()
checkFields<Diff<{ __tag__: 'generateStaticParams', __return_type__: any[] | Promise<any[]> }, { __tag__: 'generateStaticParams', __return_type__: ReturnType<MaybeField<TEntry, 'generateStaticParams'>> }>>()
}
type PageParams = any
export interface PageProps {
params?: any
searchParams?: any
}
export interface LayoutProps {
children?: React.ReactNode
params?: any
}
// =============
// Utility types
type RevalidateRange<T> = T extends { revalidate: any } ? NonNegative<T['revalidate']> : never
// If T is unknown or any, it will be an empty {} type. Otherwise, it will be the same as Omit<T, keyof Base>.
type OmitWithTag<T, K extends keyof any, _M> = Omit<T, K>
type Diff<Base, T extends Base, Message extends string = ''> = 0 extends (1 & T) ? {} : OmitWithTag<T, keyof Base, Message>
type FirstArg<T extends Function> = T extends (...args: [infer T, any]) => any ? unknown extends T ? any : T : never
type SecondArg<T extends Function> = T extends (...args: [any, infer T]) => any ? unknown extends T ? any : T : never
type MaybeField<T, K extends string> = T extends { [k in K]: infer G } ? G extends Function ? G : never : never
function checkFields<_ extends { [k in keyof any]: never }>() {}
// https://github.com/sindresorhus/type-fest
type Numeric = number | bigint
type Zero = 0 | 0n
type Negative<T extends Numeric> = T extends Zero ? never : `${T}` extends `-${string}` ? T : never
type NonNegative<T extends Numeric> = T extends Zero ? T : Negative<T> extends never ? T : '__invalid_negative_number__'

View File

@ -1,79 +0,0 @@
// File: /Users/aaron/source_code/astock-agent/frontend/src/app/sectors/page.tsx
import * as entry from '../../../../src/app/sectors/page.js'
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
type TEntry = typeof import('../../../../src/app/sectors/page.js')
// Check that the entry is a valid entry
checkFields<Diff<{
default: Function
config?: {}
generateStaticParams?: Function
revalidate?: RevalidateRange<TEntry> | false
dynamic?: 'auto' | 'force-dynamic' | 'error' | 'force-static'
dynamicParams?: boolean
fetchCache?: 'auto' | 'force-no-store' | 'only-no-store' | 'default-no-store' | 'default-cache' | 'only-cache' | 'force-cache'
preferredRegion?: 'auto' | 'global' | 'home' | string | string[]
runtime?: 'nodejs' | 'experimental-edge' | 'edge'
maxDuration?: number
metadata?: any
generateMetadata?: Function
viewport?: any
generateViewport?: Function
}, TEntry, ''>>()
// Check the prop type of the entry function
checkFields<Diff<PageProps, FirstArg<TEntry['default']>, 'default'>>()
// Check the arguments and return type of the generateMetadata function
if ('generateMetadata' in entry) {
checkFields<Diff<PageProps, FirstArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
checkFields<Diff<ResolvingMetadata, SecondArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
}
// Check the arguments and return type of the generateViewport function
if ('generateViewport' in entry) {
checkFields<Diff<PageProps, FirstArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
checkFields<Diff<ResolvingViewport, SecondArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
}
// Check the arguments and return type of the generateStaticParams function
if ('generateStaticParams' in entry) {
checkFields<Diff<{ params: PageParams }, FirstArg<MaybeField<TEntry, 'generateStaticParams'>>, 'generateStaticParams'>>()
checkFields<Diff<{ __tag__: 'generateStaticParams', __return_type__: any[] | Promise<any[]> }, { __tag__: 'generateStaticParams', __return_type__: ReturnType<MaybeField<TEntry, 'generateStaticParams'>> }>>()
}
type PageParams = any
export interface PageProps {
params?: any
searchParams?: any
}
export interface LayoutProps {
children?: React.ReactNode
params?: any
}
// =============
// Utility types
type RevalidateRange<T> = T extends { revalidate: any } ? NonNegative<T['revalidate']> : never
// If T is unknown or any, it will be an empty {} type. Otherwise, it will be the same as Omit<T, keyof Base>.
type OmitWithTag<T, K extends keyof any, _M> = Omit<T, K>
type Diff<Base, T extends Base, Message extends string = ''> = 0 extends (1 & T) ? {} : OmitWithTag<T, keyof Base, Message>
type FirstArg<T extends Function> = T extends (...args: [infer T, any]) => any ? unknown extends T ? any : T : never
type SecondArg<T extends Function> = T extends (...args: [any, infer T]) => any ? unknown extends T ? any : T : never
type MaybeField<T, K extends string> = T extends { [k in K]: infer G } ? G extends Function ? G : never : never
function checkFields<_ extends { [k in keyof any]: never }>() {}
// https://github.com/sindresorhus/type-fest
type Numeric = number | bigint
type Zero = 0 | 0n
type Negative<T extends Numeric> = T extends Zero ? never : `${T}` extends `-${string}` ? T : never
type NonNegative<T extends Numeric> = T extends Zero ? T : Negative<T> extends never ? T : '__invalid_negative_number__'

View File

@ -0,0 +1,35 @@
import { NextRequest } from "next/server";
const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:8000";
export async function POST(req: NextRequest) {
const body = await req.json();
const backendRes = await fetch(`${BACKEND_URL}/api/chat/stream`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!backendRes.ok) {
return new Response(
JSON.stringify({ error: `Backend error: ${backendRes.status}` }),
{ status: backendRes.status, headers: { "Content-Type": "application/json" } }
);
}
if (!backendRes.body) {
return new Response(JSON.stringify({ error: "No response body" }), {
status: 502,
headers: { "Content-Type": "application/json" },
});
}
return new Response(backendRes.body, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}

View File

@ -96,7 +96,7 @@ export default function ChatPage() {
</div>
<div>
<h1 className="text-sm font-semibold">AI </h1>
<p className="text-[10px] text-text-muted">
<p className="text-xs text-text-muted">
</p>
</div>
@ -104,7 +104,7 @@ export default function ChatPage() {
{messages.length > 0 && (
<button
onClick={() => setMessages([])}
className="text-[10px] text-text-muted hover:text-text-primary px-3 py-1.5 rounded-lg hover:bg-white/[0.04] transition-all duration-200"
className="text-xs text-text-muted hover:text-text-primary px-3 py-1.5 rounded-lg hover:bg-white/[0.04] transition-all duration-200"
>
</button>
@ -121,7 +121,7 @@ export default function ChatPage() {
</svg>
</div>
<h2 className="text-sm font-semibold mb-1.5"></h2>
<p className="text-[11px] text-text-muted mb-8 max-w-[240px] leading-relaxed">
<p className="text-xs text-text-muted mb-8 max-w-[240px] leading-relaxed">
</p>
<div className="flex flex-col gap-2 w-full max-w-[280px]">
@ -129,7 +129,7 @@ export default function ChatPage() {
<button
key={q}
onClick={() => sendMessage(q)}
className="text-[11px] px-4 py-2.5 bg-white/[0.03] rounded-xl text-text-secondary hover:text-text-primary hover:bg-white/[0.06] transition-all duration-200 border border-white/[0.04] text-left"
className="text-xs px-4 py-2.5 bg-white/[0.03] rounded-xl text-text-secondary hover:text-text-primary hover:bg-white/[0.06] transition-all duration-200 border border-white/[0.04] text-left"
>
{q}
</button>
@ -175,7 +175,7 @@ export default function ChatPage() {
{/* Status indicator during tool calls */}
{streaming && status && messages[messages.length - 1]?.content && (
<div className="flex justify-start">
<div className="text-[11px] text-accent-indigo/50 flex items-center gap-2 px-3">
<div className="text-xs text-accent-indigo/50 flex items-center gap-2 px-3">
<span className="inline-block w-2.5 h-2.5 border border-accent-indigo/30 border-t-accent-indigo/70 rounded-full animate-spin" />
{status}
</div>
@ -206,7 +206,7 @@ export default function ChatPage() {
</button>
</div>
<div className="text-[9px] text-text-muted/30 text-center mt-2">
<div className="text-xs text-text-muted/30 text-center mt-2">
AI
</div>
</div>

View File

@ -33,7 +33,7 @@ export default function RootLayout({
</div>
<div>
<h1 className="text-sm font-semibold tracking-tight">Dragon AI Agent</h1>
<p className="text-[10px] text-text-muted mt-0.5 font-light tracking-wide"> · </p>
<p className="text-xs text-text-muted mt-0.5 font-light tracking-wide"> · </p>
</div>
</div>
</div>
@ -51,7 +51,7 @@ export default function RootLayout({
{/* Footer */}
<div className="px-6 py-5 border-t border-slate-800/50">
<div className="text-[10px] text-text-muted leading-relaxed">
<div className="text-xs text-text-muted leading-relaxed">
<div className="flex items-center gap-1.5 mb-1">
<span className="w-1 h-1 rounded-full bg-emerald-500" />
<span>Tushare Pro + </span>
@ -105,7 +105,7 @@ function MobileNavItem({ href, label, children }: { href: string; label: string;
className="flex flex-col items-center gap-1 text-text-muted hover:text-text-primary transition-colors active:scale-95"
>
<span className="text-lg">{children}</span>
<span className="text-[10px] font-medium">{label}</span>
<span className="text-xs font-medium">{label}</span>
</a>
);
}

View File

@ -2,7 +2,7 @@
import { useEffect, useState, useCallback } from "react";
import { fetchAPI, postAPI } from "@/lib/api";
import type { LatestResult, SectorData } from "@/lib/api";
import type { LatestResult, SectorData, IndexOverview } from "@/lib/api";
import MarketTemp from "@/components/market-temp";
import StockCard from "@/components/stock-card";
import SectorHeatmap from "@/components/sector-heatmap";
@ -22,19 +22,22 @@ export default function DashboardPage() {
const [refreshing, setRefreshing] = useState(false);
const [refreshResult, setRefreshResult] = useState<string | null>(null);
const [llmEnabled, setLlmEnabled] = useState(false);
const [indices, setIndices] = useState<IndexOverview[]>([]);
const loadData = useCallback(async () => {
try {
const [latest, sectorData, status, health] = await Promise.all([
const [latest, sectorData, status, health, overview] = await Promise.all([
fetchAPI<LatestResult>("/api/recommendations/latest"),
fetchAPI<SectorData[]>("/api/sectors/hot?limit=8"),
fetchAPI<ScanStatus>("/api/recommendations/status"),
fetchAPI<{ llm_enabled: boolean }>("/api/health"),
fetchAPI<IndexOverview[]>("/api/market/overview").catch(() => []),
]);
setData(latest);
setSectors(sectorData);
setScanStatus(status);
setLlmEnabled(health.llm_enabled);
setIndices(overview);
} catch (e) {
console.error("加载数据失败:", e);
} finally {
@ -92,7 +95,7 @@ export default function DashboardPage() {
<div>
<h1 className="text-lg font-bold md:hidden tracking-tight">Dragon AI Agent</h1>
{scanStatus && (
<p className="text-[11px] text-text-muted mt-1">
<p className="text-xs text-text-muted mt-1">
{scanStatus.is_trading ? (
<span className="inline-flex items-center gap-1.5">
<span className="w-1.5 h-1.5 bg-emerald-400 rounded-full animate-pulse" />
@ -139,7 +142,7 @@ export default function DashboardPage() {
{/* Market temp + Sector heatmap */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<MarketTemp data={data?.market_temperature ?? null} />
<MarketTemp data={data?.market_temperature ?? null} indices={indices} />
<SectorHeatmap sectors={sectors} />
</div>
@ -152,7 +155,7 @@ export default function DashboardPage() {
<span className="text-text-primary ml-1.5 font-mono tabular-nums">{data.recommendations.length}</span>
) : ""}
</h2>
<a href="/recommendations" className="text-[11px] text-text-muted hover:text-orange-400 transition-colors flex items-center gap-1">
<a href="/recommendations" className="text-xs text-text-muted hover:text-orange-400 transition-colors flex items-center gap-1">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 12h14M12 5l7 7-7 7" />

View File

@ -48,7 +48,7 @@ export default function RecommendationsPage() {
<div className="flex items-center justify-between mb-5 animate-fade-in-up">
<div>
<h1 className="text-lg font-bold tracking-tight"></h1>
<p className="text-[11px] text-text-muted mt-0.5">
<p className="text-xs text-text-muted mt-0.5">
<span className="font-mono tabular-nums">{filtered.length}</span>
</p>
</div>
@ -66,7 +66,7 @@ export default function RecommendationsPage() {
<button
key={key}
onClick={() => setFilter(key)}
className={`text-[11px] px-4 py-1.5 rounded-xl whitespace-nowrap transition-all duration-200 font-medium ${
className={`text-xs px-4 py-1.5 rounded-xl whitespace-nowrap transition-all duration-200 font-medium ${
filter === key
? "bg-gradient-to-r from-orange-500/25 to-amber-500/25 text-orange-400 border border-orange-500/15"
: "bg-white/[0.03] text-text-muted hover:text-text-secondary hover:bg-white/[0.06] border border-transparent"

View File

@ -16,7 +16,7 @@ export default function SectorsPage() {
<div className="max-w-5xl mx-auto px-4 md:px-8 pt-6 pb-20 md:pb-10">
<div className="mb-5 animate-fade-in-up">
<h1 className="text-lg font-bold tracking-tight"></h1>
<p className="text-[11px] text-text-muted mt-0.5"></p>
<p className="text-xs text-text-muted mt-0.5"></p>
</div>
<SectorHeatmap sectors={sectors} />
</div>

View File

@ -69,7 +69,7 @@ export default function StockDetailPage() {
return (
<div className="max-w-6xl mx-auto px-4 md:px-8 pt-6 pb-20 md:pb-10 space-y-5">
{/* Back */}
<a href="/" className="inline-flex items-center gap-1.5 text-[11px] text-text-muted hover:text-text-primary transition-colors animate-fade-in-up">
<a href="/" className="inline-flex items-center gap-1.5 text-xs text-text-muted hover:text-text-primary transition-colors animate-fade-in-up">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M19 12H5M12 19l-7-7 7-7" />
</svg>
@ -84,7 +84,7 @@ export default function StockDetailPage() {
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<span className="text-lg font-bold tracking-tight">{quote.name}</span>
<span className="text-[11px] text-text-muted font-mono tabular-nums">{quote.ts_code}</span>
<span className="text-xs text-text-muted font-mono tabular-nums">{quote.ts_code}</span>
</div>
</div>
<div className="flex items-baseline gap-3">
@ -174,7 +174,7 @@ export default function StockDetailPage() {
function QuoteStat({ label, value }: { label: string; value: string }) {
return (
<div className="bg-white/[0.02] rounded-lg px-3 py-2 border border-white/[0.03]">
<div className="text-[9px] text-text-muted uppercase tracking-wider mb-0.5">{label}</div>
<div className="text-xs text-text-muted mb-0.5">{label}</div>
<div className="text-xs font-mono tabular-nums">{value}</div>
</div>
);

View File

@ -1,13 +1,18 @@
"use client";
import { getTempColor, getTempLabel } from "@/lib/utils";
import type { MarketTemperatureData } from "@/lib/api";
import type { MarketTemperatureData, IndexOverview } from "@/lib/api";
export default function MarketTemp({ data }: { data: MarketTemperatureData | null }) {
interface MarketTempProps {
data: MarketTemperatureData | null;
indices?: IndexOverview[];
}
export default function MarketTemp({ data, indices }: MarketTempProps) {
if (!data) {
return (
<div className="glass-card-static p-5 animate-fade-in-up">
<div className="h-24 animate-shimmer rounded-lg" />
<div className="h-32 animate-shimmer rounded-lg" />
</div>
);
}
@ -15,16 +20,19 @@ export default function MarketTemp({ data }: { data: MarketTemperatureData | nul
const color = getTempColor(data.temperature);
const label = getTempLabel(data.temperature);
const ratio = data.up_count / Math.max(data.down_count, 1);
const hasBrokenRate = data.broken_rate != null && data.broken_rate > 0;
const hasMaxStreak = data.max_streak != null && data.max_streak > 0;
return (
<div className="glass-card-static p-5 animate-fade-in-up">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h2 className="text-xs font-semibold text-text-muted uppercase tracking-wider"></h2>
<span className="text-[10px] text-text-muted font-mono tabular-nums">{data.trade_date}</span>
<span className="text-xs text-text-muted font-mono tabular-nums">{data.trade_date}</span>
</div>
{/* Temperature gauge + stats */}
<div className="flex items-center gap-5">
{/* Top row: Gauge + Stats grid */}
<div className="flex items-center gap-5 mb-4">
{/* Circular gauge */}
<div className="relative w-24 h-24 flex-shrink-0">
<svg viewBox="0 0 100 100" className="w-full h-full -rotate-90">
@ -42,7 +50,6 @@ export default function MarketTemp({ data }: { data: MarketTemperatureData | nul
strokeLinecap="round"
className="transition-all duration-1000 ease-out"
/>
{/* Glow effect */}
<circle
cx="50" cy="50" r="40" fill="none"
stroke={color} strokeWidth="7"
@ -54,30 +61,86 @@ export default function MarketTemp({ data }: { data: MarketTemperatureData | nul
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-xl font-bold font-mono tabular-nums" style={{ color }}>{data.temperature}</span>
<span className="text-[9px] text-text-muted font-medium mt-0.5">{label}</span>
<span className="text-xs text-text-muted font-medium mt-0.5">{label}</span>
</div>
</div>
{/* Stats grid */}
<div className="flex-1 grid grid-cols-2 gap-2">
<StatCard label="涨/跌">
<span className="text-red-400 font-mono tabular-nums">{data.up_count}</span>
<span className="text-text-muted/40"> / </span>
<span className="text-emerald-400 font-mono tabular-nums">{data.down_count}</span>
{/* Key stats grid 3x2 */}
<div className="flex-1 grid grid-cols-3 gap-2">
<StatCard label="涨停">
<span className="text-red-400 font-mono tabular-nums font-semibold">{data.limit_up_count}</span>
</StatCard>
<StatCard label="跌停">
<span className="text-emerald-400 font-mono tabular-nums font-semibold">{data.limit_down_count ?? 0}</span>
</StatCard>
<StatCard label="涨跌比">
<span className={`font-mono tabular-nums font-medium ${ratio > 1 ? "text-red-400" : "text-emerald-400"}`}>
<span className={`font-mono tabular-nums font-semibold ${ratio > 1 ? "text-red-400" : "text-emerald-400"}`}>
{ratio.toFixed(2)}
</span>
</StatCard>
<StatCard label="">
<span className="text-red-400 font-mono tabular-nums font-medium">{data.limit_up_count}</span>
<StatCard label="涨">
<span className="text-red-400 font-mono tabular-nums">{data.up_count}</span>
</StatCard>
<StatCard label="连板">
<span className="text-orange-400 font-mono tabular-nums font-medium">{data.max_streak || "-"}</span>
<StatCard label="下跌">
<span className="text-emerald-400 font-mono tabular-nums">{data.down_count}</span>
</StatCard>
<StatCard label={hasMaxStreak ? "最高连板" : "连板"}>
<span className="text-orange-400 font-mono tabular-nums font-semibold">
{hasMaxStreak ? `${data.max_streak}连板` : "-"}
</span>
</StatCard>
</div>
</div>
{/* Middle row: Broken rate + MA20 */}
<div className="grid grid-cols-2 gap-2 mb-2">
<div className="bg-white/[0.02] rounded-lg px-3 py-2 border border-white/[0.03]">
<div className="text-xs text-text-muted mb-0.5"></div>
{hasBrokenRate ? (
<div className="flex items-baseline gap-1">
<span className="text-sm font-mono tabular-nums font-semibold text-amber-400">
{data.broken_rate!.toFixed(1)}%
</span>
{data.broken_rate! > 50 && (
<span className="text-xs text-amber-500/60"></span>
)}
</div>
) : (
<span className="text-sm text-text-muted/40">-</span>
)}
</div>
<div className="bg-white/[0.02] rounded-lg px-3 py-2 border border-white/[0.03]">
<div className="text-xs text-text-muted mb-0.5">线</div>
<div className="flex items-center gap-1.5">
<span className={`w-1.5 h-1.5 rounded-full ${data.index_above_ma20 ? "bg-red-400" : "bg-emerald-400"}`} />
<span className={`text-sm font-semibold ${data.index_above_ma20 ? "text-red-400" : "text-emerald-400"}`}>
{data.index_above_ma20 ? "均线之上" : "均线之下"}
</span>
</div>
</div>
</div>
{/* Bottom row: 3 major indices in one row */}
{indices && indices.length > 0 && (
<div className="grid grid-cols-3 gap-2">
{indices.map((idx) => (
<div key={idx.code} className="bg-white/[0.02] rounded-lg px-3 py-2 border border-white/[0.03]">
<div className="text-xs text-text-muted mb-0.5">
{idx.name}
{idx.realtime && <span className="text-emerald-400/60 ml-1">· </span>}
</div>
<div className="flex items-baseline gap-1.5">
<span className={`text-sm font-mono tabular-nums font-semibold ${idx.pct_chg > 0 ? "text-red-400" : idx.pct_chg < 0 ? "text-emerald-400" : "text-text-primary"}`}>
{idx.close.toFixed(2)}
</span>
<span className={`text-xs font-mono tabular-nums ${idx.pct_chg > 0 ? "text-red-400" : idx.pct_chg < 0 ? "text-emerald-400" : "text-text-muted"}`}>
{idx.pct_chg > 0 ? "+" : ""}{idx.pct_chg.toFixed(2)}%
</span>
</div>
</div>
))}
</div>
)}
</div>
);
}
@ -85,8 +148,8 @@ export default function MarketTemp({ data }: { data: MarketTemperatureData | nul
function StatCard({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="bg-white/[0.02] rounded-lg px-3 py-2 border border-white/[0.03]">
<div className="text-[9px] text-text-muted mb-0.5 font-medium uppercase tracking-wider">{label}</div>
<div className="text-xs">{children}</div>
<div className="text-xs text-text-muted mb-0.5 font-medium">{label}</div>
<div className="text-sm">{children}</div>
</div>
);
}

View File

@ -7,8 +7,8 @@ export default function SectorHeatmap({ sectors }: { sectors: SectorData[] }) {
if (!sectors.length) {
return (
<div className="glass-card-static p-5">
<h2 className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-4"></h2>
<div className="text-xs text-text-muted text-center py-6"></div>
<h2 className="text-sm font-semibold text-text-muted mb-4"></h2>
<div className="text-sm text-text-muted text-center py-6"></div>
</div>
);
}
@ -17,52 +17,73 @@ export default function SectorHeatmap({ sectors }: { sectors: SectorData[] }) {
return (
<div className="glass-card-static p-5">
<h2 className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-4"></h2>
<div className="space-y-1.5">
<h2 className="text-sm font-semibold text-text-muted mb-4"></h2>
<div className="space-y-2">
{sectors.map((s, index) => {
const intensity = s.heat_score / Math.max(maxScore, 1);
const isUp = s.pct_change > 0;
const barColor = isUp
? `rgba(239, 68, 68, ${0.08 + intensity * 0.15})`
: `rgba(34, 197, 94, ${0.08 + intensity * 0.15})`;
const accentColor = isUp
? `rgba(239, 68, 68, ${0.4 + intensity * 0.6})`
: `rgba(34, 197, 94, ${0.4 + intensity * 0.6})`;
const isTop3 = index < 3;
// Bar width based on score relative to max
const barWidth = `${Math.max(intensity * 100, 15)}%`;
return (
<div
key={s.sector_code}
className="relative rounded-lg overflow-hidden animate-fade-in-up"
style={{ animationDelay: `${index * 50}ms` }}
className="relative rounded-xl overflow-hidden animate-fade-in-up"
style={{ animationDelay: `${index * 40}ms` }}
>
{/* Background fill */}
{/* Colored bar background - width proportional to heat score */}
<div
className="absolute inset-0"
style={{ backgroundColor: barColor }}
className="absolute inset-y-0 left-0 rounded-xl transition-all duration-500"
style={{
width: barWidth,
background: isUp
? `linear-gradient(90deg, rgba(239, 68, 68, ${0.06 + intensity * 0.18}), rgba(239, 68, 68, ${0.02 + intensity * 0.06}))`
: `linear-gradient(90deg, rgba(34, 197, 94, ${0.06 + intensity * 0.18}), rgba(34, 197, 94, ${0.02 + intensity * 0.06}))`,
}}
/>
{/* Left accent line */}
<div
className="absolute left-0 top-0 bottom-0 w-0.5"
style={{ backgroundColor: accentColor }}
/>
<div className="relative flex items-center justify-between px-4 py-2.5">
<div className="flex items-center gap-2.5">
<span className="text-sm font-medium">{s.sector_name}</span>
{s.limit_up_count > 0 && (
<span className="text-[10px] text-text-muted bg-white/[0.04] px-1.5 py-0.5 rounded">
{s.limit_up_count}
<div className="relative flex items-center justify-between px-3 py-3">
<div className="flex items-center gap-3">
{/* Rank number */}
<span className={`w-6 h-6 rounded-lg flex items-center justify-center text-xs font-bold shrink-0 ${
index === 0
? "bg-gradient-to-br from-amber-500/30 to-amber-600/20 text-amber-400 border border-amber-500/20"
: index === 1
? "bg-gradient-to-br from-slate-400/20 to-slate-500/15 text-slate-300 border border-slate-400/15"
: index === 2
? "bg-gradient-to-br from-orange-700/20 to-orange-800/15 text-orange-400 border border-orange-600/15"
: "bg-white/[0.03] text-text-muted border border-white/[0.04]"
}`}>
{index + 1}
</span>
<div>
<span className={`text-sm font-medium ${isTop3 ? "text-text-primary" : "text-text-secondary"}`}>
{s.sector_name}
</span>
)}
{s.limit_up_count > 0 && (
<span className="text-xs text-text-muted ml-2">
{s.limit_up_count}
</span>
)}
</div>
</div>
<div className="flex items-center gap-3 text-xs">
<div className="flex items-center gap-4 text-sm">
<span className={`font-mono tabular-nums ${s.capital_inflow > 0 ? "text-red-400" : "text-emerald-400"}`}>
{s.capital_inflow > 0 ? "+" : ""}
{formatNumber(s.capital_inflow)}
</span>
<span className={`font-mono tabular-nums font-medium ${isUp ? "text-red-400" : "text-emerald-400"}`}>
<span className={`font-mono tabular-nums font-semibold min-w-[60px] text-right ${isUp ? "text-red-400" : "text-emerald-400"}`}>
{s.pct_change > 0 ? "+" : ""}
{s.pct_change.toFixed(2)}%
</span>
<span className="text-orange-400/80 font-mono tabular-nums text-[10px] bg-orange-500/[0.08] px-1.5 py-0.5 rounded">
{/* Heat score pill */}
<span className={`font-mono tabular-nums text-xs font-semibold px-2 py-1 rounded-lg min-w-[36px] text-center ${
isTop3
? "bg-orange-500/20 text-orange-400 border border-orange-500/15"
: "bg-white/[0.04] text-text-muted border border-white/[0.04]"
}`}>
{s.heat_score.toFixed(0)}
</span>
</div>

View File

@ -28,6 +28,15 @@ export interface MarketTemperatureData {
index_above_ma20?: boolean;
}
export interface IndexOverview {
name: string;
code: string;
close: number;
pct_chg: number;
volume: number;
realtime: boolean;
}
export interface RecommendationData {
ts_code: string;
name: string;