完善前端
This commit is contained in:
1 parent
7c57d27494
commit
b63542add3
81 files changed
+12075
-1441
No files matched your search
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -2,6 +2,15 @@ import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
// 同源代理:前端 /api/* → 本地后端(避免开发档 CORS;生产由反向代理承担同职责)
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/api/:path*",
|
||||
destination: `${process.env.HUNJING_API_URL ?? "http://127.0.0.1:8000"}/:path*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
Generated
-1421
File diff suppressed because it is too large.
Load diff
@@ -2,12 +2,15 @@
|
||||
"name": "hunjing-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.21.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"generate:api": "openapi-typescript ../openapi/openapi.yaml -o src/types/api.d.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.103.1",
|
||||
"lucide-react": "^1.46.0",
|
||||
"next": "^15.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
|
||||
Generated
+887
@@ -0,0 +1,887 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.103.1
|
||||
version: 5.103.1(react@19.2.8)
|
||||
lucide-react:
|
||||
specifier: ^1.46.0
|
||||
version: 1.46.0(react@19.2.8)
|
||||
next:
|
||||
specifier: ^15.1.0
|
||||
version: 15.5.25(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.8
|
||||
react-dom:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.8(react@19.2.8)
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.10.0
|
||||
version: 22.20.1
|
||||
'@types/react':
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.18
|
||||
'@types/react-dom':
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.7(@types/react@19.2.18)
|
||||
openapi-typescript:
|
||||
specifier: ^7.5.0
|
||||
version: 7.13.0(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
|
||||
packages:
|
||||
|
||||
'@babel/code-frame@7.29.7':
|
||||
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.29.7':
|
||||
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@emnapi/runtime@1.11.3':
|
||||
resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.35.4':
|
||||
resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-freebsd-wasm32@0.35.4':
|
||||
resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
os: [freebsd]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.3.3':
|
||||
resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.3.3':
|
||||
resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.3.3':
|
||||
resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.3.3':
|
||||
resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.3.3':
|
||||
resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.3.3':
|
||||
resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.3.3':
|
||||
resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.3.3':
|
||||
resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.3':
|
||||
resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.3.3':
|
||||
resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.35.4':
|
||||
resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.35.4':
|
||||
resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.35.4':
|
||||
resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.35.4':
|
||||
resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.35.4':
|
||||
resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.35.4':
|
||||
resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.35.4':
|
||||
resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
|
||||
'@img/sharp-webcontainers-wasm32@0.35.4':
|
||||
resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.35.4':
|
||||
resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==}
|
||||
engines: {node: ^20.9.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.35.4':
|
||||
resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@next/env@15.5.25':
|
||||
resolution: {integrity: sha512-42h1lLr07vl4gawALP1hsgRZjHB1xYa58JfUfHwr0f7jG/zhPakh5GHkADHXOC9ZxUvlQFOPIrp7s6qX4DezPQ==}
|
||||
|
||||
'@next/swc-darwin-arm64@15.5.25':
|
||||
resolution: {integrity: sha512-w+RR0v/QuApnWEjRGm1z6gcObKwGMb5YPA7V3bzBEVSBpMFUXprer0tS27UxjUcEnqbhL7Zuzohej79B6rYmBg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@15.5.25':
|
||||
resolution: {integrity: sha512-QiGGBUSakt8S1H4Lt9Ehsh6Ja87axiBnQQgysOObvCbI7iUfJnRGntF1P64S4/ijuHFnSB8KLsEddkY3nN26uw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.5.25':
|
||||
resolution: {integrity: sha512-ehLos/66zo0d/mJCU5u96a/VDcr01aaUrX0o/i16UdInxz8qPTCDSxGtjk/Lps1sIr1RJFdiX3hxc0fxJo+cPA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.5.25':
|
||||
resolution: {integrity: sha512-ZVMrqLiJ7DiChgmbkQwFtdhAnUkSH/4p7tB29QY+giATb0Q/XGHNRSKAb/B8XGDHRUaA67NepOW5W8u3ZRJBAA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.5.25':
|
||||
resolution: {integrity: sha512-UOewtDGkTMJTiODrEdeLZ50yGb59xCZSriNpXkfPMxRRgwDkGc7i8mLWqV5076wEdb+Ca/XN7MhJyM3CupyNyQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@15.5.25':
|
||||
resolution: {integrity: sha512-UBHwA8AhkCZgtRfU1aJpunuAJe/6gZv6jDESQe4p5MjTb5V0YEeJBWCdNqx15Vj3x+5jmauRfeMJSjfQj9HGFQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.5.25':
|
||||
resolution: {integrity: sha512-QcFcPRr16djk5IqK5+e8O80eZfgWzIvVBXfitIq0tQ/uc+eyfdoZ0NmKc0cnbIJyfVwREapKuG97YcxWA9gcpA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.5.25':
|
||||
resolution: {integrity: sha512-zREeykps3ndWr9egJgvJKqVkkDuaw6Zrrg23cYBos0ygydFkAWYU4+PaPVwXzP1eAYQJe53ShSK45iDM529BOg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@redocly/ajv@8.11.2':
|
||||
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
|
||||
|
||||
'@redocly/config@0.22.0':
|
||||
resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==}
|
||||
|
||||
'@redocly/openapi-core@1.34.19':
|
||||
resolution: {integrity: sha512-o/0VgsBXgwcY1lyeqcVtSGdTQAPnVggo0fbFVPlxl5XVDKUcVH0OLRqt3CbkwByT5FU305E0iE0O7MzThjDblw==}
|
||||
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
'@tanstack/query-core@5.103.1':
|
||||
resolution: {integrity: sha512-rms8HqTGp6zA00dM+cUQ2eBcgzNJefuu5CAMB37i/6MiGT1zulPOytCFu2a0qjLqVR2n1jENPj9woqFQNuCzWA==}
|
||||
|
||||
'@tanstack/react-query@5.103.1':
|
||||
resolution: {integrity: sha512-rmAPPApNEK17VXRJhtE1rja53n23VV5w30C0saCgJhhX+EpdUnVqMGV/s5nnzV43X75KTHKzuoiuxSzGTBIkag==}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19
|
||||
|
||||
'@types/node@22.20.1':
|
||||
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
|
||||
|
||||
'@types/react-dom@19.2.7':
|
||||
resolution: {integrity: sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==}
|
||||
peerDependencies:
|
||||
'@types/react': ^19.2.0
|
||||
|
||||
'@types/react@19.2.18':
|
||||
resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==}
|
||||
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ansi-colors@4.1.3:
|
||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
brace-expansion@2.1.4:
|
||||
resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==}
|
||||
|
||||
caniuse-lite@1.0.30001810:
|
||||
resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==}
|
||||
|
||||
change-case@5.4.4:
|
||||
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
|
||||
|
||||
client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
colorette@1.4.0:
|
||||
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
index-to-position@1.2.0:
|
||||
resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
js-levenshtein@1.1.6:
|
||||
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
js-yaml@4.3.1:
|
||||
resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==}
|
||||
hasBin: true
|
||||
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
lucide-react@1.46.0:
|
||||
resolution: {integrity: sha512-Bv+FZXgZPrxc/NCl1e7JJVQFLdiCxYgxNVhqoV7X0p6I8ADJo8DxBnK1auH0fZz4AmqOJ3jgneL4f1i8LJQRAA==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
minimatch@5.1.9:
|
||||
resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
nanoid@3.3.18:
|
||||
resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
next@15.5.25:
|
||||
resolution: {integrity: sha512-OMWNulIIqKM2ykvC2qMjIt0IoavB4UB2SCs4iXJ6z6847FvyH8jBmBWcvrF5iuhTu8Przh20Fo/aoszIdqx4PA==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.1.0
|
||||
'@playwright/test': ^1.51.1
|
||||
babel-plugin-react-compiler: '*'
|
||||
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||
sass: ^1.3.0
|
||||
peerDependenciesMeta:
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
'@playwright/test':
|
||||
optional: true
|
||||
babel-plugin-react-compiler:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
|
||||
openapi-typescript@7.13.0:
|
||||
resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: ^5.x
|
||||
|
||||
parse-json@8.3.0:
|
||||
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
pluralize@8.0.0:
|
||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postcss@8.4.31:
|
||||
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
react-dom@19.2.8:
|
||||
resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
|
||||
peerDependencies:
|
||||
react: ^19.2.8
|
||||
|
||||
react@19.2.8:
|
||||
resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-from-string@2.0.2:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
semver@7.8.5:
|
||||
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
sharp@0.35.4:
|
||||
resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '*'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
styled-jsx@5.1.6:
|
||||
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': '*'
|
||||
babel-plugin-macros: '*'
|
||||
react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0'
|
||||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
babel-plugin-macros:
|
||||
optional: true
|
||||
|
||||
supports-color@10.2.2:
|
||||
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
type-fest@4.41.0:
|
||||
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
uri-js-replace@1.0.1:
|
||||
resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
|
||||
|
||||
yaml-ast-parser@0.0.43:
|
||||
resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@babel/code-frame@7.29.7':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
'@babel/helper-validator-identifier@7.29.7': {}
|
||||
|
||||
'@emnapi/runtime@1.11.3':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-arm64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-freebsd-wasm32@0.35.4':
|
||||
dependencies:
|
||||
'@img/sharp-wasm32': 0.35.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-ppc64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-riscv64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.35.4':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.11.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-webcontainers-wasm32@0.35.4':
|
||||
dependencies:
|
||||
'@img/sharp-wasm32': 0.35.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.35.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.35.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.35.4':
|
||||
optional: true
|
||||
|
||||
'@next/env@15.5.25': {}
|
||||
|
||||
'@next/swc-darwin-arm64@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@redocly/ajv@8.11.2':
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
uri-js-replace: 1.0.1
|
||||
|
||||
'@redocly/config@0.22.0': {}
|
||||
|
||||
'@redocly/openapi-core@1.34.19(supports-color@10.2.2)':
|
||||
dependencies:
|
||||
'@redocly/ajv': 8.11.2
|
||||
'@redocly/config': 0.22.0
|
||||
colorette: 1.4.0
|
||||
https-proxy-agent: 7.0.6(supports-color@10.2.2)
|
||||
js-levenshtein: 1.1.6
|
||||
js-yaml: 4.3.1
|
||||
minimatch: 5.1.9
|
||||
pluralize: 8.0.0
|
||||
yaml-ast-parser: 0.0.43
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@tanstack/query-core@5.103.1': {}
|
||||
|
||||
'@tanstack/react-query@5.103.1(react@19.2.8)':
|
||||
dependencies:
|
||||
'@tanstack/query-core': 5.103.1
|
||||
react: 19.2.8
|
||||
|
||||
'@types/node@22.20.1':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/react-dom@19.2.7(@types/react@19.2.18)':
|
||||
dependencies:
|
||||
'@types/react': 19.2.18
|
||||
|
||||
'@types/react@19.2.18':
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
ansi-colors@4.1.3: {}
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
brace-expansion@2.1.4:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
caniuse-lite@1.0.30001810: {}
|
||||
|
||||
change-case@5.4.4: {}
|
||||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
colorette@1.4.0: {}
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
debug@4.4.3(supports-color@10.2.2):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 10.2.2
|
||||
|
||||
detect-libc@2.1.2:
|
||||
optional: true
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
https-proxy-agent@7.0.6(supports-color@10.2.2):
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
index-to-position@1.2.0: {}
|
||||
|
||||
js-levenshtein@1.1.6: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@4.3.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
lucide-react@1.46.0(react@19.2.8):
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
|
||||
minimatch@5.1.9:
|
||||
dependencies:
|
||||
brace-expansion: 2.1.4
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
nanoid@3.3.18: {}
|
||||
|
||||
next@15.5.25(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
|
||||
dependencies:
|
||||
'@next/env': 15.5.25
|
||||
'@swc/helpers': 0.5.15
|
||||
caniuse-lite: 1.0.30001810
|
||||
postcss: 8.4.31
|
||||
react: 19.2.8
|
||||
react-dom: 19.2.8(react@19.2.8)
|
||||
styled-jsx: 5.1.6(react@19.2.8)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 15.5.25
|
||||
'@next/swc-darwin-x64': 15.5.25
|
||||
'@next/swc-linux-arm64-gnu': 15.5.25
|
||||
'@next/swc-linux-arm64-musl': 15.5.25
|
||||
'@next/swc-linux-x64-gnu': 15.5.25
|
||||
'@next/swc-linux-x64-musl': 15.5.25
|
||||
'@next/swc-win32-arm64-msvc': 15.5.25
|
||||
'@next/swc-win32-x64-msvc': 15.5.25
|
||||
sharp: 0.35.4(@types/node@22.20.1)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
|
||||
openapi-typescript@7.13.0(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@redocly/openapi-core': 1.34.19(supports-color@10.2.2)
|
||||
ansi-colors: 4.1.3
|
||||
change-case: 5.4.4
|
||||
parse-json: 8.3.0
|
||||
supports-color: 10.2.2
|
||||
typescript: 5.9.3
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
parse-json@8.3.0:
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
index-to-position: 1.2.0
|
||||
type-fest: 4.41.0
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
postcss@8.4.31:
|
||||
dependencies:
|
||||
nanoid: 3.3.18
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
react-dom@19.2.8(react@19.2.8):
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
scheduler: 0.27.0
|
||||
|
||||
react@19.2.8: {}
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
semver@7.8.5:
|
||||
optional: true
|
||||
|
||||
sharp@0.35.4(@types/node@22.20.1):
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.8.5
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.35.4
|
||||
'@img/sharp-darwin-x64': 0.35.4
|
||||
'@img/sharp-freebsd-wasm32': 0.35.4
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.3
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.3
|
||||
'@img/sharp-libvips-linux-arm': 1.3.3
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.3
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.3
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.3
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.3
|
||||
'@img/sharp-libvips-linux-x64': 1.3.3
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
|
||||
'@img/sharp-linux-arm': 0.35.4
|
||||
'@img/sharp-linux-arm64': 0.35.4
|
||||
'@img/sharp-linux-ppc64': 0.35.4
|
||||
'@img/sharp-linux-riscv64': 0.35.4
|
||||
'@img/sharp-linux-s390x': 0.35.4
|
||||
'@img/sharp-linux-x64': 0.35.4
|
||||
'@img/sharp-linuxmusl-arm64': 0.35.4
|
||||
'@img/sharp-linuxmusl-x64': 0.35.4
|
||||
'@img/sharp-webcontainers-wasm32': 0.35.4
|
||||
'@img/sharp-win32-arm64': 0.35.4
|
||||
'@img/sharp-win32-ia32': 0.35.4
|
||||
'@img/sharp-win32-x64': 0.35.4
|
||||
'@types/node': 22.20.1
|
||||
optional: true
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
styled-jsx@5.1.6(react@19.2.8):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 19.2.8
|
||||
|
||||
supports-color@10.2.2: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
type-fest@4.41.0: {}
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
uri-js-replace@1.0.1: {}
|
||||
|
||||
yaml-ast-parser@0.0.43: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Topbar } from "@/components/shell/Topbar";
|
||||
import { Crumbs } from "@/components/ui";
|
||||
import { RunsPanel } from "@/components/panels";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Topbar crumbs={<Crumbs cn="天工阁" here="Agent 任务工作台" />} />
|
||||
<div className="content">
|
||||
<RunsPanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Topbar } from "@/components/shell/Topbar";
|
||||
import { Crumbs } from "@/components/ui";
|
||||
import { AvatarPanel } from "@/components/panels";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Topbar crumbs={<Crumbs cn="形仪" here="形象与资产" />} />
|
||||
<div className="content">
|
||||
<AvatarPanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Topbar } from "@/components/shell/Topbar";
|
||||
import { Crumbs } from "@/components/ui";
|
||||
import { CharactersPagePanel } from "@/components/characters/CharactersPagePanel";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Topbar crumbs={<Crumbs cn="镜生" here="角色" />} />
|
||||
<div className="content content--wide">
|
||||
<CharactersPagePanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Topbar } from "@/components/shell/Topbar";
|
||||
import { Crumbs } from "@/components/ui";
|
||||
import { CollaborationPanel } from "@/components/collaboration/CollaborationPanel";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Topbar crumbs={<Crumbs cn="同契台" here="协作空间" />} />
|
||||
<div className="content">
|
||||
<CollaborationPanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Topbar } from "@/components/shell/Topbar";
|
||||
import { Crumbs } from "@/components/ui";
|
||||
import { CommunityPanel } from "@/components/community/CommunityPanel";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Topbar crumbs={<Crumbs cn="雅集" here="社区广场" />} />
|
||||
<div className="content">
|
||||
<CommunityPanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeButton } from "@/lib/theme";
|
||||
import { ConversationPanel } from "@/components/conversation/ConversationPanel";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<header className="topbar">
|
||||
<div className="crumbs">
|
||||
<span className="cn">镜树</span>
|
||||
<span className="crumbs__sep">/</span>
|
||||
<span className="here">分支对话</span>
|
||||
</div>
|
||||
<div className="topbar__spacer" />
|
||||
<span className="prototype-tag">真实接口 · 联调中</span>
|
||||
<ThemeButton />
|
||||
</header>
|
||||
<ConversationPanel />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Topbar } from "@/components/shell/Topbar";
|
||||
import { Crumbs } from "@/components/ui";
|
||||
import { DevicesPanel } from "@/components/panels";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Topbar crumbs={<Crumbs cn="器谱" here="设备与感知" />} />
|
||||
<div className="content">
|
||||
<DevicesPanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Topbar } from "@/components/shell/Topbar";
|
||||
import { Crumbs } from "@/components/ui";
|
||||
import { GovernancePanel } from "@/components/panels";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Topbar crumbs={<Crumbs cn="司命台" here="安全与治理" />} />
|
||||
<div className="content">
|
||||
<GovernancePanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Topbar } from "@/components/shell/Topbar";
|
||||
import { Crumbs } from "@/components/ui";
|
||||
import { HomePanel } from "@/components/panels";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Topbar crumbs={<Crumbs cn="镜庭" here="个人首页" />} />
|
||||
<div className="content">
|
||||
<HomePanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { AppShell } from "@/components/shell/AppShell";
|
||||
|
||||
/** 用户侧 14 域 + 设计说明共用应用外壳 */
|
||||
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
return <AppShell>{children}</AppShell>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Topbar } from "@/components/shell/Topbar";
|
||||
import { Crumbs } from "@/components/ui";
|
||||
import { MarketplacePanel } from "@/components/marketplace/MarketplacePanel";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Topbar crumbs={<Crumbs cn="云市" here="创作市场" />} />
|
||||
<div className="content">
|
||||
<MarketplacePanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Topbar } from "@/components/shell/Topbar";
|
||||
import { Crumbs } from "@/components/ui";
|
||||
import { MemoryPanel } from "@/components/memory/MemoryPanel";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Topbar crumbs={<Crumbs cn="息壤" here="记忆与知识" />} />
|
||||
<div className="content">
|
||||
<MemoryPanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Topbar } from "@/components/shell/Topbar";
|
||||
import { Crumbs } from "@/components/ui";
|
||||
import { GatewayPanel } from "@/components/panels";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Topbar crumbs={<Crumbs cn="神识阁" here="模型与路由" />} />
|
||||
<div className="content">
|
||||
<GatewayPanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { Topbar } from "@/components/shell/Topbar";
|
||||
import { Crumbs } from "@/components/ui";
|
||||
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Topbar crumbs={<Crumbs cn="设计说明" here="React 转写版" />} />
|
||||
<div className="content doc">
|
||||
|
||||
<div className="doc-hero">
|
||||
<h1>魂镜 <span className="ver">原型 v0.2</span></h1>
|
||||
<p>魂镜 Web 界面静态 HTML 原型(v0.1 第一批方向已确认,v0.2 增补第二批共 15 个界面),供审查视觉方向、布局结构与文化语言的界面落地方式。全部确认后按此实现为 React 组件;未在本文档登记的视觉细节以本页令牌为准。</p>
|
||||
</div>
|
||||
|
||||
<section className="doc-sec">
|
||||
<h2>原型页面</h2>
|
||||
<p className="sub">第一批(已确认):镜庭、镜生、镜树;第二批:息壤、形仪、天工阁、神识阁、百工坊、雅集、同契台、云市、器谱、司命台、设置,及运营者/自托管使用的管理面与认证页。全站链接已疏通:跨页指向对应页面、页内为锚点;logo 单击回本目录,350ms 内连点 5 次触发「镜启」彩蛋(承接 main 分支旧版设计);侧栏可折叠并记忆偏好。</p>
|
||||
<div className="page-nav">
|
||||
<a className="card" href="/home"><span className="cn">镜庭<span className="cn__plain">个人首页</span></span><span className="d">全局框架、角色入口、任务与待审事项</span><span className="p">home.html</span></a>
|
||||
<a className="card" href="/conversation"><span className="cn">镜树<span className="cn__plain">分支对话</span></span><span className="d">ContentBlock、请命朱批、界域、候答、流式芽体、分支树</span><span className="p">conversation.html</span></a>
|
||||
<a className="card" href="/characters"><span className="cn">镜生<span className="cn__plain">角色身份</span></span><span className="d">道体、缘契、内驱力、大周天、谱系、生命周期</span><span className="p">characters.html</span></a>
|
||||
<a className="card" href="/memory"><span className="cn">息壤<span className="cn__plain">记忆与知识</span></span><span className="d">候选审阅、正影纠正、明忆版本、玄络投影</span><span className="p">memory.html</span></a>
|
||||
<a className="card" href="avatar.html"><span className="cn">形仪<span className="cn__plain">形象与资产</span></span><span className="d">真形锚、主仪化仪、声音、Live2D 与 3D、易相历史</span><span className="p">avatar.html</span></a>
|
||||
<a className="card" href="/agent-workspace"><span className="cn">天工阁<span className="cn__plain">Agent 任务</span></span><span className="d">工筹计划、预算、分工使、轨迹、验收</span><span className="p">agent-workspace.html</span></a>
|
||||
<a className="card" href="/model-gateway"><span className="cn">神识阁<span className="cn__plain">模型与路由</span></span><span className="d">逻辑模型、路由继承链、BYOK、用量观测</span><span className="p">model-gateway.html</span></a>
|
||||
<a className="card" href="/plugins"><span className="cn">百工坊<span className="cn__plain">插件生态</span></span><span className="d">沙箱、已安装、升级权限确认、授契记录</span><span className="p">plugins.html</span></a>
|
||||
<a className="card" href="community.html"><span className="cn">雅集<span className="cn__plain">社区广场</span></span><span className="d">动态流、角色谱、话题、推荐解释与重置</span><span className="p">community.html</span></a>
|
||||
<a className="card" href="collaboration.html"><span className="cn">同契台<span className="cn__plain">协作空间</span></span><span className="d">分支版本、审稿合卷、成员权限矩阵、活动</span><span className="p">collaboration.html</span></a>
|
||||
<a className="card" href="marketplace.html"><span className="cn">云市<span className="cn__plain">创作市场</span></span><span className="d">商品、市契许可、退契规则、分润结算</span><span className="p">marketplace.html</span></a>
|
||||
<a className="card" href="devices.html"><span className="cn">器谱<span className="cn__plain">设备与感知</span></span><span className="d">形器登记、授契、通感会话、直白权限请求</span><span className="p">devices.html</span></a>
|
||||
<a className="card" href="/governance"><span className="cn">司命台<span className="cn__plain">安全与治理</span></span><span className="d">治理队列、诊断双重授权、审计、安全概览</span><span className="p">governance.html</span></a>
|
||||
<a className="card" href="admin.html"><span className="cn">管理面<span className="cn__plain">运营与部署后台</span></span><span className="d">司议队列、支持工单与脱敏支持包、权限管理(RBAC / 双人授权)、部署健康(灵台双形态)、SLO</span><span className="p">admin.html</span></a>
|
||||
<a className="card" href="login.html"><span className="cn">登录<span className="cn__plain">铜镜鉴心</span></span><span className="d">铭文环镜背、扫描亮带、会话 Cookie / PKCE 说明、通行密钥与恢复钥匙</span><span className="p">login.html</span></a>
|
||||
<a className="card" href="register.html"><span className="cn">注册<span className="cn__plain">创建账户</span></span><span className="d">邮箱验证码、数据权告知(默认不训练 / 可导出删除 / 私箧持钥)、条款确认</span><span className="p">register.html</span></a>
|
||||
<a className="card" href="/settings"><span className="cn">设置<span className="cn__plain">账户与偏好</span></span><span className="d">会话、私箧、数据权、删除、封镜</span><span className="p">settings.html</span></a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="doc-sec">
|
||||
<h2>设计立场</h2>
|
||||
<p className="sub">四条约束所有页面的决定,审查时优先核对这四条是否成立。</p>
|
||||
<div className="card" style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
|
||||
<div><b className="cn">一 · 双层文字系统</b><span style={{ color: "var(--text-2)" }}> —— 文化名用衬线宋体,直白解释用无衬线小字,两种字体本身就是"文化名 · 直白解释"两层结构的可视化;风险、金额、权限、删除等后果始终以直白语言为主,文化名只作辅助。</span></div>
|
||||
<div><b className="cn">二 · 朱砂只属于朱批</b><span style={{ color: "var(--text-2)" }}> —— 朱砂色是"用户确认决定"的专用语义色:请命卡的允许按钮、危险动作入口使用它;普通操作一律铜镜青。颜色即权限语言。</span></div>
|
||||
<div><b className="cn">三 · 墨与月白</b><span style={{ color: "var(--text-2)" }}> —— 深色为墨(青调黑,非纯黑),浅色为月白(青灰白,非暖米);铜镜青为主色,取铜镜铜绿之意,不用蓝紫系默认色。</span></div>
|
||||
<div><b className="cn">四 · 状态永远三段式</b><span style={{ color: "var(--text-2)" }}> —— 状态徽章固定为「文化状态 · 直白状态 + 稳定工程值」,例如「运转 · 运行中 <span className="mono">running</span>」;错误与交易状态绝不只显示文化名。</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="doc-sec">
|
||||
<h2>色彩令牌</h2>
|
||||
<p className="sub">右下角可切换主题;以下色板随主题联动。转 React 时这些令牌进入 Tailwind 配置 / CSS 变量。</p>
|
||||
<div className="swatches">
|
||||
<div className="swatch"><div className="swatch__c" style={{ background: "var(--bg)", borderBottom: "1px solid var(--line-soft)" }}></div><div className="swatch__m"><b>底 · 墨 / 月白</b>表面层级 0<span className="mono">--bg</span></div></div>
|
||||
<div className="swatch"><div className="swatch__c" style={{ background: "var(--bg-raised)" }}></div><div className="swatch__m"><b>浮面</b>卡片与气泡<span className="mono">--bg-raised</span></div></div>
|
||||
<div className="swatch"><div className="swatch__c" style={{ background: "var(--accent)" }}></div><div className="swatch__m"><b>铜镜青</b>主色 · 选中/链接/运转<span className="mono">--accent</span></div></div>
|
||||
<div className="swatch"><div className="swatch__c" style={{ background: "var(--cinnabar)" }}></div><div className="swatch__m"><b>朱砂</b>朱批 · 确认与危险<span className="mono">--cinnabar</span></div></div>
|
||||
<div className="swatch"><div className="swatch__c" style={{ background: "var(--ok)" }}></div><div className="swatch__m"><b>已成</b>完成态<span className="mono">--ok</span></div></div>
|
||||
<div className="swatch"><div className="swatch__c" style={{ background: "var(--warn)" }}></div><div className="swatch__m"><b>秋香</b>整备 / 幻境<span className="mono">--warn</span></div></div>
|
||||
<div className="swatch"><div className="swatch__c" style={{ background: "var(--err)" }}></div><div className="swatch__m"><b>受阻</b>失败态<span className="mono">--err</span></div></div>
|
||||
<div className="swatch"><div className="swatch__c" style={{ background: "var(--text-1)" }}></div><div className="swatch__m"><b>正文</b>一级文字<span className="mono">--text-1</span></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="doc-sec">
|
||||
<h2>字体规范</h2>
|
||||
<p className="sub">不引入外部字体文件,使用系统中文字体栈;转 React 时同栈迁移。</p>
|
||||
<div className="card type-spec">
|
||||
<div className="type-spec__row"><span className="type-spec__label">文化名 · 宋体衬线</span><span className="type-spec__sample cn" style={{ fontSize: "22px", fontWeight: "600" }}>镜树 · 分支对话</span></div>
|
||||
<div className="type-spec__row"><span className="type-spec__label">界面正文 · 无衬线</span><span className="type-spec__sample" style={{ fontSize: "14px" }}>正文 14px / 行高 1.65,辅助文字 12.5px,元信息 11.5px</span></div>
|
||||
<div className="type-spec__row"><span className="type-spec__label">稳定值 · 等宽</span><span className="type-spec__sample mono" style={{ fontSize: "13px" }}>running · m_01h8xq… · block_id: blk_7f3a</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="doc-sec">
|
||||
<h2>状态徽章全集</h2>
|
||||
<p className="sub">对应 docs/product/cultural-language.md 第 10 节;徽章三段:文化状态 · 直白状态 · 稳定值。</p>
|
||||
<div className="card">
|
||||
<div className="table-wrap"><table className="data badge-table">
|
||||
<thead><tr><th>徽章</th><th>稳定值</th><th>使用场景</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><span className="badge badge--muted"><span className="badge__dot"></span>待启 · 排队中 <span className="mono">queued</span></span></td><td className="mono">queued</td><td>Run / 媒体任务排队</td></tr>
|
||||
<tr><td><span className="badge badge--info"><span className="badge__dot"></span>运转 · 运行中 <span className="mono">running</span></span></td><td className="mono">running</td><td>Run 执行中</td></tr>
|
||||
<tr><td><span className="badge badge--warn"><span className="badge__dot"></span>请命中 · 等待确认 <span className="mono">waiting_approval</span></span></td><td className="mono">waiting_approval</td><td>等待朱批</td></tr>
|
||||
<tr><td><span className="badge badge--ok"><span className="badge__dot"></span>已成 · 已完成 <span className="mono">completed</span></span></td><td className="mono">completed</td><td>Run / 任务完成</td></tr>
|
||||
<tr><td><span className="badge badge--err"><span className="badge__dot"></span>受阻 · 失败 <span className="mono">failed</span></span></td><td className="mono">failed</td><td>失败(附原因与重试)</td></tr>
|
||||
<tr><td><span className="badge badge--muted"><span className="badge__dot"></span>已止 · 已取消 <span className="mono">cancelled</span></span></td><td className="mono">cancelled</td><td>用户取消</td></tr>
|
||||
<tr><td><span className="badge badge--muted"><span className="badge__dot"></span>守一 · 安静待命 <span className="mono">idle</span></span></td><td className="mono">idle</td><td>角色无运行</td></tr>
|
||||
<tr><td><span className="badge badge--warn"><span className="badge__dot"></span>整备 · 正在处理 <span className="mono">processing</span></span></td><td className="mono">processing</td><td>媒体 / 资源处理</td></tr>
|
||||
<tr><td><span className="badge badge--warn"><span className="badge__dot"></span>待审 · 等待审核 <span className="mono">pending_review</span></span></td><td className="mono">pending_review</td><td>社区 / 市场审核</td></tr>
|
||||
<tr><td><span className="badge badge--ok"><span className="badge__dot"></span>已刊 · 已发布 <span className="mono">published</span></span></td><td className="mono">published</td><td>社区内容</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="doc-sec">
|
||||
<h2>组件样例</h2>
|
||||
<p className="sub">按钮语义:铜镜青为主操作,朱砂仅用于朱批(用户确认决定)与危险操作,描边为次操作。</p>
|
||||
|
||||
<div className="card demo-block">
|
||||
<div className="demo-row">
|
||||
<button className="btn btn--primary">开始对话</button>
|
||||
<button className="btn btn--cinnabar">允许(朱批)</button>
|
||||
<button className="btn btn--outline">查看谱系</button>
|
||||
<button className="btn btn--ghost">取消</button>
|
||||
<button className="btn btn--danger">删除角色数据</button>
|
||||
<button className="btn btn--outline btn--sm">续生</button>
|
||||
</div>
|
||||
<div className="demo-row">
|
||||
<span className="cn" style={{ fontSize: "16px" }}>请命<span className="cn__plain">请求你的确认</span></span>
|
||||
<span className="cn" style={{ fontSize: "16px" }}>息壤<span className="cn__plain">角色记忆</span></span>
|
||||
<span className="cn" style={{ fontSize: "16px" }}>幻境<span className="cn__plain">临时试演 · 不入正史</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card demo-block">
|
||||
<div className="block__head" style={{ borderRadius: "var(--radius) var(--radius) 0 0" }}><span className="lang">python</span><span className="fname">reading_progress.py</span><span className="acts"><button className="icon-btn" style={{ width: "26px", height: "26px" }} aria-label="复制源码"><svg style={{ width: "13px", height: "13px" }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button></span></div>
|
||||
<pre className="code"><span className="c"># 汇总共读接龙进度(示意)</span>
|
||||
<span className="k">from</span> pathlib <span className="k">import</span> Path
|
||||
|
||||
<span className="k">def</span> <span className="f">summarize</span>(records: <span className="k">list</span>[<span className="k">dict</span>]) -> <span className="k">dict</span>:
|
||||
done = [r <span className="k">for</span> r <span className="k">in</span> records <span className="k">if</span> r[<span className="s">"finished"</span>]]
|
||||
<span className="k">return</span> {'{'}
|
||||
<span className="s">"total"</span>: <span className="f">len</span>(records),
|
||||
<span className="s">"finished"</span>: <span className="f">len</span>(done),
|
||||
<span className="s">"pending"</span>: [r[<span className="s">"name"</span>] <span className="k">for</span> r <span className="k">in</span> records <span className="k">if</span> <span className="k">not</span> r[<span className="s">"finished"</span>]],
|
||||
{'}'}</pre>
|
||||
</div>
|
||||
|
||||
<div className="card demo-block">
|
||||
<div className="math">注意力权重 = softmax( QK<sup>T</sup> / √d<sub>k</sub> ) · V
|
||||
<span className="math__fallback">fallback:注意力权重等于 Q、K 转置乘积经缩放与归一化后对 V 的加权求和</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="doc-sec">
|
||||
<h2>审查重点</h2>
|
||||
<p className="sub">第一批(色彩、字体、镜树/镜生/镜庭结构)已确认;本轮重点核对第二批页面,反馈时按页名 + 位置描述即可。</p>
|
||||
<div className="card" style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
<div>1. 息壤:候选审阅的「归真入忆 / 正影纠正 / 忽略」操作,以及大周天来源、重忆提示的呈现是否可理解。</div>
|
||||
<div>2. 天工阁:工筹步骤、预算、分工使、轨迹的信息层级是否符合你对 Run 治理的预期。</div>
|
||||
<div>3. 神识阁 / 百工坊:路由继承链与「升级新增权限需重新确认」的展示是否清晰,朱批语义是否仍然专一。</div>
|
||||
<div>4. 云市:金额、分润、退契规则是否足够直白(文化名没有掩盖交易后果)。</div>
|
||||
<div>5. 器谱 / 司命台 / 设置:直白权限句、双重授权、私箧与删除等高风险项的措辞与视觉重量。</div>
|
||||
<div>6. 雅集 / 同契台:社区帖卡与协作审稿流的信息密度是否合适。</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="doc-sec">
|
||||
<h2>旧版传承与候选</h2>
|
||||
<p className="sub">已系统挖掘 main 分支旧版 25 页设计原型(铜镜鉴心体系),按「取手法、不照搬旧概念名」原则融入——六爻 / 九枢 / 三魂等概念 1.0 已由小周天、灵台六模块、内驱力等替代,仅传承其可视化手法。</p>
|
||||
<div className="card">
|
||||
<div className="table-wrap"><table className="data">
|
||||
<thead><tr><th>旧版想法</th><th>来源页</th><th>状态</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>鎏金发丝线三档令牌</td><td>tokens.css</td><td className="mark-ok">✓ 已融入全站(分组标题 / 金标竖条 / 品牌外环 / 时间线)</td></tr>
|
||||
<tr><td>logo 五连击「镜启」诗签彩蛋</td><td>_theme.js</td><td className="mark-ok">✓ 已融入(修好首次单击即跳转的缺陷)</td></tr>
|
||||
<tr><td>铜镜登录页(铭文环 / 铜面噪点 / 扫描亮带)</td><td>copper-mirror-login</td><td className="mark-ok">✓ 已融入登录 / 注册页</td></tr>
|
||||
<tr><td>极光铜晕底 + 卡片 hover 光扫</td><td>_base.css / rune-engine</td><td className="mark-ok">✓ 已融入全站(极淡,不干扰阅读)</td></tr>
|
||||
<tr><td>日晷时辰盘 + 节气 + 相伴日文案</td><td>time</td><td className="mark-ok">✓ 已融入镜庭 hero(巳时 · 白露 · 第 918 日)</td></tr>
|
||||
<tr><td>朱批卡风险分级(RISK L2)</td><td>chat-redesign</td><td className="mark-ok">✓ 已融入镜树请命卡</td></tr>
|
||||
<tr><td>「呈镜」发送按钮命名(旧版已确立血脉)</td><td>chat-redesign</td><td className="mark-ok">✓ 已融入镜树输入区(aria 仍读「发送」)</td></tr>
|
||||
<tr><td>运行阶段爻象三态(done / active / pending)</td><td>chat / evolution</td><td className="mark-ok">✓ 已融入天工阁工筹(active 步骤鎏金光晕)</td></tr>
|
||||
<tr><td>河图置信度图谱(置信→线宽,假设→朱漆虚线待验)</td><td>world</td><td className="mark-ok">✓ 已融入息壤玄络(待验边 + 标注)</td></tr>
|
||||
<tr><td>息壤浮现(本次回应用了哪些记忆)</td><td>chat / copper-v2</td><td className="mark-ok">✓ 对应 1.0 上下文镜(镜树右栏,同源 ContextSnapshot)</td></tr>
|
||||
<tr><td>九枢星盘「活的仪器」+ 铜镜四态健康词汇</td><td>admin / copper-v2</td><td className="mark-ok">✓ 已复刻为灵台六枢星盘(太一居中 + 五枢环绕 + 呼吸刻度 / 慢旋 / 告警闪烁)+ 镜态徽章(管理面)</td></tr>
|
||||
<tr><td>五行生克健康环(系统健康=关系动力学)</td><td>wuxing</td><td className="mark-ok">✓ 已复刻为相生 / 相克指标对告警(管理面星盘卡;环状图 React 阶段评估)</td></tr>
|
||||
<tr><td>眉批 depth 标尺注入预览</td><td>margin-notes</td><td style={{ color: "var(--warn)" }}>候选 —— 上下文镜深度维度(React 阶段与 Context Manifest 联动)</td></tr>
|
||||
<tr><td>好奇萤火(亮度=期望值)与探索天平</td><td>drive</td><td className="mark-ok">✓ 萤火已融入内驱力维度(亮度=维度值);探索天平待天工阁预算增强时评估</td></tr>
|
||||
<tr><td>周天四镜阶段链(评 / 演 / 候 / 藏 四面小铜镜)</td><td>evolution</td><td className="mark-ok">✓ 已复刻为大周天四镜:回 / 照 / 议 / 呈(镜生页,ADR-023 四阶段)</td></tr>
|
||||
<tr><td>会话列表「今日 / 本周 / 更早」分组 + 彩点状态</td><td>chat-redesign</td><td className="mark-ok">✓ 已融入镜树会话列表(活跃铜绿 / 幻境秋香 / 待朱批朱砂)</td></tr>
|
||||
<tr><td>「深虑 / 博闻」输入控制药丸 + 用户消息鎏金右边线</td><td>chat-redesign / copper-v2</td><td className="mark-ok">✓ 已融入镜树输入区与用户气泡</td></tr>
|
||||
<tr><td>起居注时间河「卷」章分割</td><td>admin</td><td className="mark-ok">✓ 已融入管理面审计快照</td></tr>
|
||||
<tr><td>太极阴阳平衡 gauge(三态)</td><td>cosmos</td><td className="mark-ok">✓ 已复刻为朱批治理阴阳平衡(司命台,阳亢 / 平衡 / 阴盛)</td></tr>
|
||||
<tr><td>角色 card-banner 渐变横幅</td><td>character-detail</td><td className="mark-ok">✓ 已融入镜生详情页</td></tr>
|
||||
<tr><td>思考块折叠卡(复刻 chat-redesign)</td><td>chat-redesign</td><td className="mark-ok">✓ 已融入(用户确认):思考摘要可折叠 + 深虑强度(低/中/高)——<b>契约候选</b>:正式实现前需在 content-blocks 契约登记 <span className="mono">thinking</span> 块类型与流式语义;摘要仅会话内可见,不入长期记录与诊断导出</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Topbar } from "@/components/shell/Topbar";
|
||||
import { Crumbs } from "@/components/ui";
|
||||
import { PluginsPanel } from "@/components/plugins/PluginsPanel";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Topbar crumbs={<Crumbs cn="百工坊" here="插件与能力生态" />} />
|
||||
<div className="content">
|
||||
<PluginsPanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Topbar } from "@/components/shell/Topbar";
|
||||
import { Crumbs } from "@/components/ui";
|
||||
import { SettingsPanel } from "@/components/panels";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Topbar crumbs={<Crumbs cn="设置" here="账户与偏好" />} />
|
||||
<div className="content">
|
||||
<SettingsPanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { AdminShell } from "@/components/shell/AdminShell";
|
||||
|
||||
/** 管理面独立外壳(运营 / 部署 / 观测) */
|
||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
return <AdminShell>{children}</AdminShell>;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { Topbar } from "@/components/shell/Topbar";
|
||||
import { Crumbs } from "@/components/ui";
|
||||
import { AdminPanel } from "@/components/admin/AdminPanel";
|
||||
import { readSession } from "@/lib/api";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Page() {
|
||||
const [session, setSession] = useState<{ role: string; email: string } | null>(null);
|
||||
useEffect(() => {
|
||||
// 会话身份含 localStorage,须在挂载后读取(避免 SSR/客户端不一致)
|
||||
setSession(readSession());
|
||||
}, []);
|
||||
const isAdmin = session?.role === "admin";
|
||||
return (
|
||||
<>
|
||||
<Topbar
|
||||
crumbs={<Crumbs cn="司命台" here="管理面 · 运营与部署" />}
|
||||
extra={
|
||||
session === null ? null : isAdmin ? (
|
||||
<span className="badge badge--warn">
|
||||
<span className="badge__dot" />
|
||||
管理员 · {session.email || "dev"} <span className="mono">role: admin</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="badge badge--muted">
|
||||
<span className="badge__dot" />
|
||||
普通用户 <span className="mono">role: user</span> · 治理队列需管理员
|
||||
</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="content">
|
||||
<div className="realm-banner" role="note">
|
||||
管理面边界:管理员不可读取私箧与用户明文内容;敏感操作需双人授权并全部写入审计(开发档单管理员,双人授权待运营流程接入)。
|
||||
</div>
|
||||
<AdminPanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,14 +1,27 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/lib/theme";
|
||||
import { QueryProvider } from "@/lib/query";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "魂镜 Hunjing",
|
||||
description: "长期 AI 角色、Agent 与创作社区平台",
|
||||
};
|
||||
|
||||
/** 主题初始化(防闪):与 ThemeProvider 的 localStorage 键一致 */
|
||||
const themeInit = `try{var t=localStorage.getItem("hunjing-theme");if(t==="light"||t==="dark")document.documentElement.setAttribute("data-theme",t)}catch(e){}`;
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body>{children}</body>
|
||||
<html lang="zh-CN" data-theme="dark" suppressHydrationWarning>
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: themeInit }} />
|
||||
</head>
|
||||
<body>
|
||||
<ThemeProvider>
|
||||
<QueryProvider>{children}</QueryProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { Fingerprint, Key } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { api, ApiError, storeCsrf, storeSession } from "@/lib/api";
|
||||
import { ThemeButton } from "@/lib/theme";
|
||||
|
||||
/** 登录页(StageShell 铜镜鉴心)——开发档:后端仅校验邮箱即签发会话 */
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [pending, setPending] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const session = await api<{ csrf_token: string; user_id: string; role: string; email: string }>(
|
||||
"/v1/auth/sessions",
|
||||
{ method: "POST", json: { email } },
|
||||
);
|
||||
storeCsrf(session.csrf_token);
|
||||
storeSession(session);
|
||||
router.push("/home");
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : "登录失败,请稍后再试");
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="stage">
|
||||
<div className="stage__bg" aria-hidden="true">
|
||||
<div className="stage__rings">
|
||||
<svg viewBox="0 0 800 800">
|
||||
<defs>
|
||||
<path id="ringpath" d="M400,400 m-300,0 a300,300 0 1,1 600,0 a300,300 0 1,1 -600,0" />
|
||||
</defs>
|
||||
<circle className="ring-ink" cx="400" cy="400" r="392" />
|
||||
<circle className="ring-faint" cx="400" cy="400" r="336" />
|
||||
<g className="ring-slow"><circle className="ring-faint" cx="400" cy="400" r="272" /></g>
|
||||
<circle className="ring-ink" cx="400" cy="400" r="206" />
|
||||
<circle className="ring-faint" cx="400" cy="400" r="158" />
|
||||
<g className="ring-slow" style={{ animationDirection: "reverse" }}>
|
||||
<circle className="ring-faint" cx="400" cy="400" r="104" />
|
||||
</g>
|
||||
<text fill="var(--gilt-faint)" fontSize="12" letterSpacing="6" style={{ fontFamily: "var(--font-serif)" }}>
|
||||
<textPath href="#ringpath">镜中有镜 · 心外无心 · 汝来看此镜时 · 此镜亦照汝 · 铜镜本无字 · 照尽往来人 ·</textPath>
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
<svg style={{ position: "absolute", inset: 0, width: "100%", height: "100%", opacity: 0.035 }} aria-hidden="true">
|
||||
<filter id="grain"><feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="2" /></filter>
|
||||
<rect width="100%" height="100%" filter="url(#grain)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<header className="stage__topbar">
|
||||
<span style={{ fontSize: 12, color: "var(--text-3)" }}>联调中 · 开发档登录</span>
|
||||
<span className="spacer" />
|
||||
<a href="/" style={{ fontSize: 12, color: "var(--text-3)" }}>原型目录</a>
|
||||
<ThemeButton />
|
||||
</header>
|
||||
|
||||
<main className="stage__main">
|
||||
<div className="mirror-card">
|
||||
<div className="mirror-card__brand">
|
||||
<div className="brand__mark" aria-hidden="true" />
|
||||
<div>
|
||||
<div style={{ fontFamily: "var(--font-serif)", fontSize: 20, fontWeight: 600, lineHeight: 1.2 }}>魂镜</div>
|
||||
<div style={{ fontSize: 11, color: "var(--text-3)" }}>Hunjing</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mirror-card__title"><h1>登入魂镜</h1></div>
|
||||
<p className="mirror-card__sub">长期 AI 角色、Agent 与创作社区平台</p>
|
||||
|
||||
<form onSubmit={submit}>
|
||||
<div className="field-group">
|
||||
<label htmlFor="login-email">邮箱</label>
|
||||
<input
|
||||
className="input"
|
||||
id="login-email"
|
||||
type="email"
|
||||
autoComplete="username"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="field-group">
|
||||
<label htmlFor="login-pwd">密码</label>
|
||||
<input
|
||||
className="input"
|
||||
id="login-pwd"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="开发档暂不校验,仅邮箱即可"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<p style={{ fontSize: 12.5, color: "var(--err)", marginBottom: 10 }} role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn--primary"
|
||||
style={{ width: "100%", marginTop: 6 }}
|
||||
disabled={pending}
|
||||
>
|
||||
{pending ? "登入中…" : "登入"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="login-alt">其他方式</div>
|
||||
<div style={{ display: "flex", gap: 10 }}>
|
||||
<a className="btn btn--outline btn--sm" href="#" style={{ flex: 1 }} title="通行密钥(WebAuthn,待接入)"><Fingerprint />通行密钥</a>
|
||||
<a className="btn btn--outline btn--sm" href="#" style={{ flex: 1 }} title="恢复钥匙(待接入)"><Key />恢复钥匙</a>
|
||||
</div>
|
||||
|
||||
<p className="login-note">
|
||||
开发档:输入邮箱即可登入(自动建户)。正式档为 OAuth 2.1 安全会话;凭据只经 Header 传递。
|
||||
</p>
|
||||
<div className="login-foot">
|
||||
还没有账户?<a href="/register">创建账户</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export default function Home() {
|
||||
return (
|
||||
<main style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
|
||||
<h1>魂镜 Hunjing</h1>
|
||||
<p>工程骨架已建立;界面随 WP-03/WP-10 工作包实现。</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { api, ApiError, storeCsrf } from "@/lib/api";
|
||||
import { ThemeButton } from "@/lib/theme";
|
||||
|
||||
/** 注册页(StageShell 铜镜鉴心) */
|
||||
export default function RegisterPage() {
|
||||
const router = useRouter();
|
||||
const [pending, setPending] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const email = (document.getElementById("reg-email") as HTMLInputElement).value;
|
||||
if (!email) return;
|
||||
setPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const session = await api<{ csrf_token: string }>("/v1/auth/sessions", { method: "POST", json: { email } });
|
||||
storeCsrf(session.csrf_token);
|
||||
router.push("/home");
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : "创建失败");
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="stage">
|
||||
<div className="stage__bg" aria-hidden="true">
|
||||
<div className="stage__rings">
|
||||
<svg viewBox="0 0 800 800">
|
||||
<defs>
|
||||
<path id="ringpath" d="M400,400 m-300,0 a300,300 0 1,1 600,0 a300,300 0 1,1 -600,0" />
|
||||
</defs>
|
||||
<circle className="ring-ink" cx="400" cy="400" r="392" />
|
||||
<circle className="ring-faint" cx="400" cy="400" r="336" />
|
||||
<g className="ring-slow"><circle className="ring-faint" cx="400" cy="400" r="272" /></g>
|
||||
<circle className="ring-ink" cx="400" cy="400" r="206" />
|
||||
<circle className="ring-faint" cx="400" cy="400" r="158" />
|
||||
<g className="ring-slow" style={{ animationDirection: "reverse" }}>
|
||||
<circle className="ring-faint" cx="400" cy="400" r="104" />
|
||||
</g>
|
||||
<text fill="var(--gilt-faint)" fontSize="12" letterSpacing="6" style={{ fontFamily: "var(--font-serif)" }}>
|
||||
<textPath href="#ringpath">幻由心生 · 镜随心转 · 一念起 · 万镜生 · 且向镜中看 · 那个是谁 ·</textPath>
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
<svg style={{ position: "absolute", inset: 0, width: "100%", height: "100%", opacity: 0.035 }} aria-hidden="true">
|
||||
<filter id="grain"><feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="2" /></filter>
|
||||
<rect width="100%" height="100%" filter="url(#grain)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<header className="stage__topbar">
|
||||
<a href="/login" style={{ fontSize: 12, color: "var(--text-3)" }}>← 返回登录</a>
|
||||
<span style={{ fontSize: 12, color: "var(--text-3)" }}>React 转写 · 待联调</span>
|
||||
<span className="spacer" />
|
||||
<a href="/" style={{ fontSize: 12, color: "var(--text-3)" }}>原型目录</a>
|
||||
<ThemeButton />
|
||||
</header>
|
||||
|
||||
<main className="stage__main">
|
||||
<div className="mirror-card">
|
||||
<div className="mirror-card__brand">
|
||||
<div className="brand__mark" aria-hidden="true" />
|
||||
<div>
|
||||
<div style={{ fontFamily: "var(--font-serif)", fontSize: 20, fontWeight: 600, lineHeight: 1.2 }}>魂镜</div>
|
||||
<div style={{ fontSize: 11, color: "var(--text-3)" }}>Hunjing</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mirror-card__title"><h1>创建账户</h1></div>
|
||||
<p className="mirror-card__sub">化名与法定身份分离核验;随时可导出、撤权或删除</p>
|
||||
|
||||
<form onSubmit={submit}>
|
||||
<div className="field-group">
|
||||
<label htmlFor="reg-email">邮箱</label>
|
||||
<input className="input" id="reg-email" type="email" autoComplete="email" placeholder="you@example.com" />
|
||||
</div>
|
||||
<div className="field-group">
|
||||
<label htmlFor="reg-code">验证码</label>
|
||||
<div className="field-row">
|
||||
<input className="input" id="reg-code" type="text" inputMode="numeric" placeholder="6 位验证码" />
|
||||
<button className="btn btn--outline" type="button">发送验证码</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field-group">
|
||||
<label htmlFor="reg-pwd">密码</label>
|
||||
<input className="input" id="reg-pwd" type="password" autoComplete="new-password" placeholder="至少 12 位,建议通行密钥" />
|
||||
</div>
|
||||
<div className="field-group">
|
||||
<label htmlFor="reg-pwd2">确认密码</label>
|
||||
<input className="input" id="reg-pwd2" type="password" autoComplete="new-password" placeholder="再次输入" />
|
||||
</div>
|
||||
|
||||
<div style={{ margin: "18px 0 16px", borderTop: "1px solid var(--line-soft)", paddingTop: 16 }}>
|
||||
<div style={{ fontSize: 12.5, color: "var(--text-2)", marginBottom: 10 }}>开始前请知悉(数据权):</div>
|
||||
<div className="check-row">
|
||||
<input type="checkbox" id="ck-train" defaultChecked disabled aria-label="默认不用于模型训练" />
|
||||
<label className="t" htmlFor="ck-train"><b>默认不用于模型训练</b> —— 你的个人域数据默认不参与任何模型训练;自愿贡献是独立开关,可随时撤销。</label>
|
||||
</div>
|
||||
<div className="check-row">
|
||||
<input type="checkbox" id="ck-export" defaultChecked disabled aria-label="可导出与删除" />
|
||||
<label className="t" htmlFor="ck-export"><b>可导出、可删除</b> —— 角色、记忆、对话与来源谱系随时可携导出;删除有冷静期并传播到全部派生数据。</label>
|
||||
</div>
|
||||
<div className="check-row">
|
||||
<input type="checkbox" id="ck-vault" aria-label="了解私箧规则" />
|
||||
<label className="t" htmlFor="ck-vault"><b>私箧独立持钥</b> —— 加密私密区只有你能解开;平台无法恢复,钥匙丢失即永久不可读取。</label>
|
||||
</div>
|
||||
<div className="check-row">
|
||||
<input type="checkbox" id="ck-terms" aria-label="同意服务条款" />
|
||||
<label className="t" htmlFor="ck-terms"><b>我已阅读并同意服务条款与社区规范</b> —— 角色不拥有生物意识或法律人格;社区有举报与治理机制。</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="btn btn--cinnabar" style={{ width: "100%" }} disabled={pending}>
|
||||
{pending ? "创建中…" : "创建账户"}
|
||||
</button>
|
||||
{error && <p style={{ fontSize: 12.5, color: "var(--err)", marginTop: 10 }} role="alert">{error}</p>}
|
||||
</form>
|
||||
|
||||
<p className="login-note">注册即创建你的镜域(个人数据域);角色、记忆与授权都归属镜域治理。</p>
|
||||
<div className="login-foot">
|
||||
已有账户?<a href="/login">登入</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { api, ApiError, type ToolCall } from "@/lib/api";
|
||||
import { PanelState } from "@/lib/queries";
|
||||
|
||||
interface ModerationCase {
|
||||
case_id: string;
|
||||
status: string;
|
||||
report: { target_type?: string; target_id?: string; reason?: string } | null;
|
||||
action_taken: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/** 管理面数据区:治理案件(真实裁决)+ 运行观测 + 支持包 */
|
||||
export function AdminPanel() {
|
||||
const qc = useQueryClient();
|
||||
const isAdminDenied = false; // 由下方 PanelState 呈现 403
|
||||
const cases = useQuery({
|
||||
queryKey: ["moderation-cases"],
|
||||
queryFn: () => api<{ data: ModerationCase[] }>("/v1/admin/moderation-cases"),
|
||||
});
|
||||
const metrics = useQuery({
|
||||
queryKey: ["metrics"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/v1/metrics");
|
||||
if (!res.ok) throw new Error(`metrics ${res.status}`);
|
||||
return res.text();
|
||||
},
|
||||
});
|
||||
const [bundle, setBundle] = useState<string | null>(null);
|
||||
const resolve = useMutation({
|
||||
mutationFn: ({ id, action }: { id: string; action: string }) =>
|
||||
api(`/v1/admin/moderation-cases/${id}/resolutions`, {
|
||||
method: "POST",
|
||||
json: { action },
|
||||
}),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["moderation-cases"] }),
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<section className="section">
|
||||
<div className="section__head">
|
||||
<span className="cn">司议<span className="cn__plain">治理案件 · 真实裁决</span></span>
|
||||
</div>
|
||||
<PanelState pending={cases.isPending} error={cases.error} onRetry={() => cases.refetch()} empty={(cases.data?.data ?? []).length === 0}>
|
||||
<div className="card" style={{ padding: "6px 18px" }}>
|
||||
{(cases.data?.data ?? []).map((c) => (
|
||||
<div className="list-row" key={c.case_id}>
|
||||
<div className="list-row__body">
|
||||
<div className="list-row__title mono">{c.case_id.slice(-10)} · {c.report?.target_type ?? ""}</div>
|
||||
<div className="list-row__sub">
|
||||
{c.report?.reason ?? ""} · {new Date(c.created_at).toLocaleString("zh-CN", { hour12: false })}
|
||||
{c.action_taken && ` · 已处置:${c.action_taken}`}
|
||||
</div>
|
||||
</div>
|
||||
{c.status === "pending" ? (
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
<button
|
||||
className="btn btn--danger btn--sm"
|
||||
disabled={resolve.isPending}
|
||||
onClick={() => resolve.mutate({ id: c.case_id, action: "content_removed" })}
|
||||
>
|
||||
移除内容
|
||||
</button>
|
||||
<button
|
||||
className="btn btn--outline btn--sm"
|
||||
disabled={resolve.isPending}
|
||||
onClick={() => resolve.mutate({ id: c.case_id, action: "dismissed" })}
|
||||
>
|
||||
驳回
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="badge badge--muted" style={{ fontSize: 11 }}>
|
||||
<span className="badge__dot" />{c.status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PanelState>
|
||||
</section>
|
||||
|
||||
<section className="section card">
|
||||
<div className="card__head">
|
||||
<span className="cn">运行观测<span className="cn__plain">/v1/metrics</span></span>
|
||||
</div>
|
||||
<PanelState pending={metrics.isPending} error={metrics.error} onRetry={() => metrics.refetch()}>
|
||||
<pre className="code" style={{ maxHeight: 220, overflow: "auto" }}>{metrics.data}</pre>
|
||||
</PanelState>
|
||||
</section>
|
||||
|
||||
<section className="section card">
|
||||
<div className="card__head">
|
||||
<span className="cn">支持包<span className="cn__plain">脱敏诊断导出</span></span>
|
||||
</div>
|
||||
<p style={{ fontSize: 12.5, color: "var(--text-2)", marginBottom: 12 }}>
|
||||
支持包为脱敏导出:不包含消息明文、私箧、隐藏推理、密钥与签名 URL。
|
||||
</p>
|
||||
<button
|
||||
className="btn btn--cinnabar btn--sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const r = await api<{ id?: string }>("/v1/support/bundles", { method: "POST", json: {} });
|
||||
setBundle(`已生成支持包 ${r.id ?? ""}`);
|
||||
} catch (err) {
|
||||
setBundle(err instanceof ApiError ? err.message : "生成失败");
|
||||
}
|
||||
}}
|
||||
>
|
||||
生成支持包
|
||||
</button>
|
||||
{bundle && <p style={{ fontSize: 12.5, color: "var(--text-3)", marginTop: 10 }}>{bundle}</p>}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { api, ApiError, type Character, type CursorPage } from "@/lib/api";
|
||||
|
||||
/** 角色状态 → 徽章(稳定值来自后端,文化名前端本地化) */
|
||||
const STATUS_LABEL: Record<string, { cn: string; plain: string; tone: "info" | "muted" | "warn" | "ok" }> = {
|
||||
draft: { cn: "灵胚", plain: "草稿", tone: "warn" },
|
||||
active: { cn: "临照", plain: "运行中", tone: "info" },
|
||||
resting: { cn: "静修", plain: "停止运行", tone: "muted" },
|
||||
archived: { cn: "归藏", plain: "已隐藏", tone: "muted" },
|
||||
deceased: { cn: "死亡", plain: "永久闭合", tone: "muted" },
|
||||
};
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const label = STATUS_LABEL[status] ?? { cn: status, plain: "未知状态", tone: "muted" as const };
|
||||
return (
|
||||
<span className={`badge badge--${label.tone}`} style={{ fontSize: 11, padding: "1px 8px" }}>
|
||||
<span className="badge__dot" />
|
||||
{label.cn} · {label.plain}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** 镜生左列:真实角色列表(未登入 / 空 / 有数据三态)+ 创建 */
|
||||
export function CharacterListPanel() {
|
||||
const qc = useQueryClient();
|
||||
const [name, setName] = useState("");
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const list = useQuery({
|
||||
queryKey: ["characters"],
|
||||
queryFn: () => api<CursorPage<Character>>("/v1/characters?limit=20"),
|
||||
});
|
||||
|
||||
const create = useMutation({
|
||||
mutationFn: (newName: string) =>
|
||||
api<Character>("/v1/characters", { method: "POST", json: { name: newName, summary: "" } }),
|
||||
onSuccess: () => {
|
||||
setName("");
|
||||
setCreating(false);
|
||||
setError(null);
|
||||
qc.invalidateQueries({ queryKey: ["characters"] });
|
||||
},
|
||||
onError: (err) => setError(err instanceof ApiError ? err.message : "创建失败"),
|
||||
});
|
||||
|
||||
if (list.isError) {
|
||||
const status = list.error instanceof ApiError ? list.error.status : 0;
|
||||
if (status === 401 || status === 403) {
|
||||
return (
|
||||
<div className="card" style={{ padding: "26px 20px", textAlign: "center" }}>
|
||||
<p style={{ fontSize: 13.5, color: "var(--text-2)", marginBottom: 14 }}>尚未登入,无法读取你的镜域。</p>
|
||||
<a className="btn btn--primary btn--sm" href="/login">去登入</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="card" style={{ padding: "26px 20px", textAlign: "center" }}>
|
||||
<p style={{ fontSize: 13.5, color: "var(--err)", marginBottom: 8 }}>读取失败</p>
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", marginBottom: 12 }}>
|
||||
{list.error instanceof ApiError ? list.error.message : "请稍后重试"}
|
||||
</p>
|
||||
<button className="btn btn--outline btn--sm" onClick={() => list.refetch()}>重试</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (list.isPending) {
|
||||
return (
|
||||
<div className="card" style={{ padding: "22px 20px", textAlign: "center" }}>
|
||||
<p style={{ fontSize: 13, color: "var(--text-3)" }}>正在读取镜生…</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const items = list.data?.data ?? [];
|
||||
|
||||
return (
|
||||
<div className="card" style={{ padding: 10 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "4px 8px 10px" }}>
|
||||
<span className="cn" style={{ fontSize: 15, fontWeight: 600 }}>
|
||||
镜生<span className="cn__plain">{items.length} 位角色</span>
|
||||
</span>
|
||||
<button className="btn btn--outline btn--sm" onClick={() => setCreating((v) => !v)}>创建</button>
|
||||
</div>
|
||||
|
||||
{items.length === 0 && (
|
||||
<div style={{ padding: "22px 12px", textAlign: "center" }}>
|
||||
<p style={{ fontSize: 13.5, color: "var(--text-2)", marginBottom: 4 }}>镜域里还没有角色。</p>
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)" }}>从一位灵胚(角色草稿)开始——可删除,不等于角色死亡。</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{items.map((c) => (
|
||||
<div key={c.id} className="char-list-item">
|
||||
<span className="avatar">{c.name.slice(0, 1)}</span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="char-list-item__name">{c.name}</div>
|
||||
<div className="char-list-item__sub">
|
||||
{STATUS_LABEL[c.status]?.cn === "灵胚" ? "灵胚 · 未启镜" : `道体 ${c.current_version_id ? "有版本" : "待定"}`} ·{" "}
|
||||
{new Date(c.created_at).toLocaleDateString("zh-CN")}
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge status={c.status} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{creating && (
|
||||
<form
|
||||
style={{ padding: "10px 8px 6px", display: "flex", gap: 8 }}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (name.trim()) create.mutate(name.trim());
|
||||
}}
|
||||
>
|
||||
<input
|
||||
className="input"
|
||||
placeholder="角色名,如:青梧"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
aria-label="新角色名"
|
||||
autoFocus
|
||||
/>
|
||||
<button className="btn btn--primary btn--sm" type="submit" disabled={create.isPending || !name.trim()}>
|
||||
{create.isPending ? "创建中…" : "创建灵胚"}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
{error && <p style={{ fontSize: 12, color: "var(--err)", padding: "4px 8px" }}>{error}</p>}
|
||||
|
||||
<div style={{ borderTop: "1px solid var(--line-soft)", marginTop: 8, paddingTop: 8 }}>
|
||||
<div className="char-list-item" style={{ cursor: "default", opacity: 0.6 }}>
|
||||
<span style={{ fontSize: 13, color: "var(--text-3)" }}>随缘 · 生成匿名灵胚候选(待接入)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { api, ApiError, type Character, type CursorPage } from "@/lib/api";
|
||||
import { PanelState } from "@/lib/queries";
|
||||
|
||||
/** 角色状态 → 徽章(稳定值来自后端,文化名前端本地化) */
|
||||
const STATUS_LABEL: Record<string, { cn: string; plain: string; tone: "info" | "muted" | "warn" | "ok" }> = {
|
||||
draft: { cn: "灵胚", plain: "草稿", tone: "warn" },
|
||||
active: { cn: "临照", plain: "运行中", tone: "info" },
|
||||
resting: { cn: "静修", plain: "停止运行", tone: "muted" },
|
||||
archived: { cn: "归藏", plain: "已隐藏", tone: "muted" },
|
||||
deceased: { cn: "死亡", plain: "永久闭合", tone: "muted" },
|
||||
};
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const label = STATUS_LABEL[status] ?? { cn: status, plain: "未知状态", tone: "muted" as const };
|
||||
return (
|
||||
<span className={`badge badge--${label.tone}`} style={{ fontSize: 11, padding: "1px 8px" }}>
|
||||
<span className="badge__dot" />
|
||||
{label.cn} · {label.plain}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** 镜生页:左列真实列表 + 右列真实详情(选中联动) */
|
||||
export function CharactersPagePanel() {
|
||||
const qc_client = { key: "characters" };
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [name, setName] = useState("");
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const list = useQuery({
|
||||
queryKey: [qc_client.key],
|
||||
queryFn: () => api<CursorPage<Character>>("/v1/characters?limit=20"),
|
||||
});
|
||||
const detail = useQuery({
|
||||
queryKey: ["character", selectedId],
|
||||
enabled: !!selectedId,
|
||||
queryFn: () => api<Character>(`/v1/characters/${selectedId}`),
|
||||
});
|
||||
|
||||
async function create(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) return;
|
||||
setError(null);
|
||||
try {
|
||||
const c = await api<Character>("/v1/characters", { method: "POST", json: { name: name.trim(), summary: "" } });
|
||||
setName("");
|
||||
setCreating(false);
|
||||
list.refetch();
|
||||
setSelectedId(c.id);
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : "创建失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function activate(id: string) {
|
||||
try {
|
||||
await api(`/v1/characters/${id}/activations`, { method: "POST", json: {} });
|
||||
list.refetch();
|
||||
detail.refetch();
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : "启镜失败");
|
||||
}
|
||||
}
|
||||
|
||||
const items = list.data?.data ?? [];
|
||||
|
||||
return (
|
||||
<div className="char-layout">
|
||||
<div>
|
||||
<div className="card" style={{ padding: 10 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "4px 8px 10px" }}>
|
||||
<span className="cn" style={{ fontSize: 15, fontWeight: 600 }}>
|
||||
镜生<span className="cn__plain">{items.length} 位角色</span>
|
||||
</span>
|
||||
<button className="btn btn--outline btn--sm" onClick={() => setCreating((v) => !v)}>创建</button>
|
||||
</div>
|
||||
|
||||
{list.isPending && <p style={{ fontSize: 13, color: "var(--text-3)", padding: "18px 8px" }}>正在读取…</p>}
|
||||
{list.isError && (
|
||||
<div style={{ padding: "18px 8px" }}>
|
||||
{(list.error as ApiError).status === 401 || (list.error as ApiError).status === 403 ? (
|
||||
<>
|
||||
<p style={{ fontSize: 13.5, color: "var(--text-2)", marginBottom: 12 }}>尚未登入,无法读取你的镜域。</p>
|
||||
<a className="btn btn--primary btn--sm" href="/login">去登入</a>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p style={{ fontSize: 13, color: "var(--err)", marginBottom: 10 }}>读取失败</p>
|
||||
<button className="btn btn--outline btn--sm" onClick={() => list.refetch()}>重试</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{items.length === 0 && !list.isPending && !list.isError && (
|
||||
<div style={{ padding: "22px 12px", textAlign: "center" }}>
|
||||
<p style={{ fontSize: 13.5, color: "var(--text-2)", marginBottom: 4 }}>镜域里还没有角色。</p>
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)" }}>从一位灵胚(角色草稿)开始——可删除,不等于角色死亡。</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{items.map((c) => (
|
||||
<div
|
||||
key={c.id}
|
||||
className={selectedId === c.id ? "char-list-item is-active" : "char-list-item"}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => setSelectedId(c.id)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") setSelectedId(c.id); }}
|
||||
aria-current={selectedId === c.id ? "true" : undefined}
|
||||
>
|
||||
<span className="avatar">{c.name.slice(0, 1)}</span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="char-list-item__name">{c.name}</div>
|
||||
<div className="char-list-item__sub">
|
||||
{c.status === "draft" ? "灵胚 · 未启镜" : c.status} · {new Date(c.created_at).toLocaleDateString("zh-CN")}
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge status={c.status} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{creating && (
|
||||
<form style={{ padding: "10px 8px 6px", display: "flex", gap: 8 }} onSubmit={create}>
|
||||
<input className="input" placeholder="角色名,如:青梧" value={name} onChange={(e) => setName(e.target.value)} aria-label="新角色名" autoFocus />
|
||||
<button className="btn btn--primary btn--sm" type="submit" disabled={!name.trim()}>创建灵胚</button>
|
||||
</form>
|
||||
)}
|
||||
{error && <p style={{ fontSize: 12, color: "var(--err)", padding: "4px 8px" }}>{error}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{!selectedId && (
|
||||
<div className="card">
|
||||
<div className="card__head">
|
||||
<span className="cn">镜生<span className="cn__plain">选择一位角色查看详情</span></span>
|
||||
</div>
|
||||
<p style={{ fontSize: 13, color: "var(--text-3)" }}>
|
||||
详情包含道体、内驱力(可编辑)、大周天与生命周期操作——全部来自真实接口。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedId && (
|
||||
<PanelState pending={detail.isPending} error={detail.error} onRetry={() => detail.refetch()}>
|
||||
{(() => {
|
||||
const c = detail.data as Character;
|
||||
if (!c) return null;
|
||||
return (
|
||||
<div>
|
||||
<div className="char-banner" aria-hidden="true" />
|
||||
<div className="char-head">
|
||||
<span className="avatar avatar--lg">{c.name.slice(0, 1)}</span>
|
||||
<div className="char-head__title">
|
||||
<h2 style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
|
||||
{c.name} <StatusBadge status={c.status} />
|
||||
</h2>
|
||||
<div className="char-head__motto mono" style={{ fontSize: 12 }}>{c.id}</div>
|
||||
</div>
|
||||
<div className="char-head__actions">
|
||||
{c.status === "draft" && (
|
||||
<button className="btn btn--primary" onClick={() => activate(c.id)}>启镜 · 正式创建</button>
|
||||
)}
|
||||
<a className="btn btn--outline" href="/conversation">进入镜树</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="section card">
|
||||
<div className="card__head">
|
||||
<span className="cn">道体<span className="cn__plain">稳定身份基线</span></span>
|
||||
</div>
|
||||
<p style={{ fontSize: 13.5, color: "var(--text-2)" }}>
|
||||
{Object.keys(c.identity_baseline ?? {}).length === 0
|
||||
? "道体尚未设定——启镜并完成初照自陈后形成首个版本。"
|
||||
: JSON.stringify(c.identity_baseline, null, 2)}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="section card">
|
||||
<div className="card__head">
|
||||
<span className="cn">内驱力<span className="cn__plain">动机维度 · 可编辑(PATCH drives)</span></span>
|
||||
</div>
|
||||
<p style={{ fontSize: 13.5, color: "var(--text-2)" }}>
|
||||
{(c.drives ?? []).length === 0 ? "尚未声明内驱力维度。" : JSON.stringify(c.drives, null, 2)}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="section card">
|
||||
<div className="card__head">
|
||||
<span className="cn">生命周期<span className="cn__plain">与数据删除分离</span></span>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
|
||||
{["静修 · 停止运行", "归藏 · 隐藏可恢复", "复照 · 从旧版本重开"].map((op) => (
|
||||
<button key={op} className="btn btn--outline btn--sm" title="生命周期操作端点接入后启用">{op}</button>
|
||||
))}
|
||||
{["铸魂 · 重大重塑(强确认)", "死亡 · 永久闭合(冷静期)"].map((op) => (
|
||||
<button key={op} className="btn btn--danger btn--sm" title="生命周期操作端点接入后启用">{op}</button>
|
||||
))}
|
||||
</div>
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", marginTop: 10 }}>
|
||||
铸魂与死亡走强确认与谱系留痕;后端 lifecycle 端点(POST /v1/characters/{"{id}"}/{"{action}"})已具备,交互流程待接入。
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</PanelState>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useCreateWorkspace } from "@/lib/queries";
|
||||
import type { Workspace } from "@/lib/api";
|
||||
|
||||
export function CollaborationPanel() {
|
||||
const create = useCreateWorkspace();
|
||||
const [name, setName] = useState("");
|
||||
const [created, setCreated] = useState<Workspace | null>(null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="realm-banner" role="note">
|
||||
成员通过同一明确契约协作;编辑、发布、市场上架与财务权限分别授予。
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="card__head">
|
||||
<span className="cn">同契台<span className="cn__plain">创建协作空间</span></span>
|
||||
</div>
|
||||
<form
|
||||
style={{ display: "flex", gap: 10, flexWrap: "wrap" }}
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) return;
|
||||
const ws = await create.mutateAsync({ name: name.trim() });
|
||||
setCreated(ws);
|
||||
setName("");
|
||||
}}
|
||||
>
|
||||
<input className="input" style={{ flex: 1, minWidth: 220 }} placeholder="空间名,如:镜斋工作室" value={name} onChange={(e) => setName(e.target.value)} aria-label="空间名" />
|
||||
<button className="btn btn--primary btn--sm" type="submit" disabled={create.isPending || !name.trim()}>
|
||||
{create.isPending ? "创建中…" : "创建空间"}
|
||||
</button>
|
||||
</form>
|
||||
{create.isError && (
|
||||
<p style={{ fontSize: 12, color: "var(--err)", marginTop: 8 }}>{(create.error as { message?: string }).message}</p>
|
||||
)}
|
||||
{created && (
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<div className="list-row">
|
||||
<span className="avatar avatar--sm">契</span>
|
||||
<div className="list-row__body">
|
||||
<div className="list-row__title">{created.name}</div>
|
||||
<div className="list-row__sub mono">{created.id}</div>
|
||||
</div>
|
||||
<span className="badge badge--ok" style={{ fontSize: 11 }}><span className="badge__dot" />已创建</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", marginTop: 10 }}>
|
||||
成员、项目分支与审稿接口(邀请 / 版本 / 合卷)已具备;空间列表端点待补齐后在此展示全部空间。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { PanelState, useContents, useCreateContent } from "@/lib/queries";
|
||||
import type { CommunityContent } from "@/lib/api";
|
||||
|
||||
export function CommunityPanel() {
|
||||
const contents = useContents();
|
||||
const create = useCreateContent();
|
||||
const [title, setTitle] = useState("");
|
||||
const list = contents.data?.data ?? [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="card" style={{ marginBottom: 16, padding: 16 }}>
|
||||
<div className="card__head"><span className="cn">发布<span className="cn__plain">动态 · 公开内容</span></span></div>
|
||||
<form
|
||||
style={{ display: "flex", gap: 10, flexWrap: "wrap" }}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (title.trim()) create.mutate({ kind: "post", title: title.trim() });
|
||||
}}
|
||||
>
|
||||
<input
|
||||
className="input"
|
||||
style={{ flex: 1, minWidth: 220 }}
|
||||
placeholder="发布一条动态,如:共读《陶庵梦忆》第一讲笔记"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
aria-label="动态标题"
|
||||
/>
|
||||
<button className="btn btn--primary btn--sm" type="submit" disabled={create.isPending || !title.trim()}>
|
||||
{create.isPending ? "发布中…" : "发布"}
|
||||
</button>
|
||||
</form>
|
||||
{create.isError && (
|
||||
<p style={{ fontSize: 12, color: "var(--err)", marginTop: 8 }}>{(create.error as { message?: string }).message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PanelState pending={contents.isPending} error={contents.error} onRetry={() => contents.refetch()} empty={list.length === 0}>
|
||||
{list.map((c: CommunityContent) => (
|
||||
<div className="card post" key={c.id} style={{ marginBottom: 14 }}>
|
||||
<div className="post__head">
|
||||
<span className="avatar avatar--sm">镜</span>
|
||||
<div>
|
||||
<div className="post__author">镜域 {c.author_domain_id.slice(-6)}</div>
|
||||
<div className="post__time">{new Date(c.created_at).toLocaleString("zh-CN", { hour12: false })}</div>
|
||||
</div>
|
||||
<span className="badge badge--ok" style={{ marginLeft: "auto", fontSize: 11 }}>
|
||||
<span className="badge__dot" />已刊 · {c.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="post__body">
|
||||
<p style={{ color: "var(--text-1)", fontSize: 14.5 }}>{c.title ?? "(无标题)"}</p>
|
||||
</div>
|
||||
<div className="post__foot">
|
||||
<button className="act-btn">评论</button>
|
||||
<button className="act-btn">收藏</button>
|
||||
<button className="act-btn" style={{ marginLeft: "auto" }}>鸣冤</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</PanelState>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import type { ContentBlockLike } from "@/lib/api";
|
||||
|
||||
/**
|
||||
* ContentBlock 判别联合渲染器(静态阶段)。
|
||||
* 契约要求:穷尽判别、未知类型走兼容占位不崩溃;Markdown 未闭合结构保持源码态。
|
||||
* 完整的分级实时渲染(Shiki/KaTeX/Mermaid Worker)按 frontend-content-rendering.md 在后续接入。
|
||||
*/
|
||||
|
||||
function src(b: ContentBlockLike, ...keys: string[]): string {
|
||||
for (const k of keys) {
|
||||
const v = b[k];
|
||||
if (typeof v === "string" && v) return v;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function TextBlock({ b }: { b: ContentBlockLike }) {
|
||||
const text = src(b, "text", "source");
|
||||
if (b.status === "streaming") {
|
||||
return <p style={{ whiteSpace: "pre-wrap" }}>{text}<span className="stream-cursor" aria-hidden="true" /></p>;
|
||||
}
|
||||
return <p style={{ whiteSpace: "pre-wrap" }}>{text}</p>;
|
||||
}
|
||||
|
||||
function CodeBlock({ b }: { b: ContentBlockLike }) {
|
||||
return (
|
||||
<div className="block">
|
||||
<div className="block__head"><span className="lang">{src(b, "language") || "code"}</span>{src(b, "filename") && <span className="fname">{src(b, "filename")}</span>}</div>
|
||||
<pre className="code">{src(b, "source", "code", "text")}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function QuoteBlock({ b }: { b: ContentBlockLike }) {
|
||||
return (
|
||||
<blockquote className="quote">
|
||||
{src(b, "text", "content")}
|
||||
{src(b, "source") && <span className="quote__src">引文锚 · {src(b, "source")}</span>}
|
||||
</blockquote>
|
||||
);
|
||||
}
|
||||
|
||||
function TableBlock({ b }: { b: ContentBlockLike }) {
|
||||
const columns = Array.isArray(b.columns) ? (b.columns as string[]) : [];
|
||||
const rows = Array.isArray(b.rows) ? (b.rows as unknown[][]) : [];
|
||||
return (
|
||||
<div className="block">
|
||||
<div className="block__head">{src(b, "caption") || "表格"}</div>
|
||||
<div className="table-wrap">
|
||||
<table className="data">
|
||||
{columns.length > 0 && (
|
||||
<thead><tr>{columns.map((c, i) => <th key={i}>{c}</th>)}</tr></thead>
|
||||
)}
|
||||
<tbody>
|
||||
{rows.map((row, i) => (
|
||||
<tr key={i}>{row.map((cell, j) => <td key={j}>{String(cell ?? "")}</td>)}</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CalloutBlock({ b }: { b: ContentBlockLike }) {
|
||||
const kind = src(b, "kind") || "info";
|
||||
const tone = kind === "danger" ? "var(--err)" : kind === "warning" || kind === "success" ? "var(--warn)" : "var(--accent)";
|
||||
return (
|
||||
<div className="block" style={{ borderLeft: `3px solid ${tone}` }}>
|
||||
<div className="block__body">
|
||||
{src(b, "title") && <b style={{ display: "block", marginBottom: 4 }}>{src(b, "title")}</b>}
|
||||
<p style={{ whiteSpace: "pre-wrap" }}>{src(b, "content")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MathBlock({ b }: { b: ContentBlockLike }) {
|
||||
return (
|
||||
<div className="block">
|
||||
<div className="math">
|
||||
{src(b, "source")}
|
||||
{src(b, "fallback_text") && <span className="math__fallback">fallback:{src(b, "fallback_text")}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MermaidBlock({ b }: { b: ContentBlockLike }) {
|
||||
return (
|
||||
<div className="block">
|
||||
<div className="block__head"><span className="lang">mermaid</span><span>完成后以 strict mode 隔离渲染</span></div>
|
||||
<pre className="code">{src(b, "source")}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MediaBlock({ b }: { b: ContentBlockLike }) {
|
||||
const label = src(b, "file_id") ? `文件 ${src(b, "file_id").slice(-8)}` : "媒体";
|
||||
return (
|
||||
<div className="block">
|
||||
<div className="block__head">{b.type}{src(b, "filename") && <span className="fname">{src(b, "filename")}</span>}</div>
|
||||
<div className="block__body"><p style={{ fontSize: 12.5, color: "var(--text-3)" }}>{label} · 资源就绪后加载(短期授权 URL 换取待接入)</p></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBlock({ b }: { b: ContentBlockLike }) {
|
||||
const label: Record<string, string> = {
|
||||
tool_call: "工具调用", tool_result: "工具结果", approval_request: "请命",
|
||||
agent_status: "Agent 状态", artifact: "产物", citation: "引文", error: "错误",
|
||||
};
|
||||
return (
|
||||
<div className="block tool">
|
||||
<div className="tool__head">
|
||||
<span className="tool__name">{label[b.type] ?? b.type}</span>
|
||||
<span className="tool__purpose">{src(b, "tool_name", "name", "title")}</span>
|
||||
{src(b, "status") && <span className="badge badge--muted" style={{ marginLeft: "auto", fontSize: 11 }}><span className="badge__dot" />{src(b, "status")}</span>}
|
||||
</div>
|
||||
<div className="tool__body">
|
||||
<div className="tool__row"><span className="k2">内容</span><span className="mono" style={{ fontSize: 11.5 }}>{JSON.stringify(b).slice(0, 160)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BlockRenderer({ block }: { block: ContentBlockLike }) {
|
||||
switch (block.type) {
|
||||
case "text":
|
||||
case "markdown":
|
||||
return <TextBlock b={block} />;
|
||||
case "code":
|
||||
return <CodeBlock b={block} />;
|
||||
case "quote":
|
||||
return <QuoteBlock b={block} />;
|
||||
case "table":
|
||||
return <TableBlock b={block} />;
|
||||
case "callout":
|
||||
return <CalloutBlock b={block} />;
|
||||
case "math":
|
||||
return <MathBlock b={block} />;
|
||||
case "mermaid":
|
||||
return <MermaidBlock b={block} />;
|
||||
case "image":
|
||||
case "video":
|
||||
case "audio":
|
||||
case "file":
|
||||
case "gallery":
|
||||
case "avatar":
|
||||
return <MediaBlock b={block} />;
|
||||
case "tool_call":
|
||||
case "tool_result":
|
||||
case "approval_request":
|
||||
case "agent_status":
|
||||
case "artifact":
|
||||
case "citation":
|
||||
case "error":
|
||||
return <StatusBlock b={block} />;
|
||||
default:
|
||||
return (
|
||||
<div className="block">
|
||||
<div className="block__body">
|
||||
<p style={{ fontSize: 12.5, color: "var(--text-3)" }}>
|
||||
当前客户端暂不支持「{block.type}」块——已按兼容占位处理,内容不丢失。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronsLeft } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
/**
|
||||
* 镜树三栏布局:左右面板可折叠,内容区自适应放宽(773 → 1000 → 1140)。
|
||||
* 折叠时浮动恢复按钮出现在消息流顶角。
|
||||
*/
|
||||
export function ConvLayout({
|
||||
side,
|
||||
main,
|
||||
ctx,
|
||||
}: {
|
||||
side: React.ReactNode;
|
||||
main: React.ReactNode;
|
||||
ctx: React.ReactNode;
|
||||
}) {
|
||||
const [noLeft, setNoLeft] = useState(false);
|
||||
const [noRight, setNoRight] = useState(false);
|
||||
const cls = ["conv-layout", noLeft ? "no-left" : "", noRight ? "no-right" : ""]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<div className={cls}>
|
||||
<aside className="conv-side" aria-label="会话列表">
|
||||
<div className="conv-side__head">
|
||||
<span className="avatar avatar--sm">梧</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div className="cn" style={{ fontWeight: 600 }}>
|
||||
青梧
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: "var(--text-3)" }}>道体 v3 · 临照</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="panel-toggle"
|
||||
onClick={() => setNoLeft(true)}
|
||||
aria-label="收起会话列表"
|
||||
title="收起会话列表"
|
||||
style={{ marginLeft: 4 }}
|
||||
>
|
||||
<ChevronsLeft />
|
||||
</button>
|
||||
</div>
|
||||
{side}
|
||||
</aside>
|
||||
<section className="conv-main">
|
||||
<button
|
||||
type="button"
|
||||
className="conv-restore conv-restore--left"
|
||||
onClick={() => setNoLeft(false)}
|
||||
aria-label="展开会话列表"
|
||||
title="展开会话列表"
|
||||
>
|
||||
<ChevronsLeft />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="conv-restore conv-restore--right"
|
||||
onClick={() => setNoRight(false)}
|
||||
aria-label="展开分支面板"
|
||||
title="展开分支与上下文面板"
|
||||
>
|
||||
<ChevronsLeft style={{ transform: "rotate(180deg)" }} />
|
||||
</button>
|
||||
{main}
|
||||
</section>
|
||||
<aside className="conv-ctx" aria-label="分支与上下文">
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 6 }}>
|
||||
<button
|
||||
type="button"
|
||||
className="panel-toggle"
|
||||
onClick={() => setNoRight(true)}
|
||||
aria-label="收起面板"
|
||||
title="收起分支与上下文面板"
|
||||
>
|
||||
<ChevronsLeft style={{ transform: "rotate(180deg)" }} />
|
||||
</button>
|
||||
</div>
|
||||
{ctx}
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
"use client";
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Send } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { BlockRenderer } from "@/components/conversation/BlockRenderer";
|
||||
import { EffortControl, SearchPill } from "@/components/conversation/EffortControl";
|
||||
import { ConvLayout } from "@/components/conversation/ConvLayout";
|
||||
import {
|
||||
useConversation,
|
||||
useCreateConversation,
|
||||
useCreateToolCall,
|
||||
useMessages,
|
||||
useRunEvents,
|
||||
useSendMessage,
|
||||
} from "@/lib/queries";
|
||||
import type { Character, CursorPage, Message } from "@/lib/api";
|
||||
|
||||
function MessageRow({ msg }: { msg: Message }) {
|
||||
if (msg.role === "user") {
|
||||
return (
|
||||
<div className="msg msg--user">
|
||||
<div className="msg__meta">
|
||||
<span className="cn">问节</span>
|
||||
<span className="mono">{msg.id.slice(-6)}</span>
|
||||
<span>{new Date(msg.created_at).toLocaleString("zh-CN", { hour12: false })}</span>
|
||||
</div>
|
||||
<div className="bubble">
|
||||
{msg.content.map((b, i) => (
|
||||
<BlockRenderer key={String(b.id ?? i)} block={b} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="msg">
|
||||
<div className="msg--char__head">
|
||||
<span className="avatar avatar--sm">镜</span>
|
||||
<span className="msg--char__name">{msg.role === "assistant" ? "角色" : msg.role}</span>
|
||||
{msg.canonical_scope && (
|
||||
<span className="badge badge--muted" style={{ fontSize: 11 }}>
|
||||
<span className="badge__dot" />
|
||||
归真 · {msg.canonical_scope}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="msg__blocks" style={{ paddingLeft: 0 }}>
|
||||
<div className="block block--text">
|
||||
<div className="block__body">{msg.content.map((b, i) => <BlockRenderer key={String(b.id ?? i)} block={b} />)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConversationPanel() {
|
||||
const qc = useQueryClient();
|
||||
const [convId, setConvId] = useState<string | null>(null);
|
||||
const [draft, setDraft] = useState("");
|
||||
const [activeRunId, setActiveRunId] = useState<string | null>(null);
|
||||
const [title, setTitle] = useState("");
|
||||
const [characterId, setCharacterId] = useState("");
|
||||
const [effortHint, setEffortHint] = useState("深虑 · 中");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// URL ?c= 携带当前会话
|
||||
useEffect(() => {
|
||||
const c = new URLSearchParams(window.location.search).get("c");
|
||||
if (c) setConvId(c);
|
||||
}, []);
|
||||
|
||||
const characters = useQuerySafe<Character[]>("/v1/characters?limit=20");
|
||||
const conversation = useConversation(convId);
|
||||
const messages = useMessages(convId);
|
||||
const send = useSendMessage(convId);
|
||||
const create = useCreateConversation();
|
||||
|
||||
function openConversation(id: string) {
|
||||
setConvId(id);
|
||||
window.history.replaceState(null, "", `/conversation?c=${id}`);
|
||||
}
|
||||
|
||||
async function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!draft.trim() || !convId) return;
|
||||
setError(null);
|
||||
try {
|
||||
const result = await send.mutateAsync(draft.trim());
|
||||
setDraft("");
|
||||
if (result?.run?.id) setActiveRunId(result.run.id);
|
||||
} catch (err) {
|
||||
setError((err as { message?: string }).message ?? "发送失败");
|
||||
}
|
||||
}
|
||||
|
||||
const items = (characters.data as Character[] | undefined) ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!characterId && items.length > 0) setCharacterId(items[0].id);
|
||||
}, [items, characterId]);
|
||||
|
||||
return (
|
||||
<ConvLayout
|
||||
side={
|
||||
<>
|
||||
<div style={{ padding: "4px 6px 12px" }}>
|
||||
<form
|
||||
style={{ display: "flex", flexDirection: "column", gap: 8 }}
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
try {
|
||||
const c = await create.mutateAsync({
|
||||
title: title.trim() || "未命名对话",
|
||||
character_id: characterId || undefined,
|
||||
});
|
||||
setTitle("");
|
||||
openConversation(c.id);
|
||||
qc.invalidateQueries({ queryKey: ["messages"] });
|
||||
} catch (err) {
|
||||
setError((err as { message?: string }).message ?? "镜树创建失败,请重试");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<select
|
||||
className="input"
|
||||
value={characterId}
|
||||
onChange={(e) => setCharacterId(e.target.value)}
|
||||
aria-label="选择角色"
|
||||
>
|
||||
<option value="">不绑定角色</option>
|
||||
{items.map((c) => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
className="input"
|
||||
placeholder="新对话主题(可留空)"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
aria-label="新对话主题"
|
||||
/>
|
||||
<button className="btn btn--outline btn--sm" type="submit" disabled={create.isPending}>
|
||||
{create.isPending ? "创建中…" : "开始新镜树"}
|
||||
</button>
|
||||
{error && (
|
||||
<p role="alert" style={{ fontSize: 12, color: "var(--err)" }}>{error}</p>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
{convId && (
|
||||
<a className="conv-item is-active" href={`/conversation?c=${convId}`} aria-current="page">
|
||||
<div className="conv-item__title">
|
||||
<span className="conv-item__dot conv-item__dot--active" title="当前会话" />
|
||||
{String((conversation.data as { title?: string } | undefined)?.title ?? "当前对话")}
|
||||
</div>
|
||||
<div className="conv-item__sub">会话列表端点待 WP-03 补齐,当前按会话直达</div>
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
main={
|
||||
<>
|
||||
<div className="conv-stream">
|
||||
<div className="conv-stream__inner">
|
||||
{!convId && (
|
||||
<div className="realm-banner" role="status">
|
||||
从左侧选择角色并开始一棵新的镜树——分支、候答与界域切换随消息数据启用。
|
||||
</div>
|
||||
)}
|
||||
{convId && messages.isPending && <p style={{ color: "var(--text-3)", fontSize: 13 }}>正在读取消息…</p>}
|
||||
{convId && messages.isError && (
|
||||
<p style={{ color: "var(--err)", fontSize: 13 }}>
|
||||
读取失败:{(messages.error as { message?: string }).message ?? "请重试"}
|
||||
</p>
|
||||
)}
|
||||
{convId &&
|
||||
((messages.data?.data.length ?? 0) === 0) &&
|
||||
!messages.isPending && (
|
||||
<p style={{ color: "var(--text-3)", fontSize: 13, textAlign: "center", padding: "40px 0" }}>
|
||||
这棵镜树还没有消息——在下方呈镜第一句。
|
||||
</p>
|
||||
)}
|
||||
{messages.data?.data.map((m) => <MessageRow key={m.id} msg={m} />)}
|
||||
<RunStream runId={activeRunId} onDone={(finalStatus) => { messages.refetch(); if (finalStatus !== "failed") setActiveRunId(null); }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="composer">
|
||||
<div className="composer__inner">
|
||||
<div className="composer__box">
|
||||
<textarea
|
||||
className="composer__ta"
|
||||
rows={2}
|
||||
placeholder={convId ? "对角色说……(Enter 发送 · Shift+Enter 换行)" : "先在左侧开始一棵镜树"}
|
||||
aria-label="消息输入"
|
||||
disabled={!convId}
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
submit(e as unknown as React.FormEvent);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="composer__bar">
|
||||
<EffortControl
|
||||
effort="中"
|
||||
onEffortChange={(v) => setEffortHint("深虑 · " + v)}
|
||||
onToggle={(on) => setEffortHint(on ? "深虑 · 中" : "深虑 · 关")}
|
||||
/>
|
||||
<SearchPill />
|
||||
<span className="spacer" style={{ flex: 1 }} />
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--primary btn--sm"
|
||||
aria-label="发送(呈镜)"
|
||||
title="发送 · 呈镜"
|
||||
disabled={!convId || send.isPending || !draft.trim()}
|
||||
onClick={() => submit({ preventDefault: () => {} } as React.FormEvent)}
|
||||
>
|
||||
<Send />呈镜
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{error && <p style={{ fontSize: 12, color: "var(--err)", marginTop: 6 }}>{error}</p>}
|
||||
<div className="composer__hint">
|
||||
<span>模型 <span className="k">会话临时(网关路由待 WP-05 暴露选择端点)</span></span>
|
||||
<span>思考 <span className="k">{effortHint}</span>(点深虑开关,右侧调力度)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
ctx={
|
||||
<>
|
||||
<section className="ctx-section">
|
||||
<div className="ctx-section__head">
|
||||
<span className="cn">镜枝<span className="cn__plain">分支结构</span></span>
|
||||
</div>
|
||||
{convId ? (
|
||||
<BranchTree convId={convId} />
|
||||
) : (
|
||||
<p style={{ fontSize: 12.5, color: "var(--text-3)" }}>开始会话后显示真实分支。</p>
|
||||
)}
|
||||
</section>
|
||||
<section className="ctx-section">
|
||||
<div className="ctx-section__head">
|
||||
<span className="cn">上下文镜<span className="cn__plain">ContextSnapshot</span></span>
|
||||
</div>
|
||||
<p style={{ fontSize: 12.5, color: "var(--text-3)" }}>
|
||||
召回证据与省略项随 WP-04 ContextSnapshot 暴露后接入;当前仅呈现结构占位。
|
||||
</p>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** 分支树:真实 branches 数据(主脉/当前分支) */
|
||||
function BranchTree({ convId }: { convId: string }) {
|
||||
const branches = useQuerySafe<{ id: string; is_current?: boolean; [k: string]: unknown }>(
|
||||
`/v1/conversations/${convId}/branches`,
|
||||
);
|
||||
if (branches.pending) return <p style={{ fontSize: 12.5, color: "var(--text-3)" }}>读取分支…</p>;
|
||||
if (branches.error) return <p style={{ fontSize: 12.5, color: "var(--err)" }}>分支读取失败</p>;
|
||||
const list = branches.data ?? [];
|
||||
if (list.length === 0)
|
||||
return <p style={{ fontSize: 12.5, color: "var(--text-3)" }}>尚无分支——重生成会产生候答分支。</p>;
|
||||
return (
|
||||
<div className="tree" role="group" aria-label="分支树">
|
||||
<ul>
|
||||
{list.map((b) => (
|
||||
<li key={b.id}>
|
||||
<div className={b.is_current ? "tree__node tree__node--main tree__node--now" : "tree__node"}>
|
||||
<span className="nm mono">{b.id.slice(-8)}</span>
|
||||
{b.is_current && <span className="tree__tag tree__tag--main">主脉 · 当前</span>}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 轻量查询(面板内局部使用) */
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
function useQuerySafe<T>(path: string) {
|
||||
const q = useQuery({ queryKey: ["panel", path], queryFn: () => api<CursorPage<T> | { data: T[] }>(path) });
|
||||
const data = (q.data as CursorPage<T> | undefined)?.data ?? ((q.data as { data?: T[] } | undefined)?.data as T[] | undefined);
|
||||
return { pending: q.isPending, error: q.error, data, refetch: q.refetch };
|
||||
}
|
||||
|
||||
|
||||
/** Run 流式区:临时芽体(delta 累积)+ 失败卡 + 完成后由父级对账权威快照 */
|
||||
function RunStream({ runId, onDone }: { runId: string | null; onDone: (finalStatus: string | null) => void }) {
|
||||
const stream = useRunEvents(runId, onDone);
|
||||
if (!runId) return null;
|
||||
return (
|
||||
<div className="msg">
|
||||
<div className="msg--char__head">
|
||||
<span className="avatar avatar--sm">镜</span>
|
||||
<span className="msg--char__name">临时芽体</span>
|
||||
<span className={`badge badge--${stream.done ? (stream.finalStatus === "failed" ? "err" : "ok") : "info"}`}>
|
||||
<span className="badge__dot" />
|
||||
{stream.done
|
||||
? stream.finalStatus === "failed" ? `受阻 · ${stream.runStatus}` : `已成 · ${stream.runStatus}`
|
||||
: `运转 · ${stream.runStatus}`}
|
||||
<span className="mono">{stream.runStatus}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="msg__blocks" style={{ paddingLeft: 0 }}>
|
||||
{stream.blocks.map((b) => (
|
||||
<div key={b.id} className={b.status === "streaming" ? "block sprout" : "block"}>
|
||||
{b.status === "streaming" && (
|
||||
<div className="sprout__head">
|
||||
<span className="cn" style={{ fontWeight: 600 }}>临时芽体</span>
|
||||
<span className="cn__plain">流式生成中 · 完成后才固化</span>
|
||||
<span style={{ marginLeft: "auto" }} className="mono">{b.type}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="block__body">
|
||||
{b.status === "failed" ? (
|
||||
<p style={{ color: "var(--err)", fontSize: 13.5 }}>
|
||||
受阻 · 失败:<span className="mono">{b.error}</span>
|
||||
{b.error === "no_endpoint_available" && (
|
||||
<span style={{ color: "var(--text-3)", display: "block", fontSize: 12, marginTop: 4 }}>
|
||||
没有可用的模型端点——在神识阁配置端点(WP-05)后重试;流中断不伪装完成。
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p style={{ whiteSpace: "pre-wrap" }}>
|
||||
{b.text}
|
||||
{b.status === "streaming" && <span className="stream-cursor" aria-hidden="true" />}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{stream.blocks.length === 0 && !stream.done && (
|
||||
<p style={{ fontSize: 13, color: "var(--text-3)" }}>正在连接事件流…</p>
|
||||
)}
|
||||
{stream.done && stream.finalStatus && (
|
||||
<p style={{ fontSize: 11.5, color: "var(--text-3)" }}>
|
||||
message.completed(final_status: <span className="mono">{stream.finalStatus}</span>)——已按权威快照对账。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 工具调用发起(挂在当前会话最新 Run 或新 Run 上) */
|
||||
export function ToolCallForm({ runId }: { runId: string | null }) {
|
||||
const create = useCreateToolCall(runId);
|
||||
const [input, setInput] = useState('{"expression":"1+2*3"}');
|
||||
if (!runId) return null;
|
||||
return (
|
||||
<form
|
||||
style={{ padding: "8px 6px", display: "flex", gap: 8, flexWrap: "wrap" }}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
create.mutate({ tool: "calculate", input: JSON.parse(input) });
|
||||
} catch {
|
||||
/* JSON 非法时忽略 */
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 12, color: "var(--text-3)", alignSelf: "center" }} className="mono">calculate</span>
|
||||
<input className="input" style={{ flex: 1, minWidth: 180 }} value={input} onChange={(e) => setInput(e.target.value)} aria-label="工具参数 JSON" />
|
||||
<button className="btn btn--outline btn--sm" type="submit" disabled={create.isPending}>
|
||||
{create.isPending ? "调用中…" : "发起工具调用"}
|
||||
</button>
|
||||
{create.isError && <span style={{ fontSize: 12, color: "var(--err)", alignSelf: "center" }}>{(create.error as { message?: string }).message}</span>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
/**
|
||||
* 深虑控制:点击深虑 = 开启深度思考;开启后右侧伴随出现力度选项(独立控件,只调档);
|
||||
* 再次点击 = 关闭,力度选项随之消失;重开保留上次档位。
|
||||
*/
|
||||
export function EffortControl({
|
||||
effort,
|
||||
onEffortChange,
|
||||
onToggle,
|
||||
}: {
|
||||
effort: string;
|
||||
onEffortChange: (v: "低" | "中" | "高") => void;
|
||||
onToggle: (on: boolean) => void;
|
||||
}) {
|
||||
const [on, setOn] = useState(true);
|
||||
const [val, setVal] = useState<"低" | "中" | "高">(effort as "低" | "中" | "高");
|
||||
|
||||
const toggle = () => {
|
||||
const next = !on;
|
||||
setOn(next);
|
||||
onToggle(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={on ? "pill-btn is-on" : "pill-btn"}
|
||||
aria-pressed={on}
|
||||
data-pill="effort"
|
||||
onClick={toggle}
|
||||
title={
|
||||
on
|
||||
? "深虑 · 深度思考已开启,再次点击关闭"
|
||||
: "深虑 · 已关闭:本条消息不思考,直出答复(推理型模型不可关)"
|
||||
}
|
||||
>
|
||||
深虑
|
||||
</button>
|
||||
<span className="effort-seg" data-effort-seg hidden={!on} role="radiogroup" aria-label="推理力度">
|
||||
{(["低", "中", "高"] as const).map((level) => (
|
||||
<button
|
||||
key={level}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={val === level}
|
||||
title={{ 低: "低 · 快省", 中: "中 · 均衡", 高: "高 · 深究慢" }[level]}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setVal(level);
|
||||
onEffortChange(level);
|
||||
}}
|
||||
>
|
||||
{level}
|
||||
</button>
|
||||
))}
|
||||
</span>
|
||||
<span hidden>
|
||||
<input readOnly value={on ? val : "关"} aria-hidden="true" tabIndex={-1} />
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** 博闻:独立开关(联网检索,走百工坊白名单) */
|
||||
export function SearchPill() {
|
||||
const [on, setOn] = useState(false);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={on ? "pill-btn is-on" : "pill-btn"}
|
||||
aria-pressed={on}
|
||||
onClick={() => setOn((v) => !v)}
|
||||
title="博闻 · 本条消息启用联网检索(走百工坊白名单,点击开关)"
|
||||
>
|
||||
博闻<small>{on ? "开" : "关"}</small>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { PanelState, useCreateListing, useFinanceSummary, useListings } from "@/lib/queries";
|
||||
import type { Listing } from "@/lib/api";
|
||||
|
||||
function yuan(minor: number | null, currency: string | null) {
|
||||
if (minor == null) return "免费";
|
||||
return `¥${(minor / 100).toFixed(2)}${currency ? ` ${currency.toUpperCase()}` : ""}`;
|
||||
}
|
||||
|
||||
export function MarketplacePanel() {
|
||||
const listings = useListings();
|
||||
const finance = useFinanceSummary();
|
||||
const create = useCreateListing();
|
||||
const [title, setTitle] = useState("");
|
||||
const [price, setPrice] = useState("");
|
||||
const list = listings.data?.data ?? [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="realm-banner" role="note">
|
||||
购买的是版本与许可(市契),不是人格所有权。金额、分润与退契规则始终直白展示。
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginBottom: 16, padding: 16 }}>
|
||||
<div className="card__head"><span className="cn">上架<span className="cn__plain">成为创作者</span></span></div>
|
||||
<form
|
||||
style={{ display: "flex", gap: 10, flexWrap: "wrap" }}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (!title.trim()) return;
|
||||
const minor = Math.round(parseFloat(price || "0") * 100);
|
||||
create.mutate({ title: title.trim(), price_minor: isNaN(minor) ? 0 : minor, currency: "cny" });
|
||||
}}
|
||||
>
|
||||
<input className="input" style={{ flex: 1, minWidth: 200 }} placeholder="商品名,如:雾中茶寮 · 世界合集" value={title} onChange={(e) => setTitle(e.target.value)} aria-label="商品名" />
|
||||
<input className="input" style={{ width: 120 }} placeholder="价格 ¥(0 为免费)" inputMode="decimal" value={price} onChange={(e) => setPrice(e.target.value)} aria-label="价格" />
|
||||
<button className="btn btn--primary btn--sm" type="submit" disabled={create.isPending || !title.trim()}>
|
||||
{create.isPending ? "上架中…" : "上架"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<section className="section">
|
||||
<div className="section__head">
|
||||
<span className="cn">云市<span className="cn__plain">商品</span></span>
|
||||
</div>
|
||||
<PanelState pending={listings.isPending} error={listings.error} onRetry={() => listings.refetch()} empty={list.length === 0}>
|
||||
<div className="asset-grid">
|
||||
{list.map((l: Listing) => (
|
||||
<div className="asset-card" key={l.id}>
|
||||
<div className="asset-thumb" style={{ background: "linear-gradient(150deg,#2f4858,#6a8caf 60%,#3b5a70)" }}>
|
||||
<span className="glyph">{l.title.slice(0, 1)}</span>
|
||||
</div>
|
||||
<div className="asset-body">
|
||||
<span className="asset-name">{l.title}</span>
|
||||
<span className="asset-sub">{l.status === "published" ? "已发布" : l.status}</span>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 2 }}>
|
||||
<span className="price">{l.price_minor ? yuan(l.price_minor, l.currency) : <span className="price price--free">免费</span>}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PanelState>
|
||||
</section>
|
||||
|
||||
<section className="section card">
|
||||
<div className="card__head"><span className="cn">结算<span className="cn__plain">创作者收益</span></span></div>
|
||||
<PanelState pending={finance.isPending} error={finance.error} empty={Object.keys(finance.data?.totals_minor ?? {}).length === 0}>
|
||||
<div style={{ display: "flex", gap: 28, flexWrap: "wrap", alignItems: "center" }}>
|
||||
{Object.entries(finance.data?.totals_minor ?? {}).map(([cur, minor]) => (
|
||||
<div key={cur}>
|
||||
<div style={{ fontSize: 11.5, color: "var(--text-3)" }}>{cur.toUpperCase()} 余额</div>
|
||||
<div className="price" style={{ fontSize: 22 }}>{yuan(minor, cur)}</div>
|
||||
</div>
|
||||
))}
|
||||
<span className="badge badge--ok"><span className="badge__dot" />四账平衡 · {String(finance.data?.balanced ?? false)}</span>
|
||||
</div>
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", marginTop: 10 }}>
|
||||
Ledger 不可变,修正写反向分录;每日自动对账(支付 / 订单 / Ledger / 结算)。
|
||||
</p>
|
||||
</PanelState>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { PanelState, useDecideCandidate, useMemories, useMemoryCandidates } from "@/lib/queries";
|
||||
import type { Memory, MemoryCandidate } from "@/lib/api";
|
||||
|
||||
const KIND_LABEL: Record<string, string> = {
|
||||
fact: "事实", preference: "偏好", event: "事件", relationship: "关系",
|
||||
};
|
||||
|
||||
function candidateText(c: MemoryCandidate) {
|
||||
const p = c.payload as { text?: string; summary?: string; content?: string };
|
||||
return p.text ?? p.summary ?? p.content ?? JSON.stringify(c.payload);
|
||||
}
|
||||
|
||||
export function MemoryPanel() {
|
||||
const candidates = useMemoryCandidates();
|
||||
const memories = useMemories();
|
||||
const decide = useDecideCandidate();
|
||||
const [tab, setTab] = useState<"candidates" | "memories">("candidates");
|
||||
|
||||
const candList = candidates.data?.data ?? [];
|
||||
const memList = memories.data?.data ?? [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="tabs" role="tablist" aria-label="记忆类型">
|
||||
<button className={tab === "candidates" ? "tab is-on" : "tab"} role="tab" aria-selected={tab === "candidates"} onClick={() => setTab("candidates")}>
|
||||
候选审阅 <span className="cnt">{candList.length}</span>
|
||||
</button>
|
||||
<button className={tab === "memories" ? "tab is-on" : "tab"} role="tab" aria-selected={tab === "memories"} onClick={() => setTab("memories")}>
|
||||
明忆 <span className="cnt">{memList.length}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="realm-banner" role="note">
|
||||
归真的约束:候选经你确认才会影响未来上下文;境界内容只成世界知识候选,幻境内容须手动入忆。
|
||||
</div>
|
||||
|
||||
{tab === "candidates" && (
|
||||
<PanelState pending={candidates.isPending} error={candidates.error} onRetry={() => candidates.refetch()}>
|
||||
{candList.length === 0 ? (
|
||||
<div className="card">
|
||||
<p style={{ fontSize: 13.5, color: "var(--text-2)" }}>当前没有待审候选。</p>
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", marginTop: 6 }}>
|
||||
候选来自镜树对话与大周天评估——发一段对话后回到这里,或使用下方手动添加。
|
||||
</p>
|
||||
<ManualCandidate />
|
||||
</div>
|
||||
) : (
|
||||
candList.map((c) => (
|
||||
<div className="card" key={c.id} style={{ marginBottom: 14 }}>
|
||||
<div className="card__head">
|
||||
<span className="mem-type">{KIND_LABEL[c.kind] ?? c.kind}</span>
|
||||
<span style={{ fontSize: 11.5, color: "var(--text-3)" }}>
|
||||
来源 · {c.source} · 敏感度 {c.sensitivity}
|
||||
</span>
|
||||
<span style={{ marginLeft: "auto", fontSize: 11.5, color: "var(--text-3)" }}>{c.id.slice(-8)}</span>
|
||||
</div>
|
||||
<p className="cn" style={{ fontSize: 14 }}>{candidateText(c)}</p>
|
||||
<div style={{ display: "flex", gap: 8, marginTop: 12 }}>
|
||||
<button className="btn btn--primary btn--sm" disabled={decide.isPending} onClick={() => decide.mutate({ id: c.id, approve: true })}>
|
||||
归真入忆
|
||||
</button>
|
||||
<button className="btn btn--ghost btn--sm" disabled={decide.isPending} onClick={() => decide.mutate({ id: c.id, approve: false })}>
|
||||
忽略
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</PanelState>
|
||||
)}
|
||||
|
||||
{tab === "memories" && (
|
||||
<PanelState pending={memories.isPending} error={memories.error} onRetry={() => memories.refetch()} empty={memList.length === 0}>
|
||||
<div className="card" style={{ padding: "6px 18px" }}>
|
||||
{memList.map((m: Memory) => (
|
||||
<div className="list-row" key={m.id}>
|
||||
<span className="mem-type" style={{ flex: "none" }}>{KIND_LABEL[m.kind] ?? m.kind}</span>
|
||||
<div className="list-row__body">
|
||||
<div className="list-row__title">{candidateText(m as unknown as MemoryCandidate)}</div>
|
||||
<div className="list-row__sub">{m.source} · {new Date(m.created_at).toLocaleString("zh-CN", { hour12: false })}</div>
|
||||
</div>
|
||||
<span className="badge badge--ok" style={{ fontSize: 11 }}><span className="badge__dot" />已归真</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PanelState>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 手动添加候选(走同一审核流,用于联调验证闭环) */
|
||||
function ManualCandidate() {
|
||||
const [text, setText] = useState("");
|
||||
const [kind, setKind] = useState("preference");
|
||||
const [state, setState] = useState<{ pending: boolean; error: string | null; ok: boolean }>({ pending: false, error: null, ok: false });
|
||||
|
||||
return (
|
||||
<form
|
||||
style={{ display: "flex", gap: 8, marginTop: 14, flexWrap: "wrap" }}
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
if (!text.trim()) return;
|
||||
setState({ pending: true, error: null, ok: false });
|
||||
try {
|
||||
await api("/v1/memories/candidates", {
|
||||
method: "POST",
|
||||
json: { kind, sensitivity: "low", realm: "reality", payload: { text: text.trim() }, source: "manual" },
|
||||
});
|
||||
setText("");
|
||||
setState({ pending: false, error: null, ok: true });
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
setState({ pending: false, error: (err as { message?: string }).message ?? "创建失败", ok: false });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<select className="input" style={{ width: 100 }} value={kind} onChange={(e) => setKind(e.target.value)} aria-label="候选类型">
|
||||
<option value="preference">偏好</option>
|
||||
<option value="fact">事实</option>
|
||||
<option value="event">事件</option>
|
||||
<option value="relationship">关系</option>
|
||||
</select>
|
||||
<input className="input" style={{ flex: 1, minWidth: 200 }} placeholder="手动添加一条候选,如:读书会隔周四 19:30" value={text} onChange={(e) => setText(e.target.value)} aria-label="候选内容" />
|
||||
<button className="btn btn--outline btn--sm" type="submit" disabled={state.pending || !text.trim()}>
|
||||
{state.pending ? "添加中…" : "添加候选"}
|
||||
</button>
|
||||
{state.error && <span style={{ fontSize: 12, color: "var(--err)", alignSelf: "center" }}>{state.error}</span>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api, clearCsrf, type Character, type CursorPage, type ToolCall } from "@/lib/api";
|
||||
import { PanelState, useApproveToolCall, useLogout, useMemoryCandidates, useToolCalls } from "@/lib/queries";
|
||||
|
||||
/* ---------- 镜庭:聚合真实状态 ---------- */
|
||||
export function HomePanel() {
|
||||
const characters = useQuery({
|
||||
queryKey: ["characters"],
|
||||
queryFn: () => api<CursorPage<Character>>("/v1/characters?limit=10"),
|
||||
});
|
||||
const candidates = useMemoryCandidates();
|
||||
const toolCalls = useToolCalls();
|
||||
|
||||
const chars = characters.data?.data ?? [];
|
||||
const candCount = candidates.data?.data.length ?? 0;
|
||||
const calls = toolCalls.data?.data ?? [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="hero">
|
||||
<div>
|
||||
<h1>镜庭</h1>
|
||||
<p style={{ color: "var(--text-2)", fontSize: 13, marginTop: 4 }}>
|
||||
镜庭只聚合状态,各域才是数据事实源
|
||||
</p>
|
||||
</div>
|
||||
<div className="hero__stats">
|
||||
<a className="hero__stat" href="/characters"><b>{chars.length}</b><span>镜生 · 角色</span></a>
|
||||
<a className="hero__stat" href="/memory"><b>{candCount}</b><span>息壤候选 · 待审</span></a>
|
||||
<a className="hero__stat" href="/agent-workspace"><b>{calls.length}</b><span>天工 · 工具调用</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="section">
|
||||
<div className="section__head">
|
||||
<span className="cn">镜生<span className="cn__plain">你的角色</span></span>
|
||||
<a className="more" href="/characters">管理 →</a>
|
||||
</div>
|
||||
<PanelState pending={characters.isPending} error={characters.error} onRetry={() => characters.refetch()} empty={chars.length === 0}>
|
||||
<div className="grid-cards">
|
||||
{chars.map((c) => (
|
||||
<a className="card char-card" key={c.id} href="/characters">
|
||||
<div className="char-card__top">
|
||||
<span className="avatar">{c.name.slice(0, 1)}</span>
|
||||
<div>
|
||||
<div className="char-card__name">{c.name}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>
|
||||
{c.status === "draft" ? "灵胚 · 未启镜" : c.status}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="char-card__line">
|
||||
{c.status === "draft"
|
||||
? "灵胚是未创建完成的角色草稿——可删除,不等于角色死亡。"
|
||||
: "角色运行中。"}
|
||||
</p>
|
||||
<div className="char-card__foot">
|
||||
<span className={`badge badge--${c.status === "draft" ? "warn" : "info"}`}>
|
||||
<span className="badge__dot" />
|
||||
{c.status === "draft" ? "灵胚 · 草稿" : `${c.status}`}
|
||||
</span>
|
||||
<span className="char-card__time">{new Date(c.created_at).toLocaleDateString("zh-CN")}</span>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</PanelState>
|
||||
</section>
|
||||
|
||||
<section className="section card">
|
||||
<div className="card__head">
|
||||
<span className="cn">待你决定<span className="cn__plain">审批与候选</span></span>
|
||||
</div>
|
||||
<div className="list-row">
|
||||
<span className="badge" style={{ flex: "none" }}>息壤候选 ×{candCount}</span>
|
||||
<div className="list-row__body">
|
||||
<div className="list-row__title">{candCount > 0 ? "有候选等待你的归真决定" : "暂无待审候选"}</div>
|
||||
<div className="list-row__sub">归真后才会影响未来上下文</div>
|
||||
</div>
|
||||
<a className="btn btn--outline btn--sm" href="/memory">去审阅</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 形仪:真实资产列表 ---------- */
|
||||
export function AvatarPanel() {
|
||||
const avatars = useQuery({
|
||||
queryKey: ["avatars"],
|
||||
queryFn: () => api<{ data: { id: string; kind?: string; status?: string; [k: string]: unknown }[] }>("/v1/avatars"),
|
||||
});
|
||||
const list = avatars.data?.data ?? [];
|
||||
return (
|
||||
<section className="section">
|
||||
<div className="section__head">
|
||||
<span className="cn">资产库<span className="cn__plain">图像 / 声音 / Live2D / 3D</span></span>
|
||||
</div>
|
||||
<PanelState pending={avatars.isPending} error={avatars.error} onRetry={() => avatars.refetch()} empty={list.length === 0}>
|
||||
<div className="asset-grid">
|
||||
{list.map((a) => (
|
||||
<div className="asset-card" key={a.id}>
|
||||
<div className="asset-thumb" style={{ background: "linear-gradient(150deg,#233b37,#3f7a6e 60%,#2a4d47)" }}>
|
||||
<span className="glyph">{String(a.kind ?? "形").slice(0, 1)}</span>
|
||||
</div>
|
||||
<div className="asset-body">
|
||||
<span className="asset-name mono">{a.id.slice(-10)}</span>
|
||||
<span className="asset-sub">{String(a.kind ?? "avatar")} · {String(a.status ?? "")}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PanelState>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 天工阁:能力与调用 ---------- */
|
||||
export function RunsPanel() {
|
||||
const tools = useQuery({
|
||||
queryKey: ["tools"],
|
||||
queryFn: () => api<{ data: { name: string; description: string; risk_level: string; requires_approval: boolean }[] }>("/v1/tools"),
|
||||
});
|
||||
const calls = useToolCalls();
|
||||
const toolList = tools.data?.data ?? [];
|
||||
const callList = calls.data?.data ?? [];
|
||||
return (
|
||||
<div>
|
||||
<div className="realm-banner" role="note">
|
||||
天工是功能执行者,不冒充角色身份;工具结果只是观察,采用前需验证。
|
||||
</div>
|
||||
<section className="section">
|
||||
<div className="section__head"><span className="cn">工筹<span className="cn__plain">可用工具</span></span></div>
|
||||
<PanelState pending={tools.isPending} error={tools.error} onRetry={() => tools.refetch()} empty={toolList.length === 0}>
|
||||
<div className="card" style={{ padding: "6px 18px" }}>
|
||||
{toolList.map((t) => (
|
||||
<div className="list-row" key={t.name}>
|
||||
<div className="list-row__body">
|
||||
<div className="list-row__title mono">{t.name}</div>
|
||||
<div className="list-row__sub">{t.description}</div>
|
||||
</div>
|
||||
<span className={`badge badge--${t.requires_approval ? "warn" : "ok"}`} style={{ fontSize: 11 }}>
|
||||
<span className="badge__dot" />{t.requires_approval ? "须请命" : "只读"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PanelState>
|
||||
</section>
|
||||
<section className="section">
|
||||
<div className="section__head"><span className="cn">轨迹<span className="cn__plain">工具调用记录</span></span></div>
|
||||
<PanelState pending={calls.isPending} error={calls.error} onRetry={() => calls.refetch()} empty={callList.length === 0}>
|
||||
<div className="card" style={{ padding: "6px 18px" }}>
|
||||
{callList.map((c: ToolCall) => (
|
||||
<div className="list-row" key={String(c.id)}>
|
||||
<div className="list-row__body">
|
||||
<div className="list-row__title mono">{String(c.tool_name ?? c.id.slice(-8))}</div>
|
||||
<div className="list-row__sub">{String(c.status ?? "")}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PanelState>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 神识阁:观测(模型路由选择端点待 WP-05) ---------- */
|
||||
export function GatewayPanel() {
|
||||
const metrics = useQuery({
|
||||
queryKey: ["metrics"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/v1/metrics");
|
||||
if (!res.ok) throw new Error(`metrics ${res.status}`);
|
||||
return res.text();
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div className="realm-banner" role="note">
|
||||
神识是可替换推理能力,不等于角色身份。首个可见输出后不切换 Provider。
|
||||
</div>
|
||||
<section className="section card">
|
||||
<div className="card__head"><span className="cn">运行观测<span className="cn__plain">/v1/metrics</span></span></div>
|
||||
<PanelState pending={metrics.isPending} error={metrics.error} onRetry={() => metrics.refetch()}>
|
||||
<pre className="code" style={{ maxHeight: 260, overflow: "auto" }}>{metrics.data}</pre>
|
||||
</PanelState>
|
||||
</section>
|
||||
<section className="section card">
|
||||
<div className="card__head"><span className="cn">逻辑模型与路由</span></div>
|
||||
<p style={{ fontSize: 13, color: "var(--text-3)" }}>
|
||||
逻辑模型目录与路由继承链的读取端点属 WP-05(模型网关与运行观测),暴露后在此呈现五层继承与 BYOK 管理。
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 司命台:审批与治理(真实数据 + 边界) ---------- */
|
||||
export function GovernancePanel() {
|
||||
const calls = useToolCalls();
|
||||
const approve = useApproveToolCall();
|
||||
const list = calls.data?.data ?? [];
|
||||
const history = useQuery({
|
||||
queryKey: ["tool-calls-history"],
|
||||
queryFn: async () => {
|
||||
const [ok, denied] = await Promise.all([
|
||||
api<{ data: ToolCall[] }>("/v1/tool-calls?status_filter=succeeded"),
|
||||
api<{ data: ToolCall[] }>("/v1/tool-calls?status_filter=denied"),
|
||||
]);
|
||||
return [...(ok.data ?? []), ...(denied.data ?? [])];
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div className="realm-banner" role="note">
|
||||
治理不支配角色或用户数据;隐藏推理、密钥与凭据永不进入诊断与审计展示。
|
||||
</div>
|
||||
<section className="section">
|
||||
<div className="section__head"><span className="cn">请命<span className="cn__plain">等待你确认的工具调用</span></span></div>
|
||||
<PanelState pending={calls.isPending} error={calls.error} onRetry={() => calls.refetch()} empty={list.length === 0}>
|
||||
<div className="card" style={{ padding: "6px 18px" }}>
|
||||
{list.map((c: ToolCall) => {
|
||||
const pending = String(c.status ?? "") === "pending_approval";
|
||||
return (
|
||||
<div className="list-row" key={String(c.id)}>
|
||||
<div className="list-row__body">
|
||||
<div className="list-row__title mono">{String(c.tool_name ?? c.id.slice(-8))}</div>
|
||||
<div className="list-row__sub">
|
||||
{String(JSON.stringify(c.input ?? {}))}{c.output ? ` → ${String(c.output).slice(0, 60)}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
{pending ? (
|
||||
<>
|
||||
<button
|
||||
className="btn btn--cinnabar btn--sm"
|
||||
disabled={approve.isPending}
|
||||
onClick={() => approve.mutate({ id: String(c.id), approve: true })}
|
||||
>
|
||||
允许(朱批)
|
||||
</button>
|
||||
<button
|
||||
className="btn btn--outline btn--sm"
|
||||
disabled={approve.isPending}
|
||||
onClick={() => approve.mutate({ id: String(c.id), approve: false })}
|
||||
>
|
||||
拒绝
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span className={`badge badge--${c.status === "executed" || c.status === "completed" ? "ok" : c.status === "rejected" ? "muted" : "muted"}`} style={{ fontSize: 11 }}>
|
||||
<span className="badge__dot" />{String(c.status)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PanelState>
|
||||
</section>
|
||||
<section className="section">
|
||||
<div className="section__head"><span className="cn">朱批留痕<span className="cn__plain">已处理的调用</span></span></div>
|
||||
<PanelState pending={history.isPending} error={history.error} empty={(history.data ?? []).length === 0}>
|
||||
<div className="card" style={{ padding: "6px 18px" }}>
|
||||
{(history.data ?? []).map((c: ToolCall) => (
|
||||
<div className="list-row" key={String(c.id)}>
|
||||
<div className="list-row__body">
|
||||
<div className="list-row__title mono">{String(c.tool_name)}</div>
|
||||
<div className="list-row__sub">{String(JSON.stringify(c.output ?? c.input ?? {})).slice(0, 90)}</div>
|
||||
</div>
|
||||
<span className={`badge badge--${c.status === "succeeded" ? "ok" : "muted"}`} style={{ fontSize: 11 }}>
|
||||
<span className="badge__dot" />{String(c.status)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PanelState>
|
||||
</section>
|
||||
<section className="section card">
|
||||
<div className="card__head"><span className="cn">诊断授权<span className="cn__plain">双重授权</span></span></div>
|
||||
<p style={{ fontSize: 12.5, color: "var(--text-2)" }}>
|
||||
读取 Run 详细诊断需普通认证之外叠加短期诊断令牌:绑定单个 Run、单次使用、默认 15 分钟。
|
||||
需诊断时从镜树/天工阁的具体 Run 发起(POST /v1/runs/{"{run_id}"}/diagnostic-grants)。
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 设置:登出 + 财务(真实)+ 其余空态 ---------- */
|
||||
export function SettingsPanel() {
|
||||
const logout = useLogout();
|
||||
const finance = useQuery({
|
||||
queryKey: ["finance-summary"],
|
||||
queryFn: () => api<{ totals_minor: Record<string, number>; balanced: boolean }>("/v1/finance/summary"),
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<section className="section card">
|
||||
<div className="card__head"><span className="cn">会话</span></div>
|
||||
<div className="set-row">
|
||||
<div className="set-row__body">
|
||||
<div className="set-row__t">退出当前会话</div>
|
||||
<div className="set-row__d">登出并撤销 Session Cookie(DELETE /v1/auth/sessions/current)</div>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn--outline btn--sm"
|
||||
disabled={logout.isPending}
|
||||
onClick={async () => {
|
||||
await logout.mutateAsync().catch(() => undefined);
|
||||
clearCsrf();
|
||||
window.location.href = "/login";
|
||||
}}
|
||||
>
|
||||
{logout.isPending ? "登出中…" : "登出"}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<section className="section card">
|
||||
<div className="card__head"><span className="cn">结算<span className="cn__plain">财务摘要</span></span></div>
|
||||
<PanelState pending={finance.isPending} error={finance.error}>
|
||||
<div style={{ display: "flex", gap: 24, flexWrap: "wrap" }}>
|
||||
{Object.entries(finance.data?.totals_minor ?? {}).map(([cur, minor]) => (
|
||||
<div key={cur}>
|
||||
<div style={{ fontSize: 11.5, color: "var(--text-3)" }}>{cur.toUpperCase()}</div>
|
||||
<div className="price">{`¥${(minor / 100).toFixed(2)}`}</div>
|
||||
</div>
|
||||
))}
|
||||
<span className="badge badge--ok"><span className="badge__dot" />账平 · {String(finance.data?.balanced ?? false)}</span>
|
||||
</div>
|
||||
</PanelState>
|
||||
</section>
|
||||
<section className="section card">
|
||||
<div className="card__head"><span className="cn">数据权与私箧</span></div>
|
||||
<p style={{ fontSize: 12.5, color: "var(--text-3)" }}>
|
||||
导出、撤权、删除与私箧管理端点属后续工作包(WP-01 私箧 / WP-02 可携导出),暴露后在此接入。
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 器谱:真实空态(设备 API 属 WP-10) ---------- */
|
||||
export function DevicesPanel() {
|
||||
return (
|
||||
<div>
|
||||
<div className="realm-banner" role="note">
|
||||
设备能力同时受系统权限与授契约束;采集有持续指示,可即时撤销。
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="card__head"><span className="cn">形器<span className="cn__plain">登记设备</span></span></div>
|
||||
<p style={{ fontSize: 13.5, color: "var(--text-2)" }}>
|
||||
设备登记与 DeviceGrant 端点属 WP-10(Web、桌面、移动与感知),尚未暴露。
|
||||
</p>
|
||||
<p style={{ fontSize: 12.5, color: "var(--text-3)", marginTop: 6 }}>
|
||||
接入后此处显示:形器列表、授契(闻声 / 观屏 / 问路 · 范围与期限)、通感进行中、直白权限请求。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { PanelState, useInstallPlugin, usePlugins, useTools } from "@/lib/queries";
|
||||
import type { PluginMeta, ToolMeta } from "@/lib/api";
|
||||
|
||||
export function PluginsPanel() {
|
||||
const plugins = usePlugins();
|
||||
const tools = useTools();
|
||||
const install = useInstallPlugin();
|
||||
const [installed, setInstalled] = useState<string[]>([]);
|
||||
const pluginList = plugins.data?.data ?? [];
|
||||
const toolList = tools.data?.data ?? [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="realm-banner" role="note">
|
||||
购买不等于安装,安装不等于启用,启用不等于授予新增权限。安全内核不可插件化。
|
||||
</div>
|
||||
|
||||
<section className="section">
|
||||
<div className="section__head">
|
||||
<span className="cn">百工<span className="cn__plain">可用能力集合 · 能力可发现不等于已授权</span></span>
|
||||
</div>
|
||||
<PanelState pending={tools.isPending} error={tools.error} onRetry={() => tools.refetch()} empty={toolList.length === 0}>
|
||||
<div className="card" style={{ padding: "6px 18px" }}>
|
||||
{toolList.map((t: ToolMeta) => (
|
||||
<div className="list-row" key={t.name}>
|
||||
<div className="list-row__body">
|
||||
<div className="list-row__title mono">{t.name}</div>
|
||||
<div className="list-row__sub">{t.description}</div>
|
||||
</div>
|
||||
<span className={`badge badge--${t.requires_approval ? "warn" : "ok"}`} style={{ fontSize: 11 }}>
|
||||
<span className="badge__dot" />{t.requires_approval ? "须请命" : t.risk_level === "read_only" ? "只读" : t.risk_level}
|
||||
</span>
|
||||
<span className={`badge badge--${t.enabled ? "ok" : "muted"}`} style={{ fontSize: 11 }}>
|
||||
<span className="badge__dot" />{t.enabled ? "已启用" : "未启用"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PanelState>
|
||||
</section>
|
||||
|
||||
<section className="section">
|
||||
<div className="section__head">
|
||||
<span className="cn">插件<span className="cn__plain">注册表</span></span>
|
||||
</div>
|
||||
<PanelState pending={plugins.isPending} error={plugins.error} onRetry={() => plugins.refetch()} empty={pluginList.length === 0}>
|
||||
<div className="card" style={{ padding: "6px 18px" }}>
|
||||
{pluginList.map((p: PluginMeta) => (
|
||||
<div className="list-row" key={p.name}>
|
||||
<div className="list-row__body">
|
||||
<div className="list-row__title mono">{p.name}</div>
|
||||
<div className="list-row__sub">{String(p.description ?? p.kind ?? "plugin")}</div>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn--outline btn--sm"
|
||||
disabled={install.isPending || installed.includes(p.name)}
|
||||
onClick={async () => {
|
||||
await install.mutateAsync(p.name);
|
||||
setInstalled((prev) => [...prev, p.name]);
|
||||
}}
|
||||
>
|
||||
{installed.includes(p.name) ? "已安装" : "安装"}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", marginTop: 8 }}>
|
||||
插件注册表为空时表示本部署尚无已注册插件;安装不自动启用,启用不授予新增权限。
|
||||
</p>
|
||||
</PanelState>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { ADMIN_NAV } from "@/lib/nav";
|
||||
import { readSession } from "@/lib/api";
|
||||
import { Sidebar, useBrandEgg } from "@/components/shell/Sidebar";
|
||||
import { ThemeButton } from "@/lib/theme";
|
||||
import { RequireAuth } from "./RequireAuth";
|
||||
|
||||
/** 管理面外壳:独立侧栏(锚点导航)+ 管理员身份 + 双人授权提示 */
|
||||
export function AdminShell({ children }: { children: React.ReactNode }) {
|
||||
const [folded, setFolded] = useState(false);
|
||||
const [foot, setFoot] = useState<{ char: string; name: string }>({ char: "司", name: "加载中…" });
|
||||
useEffect(() => {
|
||||
const s = readSession();
|
||||
setFoot({ char: "司", name: s.role === "admin" ? `${s.email.split("@")[0] || "dev"} · 管理员` : "普通用户" });
|
||||
}, []);
|
||||
const { onBrandClick, brandState, toast } = useBrandEgg();
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem("hunjing-sidebar");
|
||||
if (saved === "collapsed") setFolded(true);
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={folded ? "app app--fold" : "app"}>
|
||||
{toast && <div className="egg-toast is-on">{toast}</div>}
|
||||
<Sidebar
|
||||
groups={ADMIN_NAV}
|
||||
folded={folded}
|
||||
onToggleFold={() => setFolded((v) => !v)}
|
||||
onBrandClick={onBrandClick}
|
||||
brandState={brandState}
|
||||
brandSub="司命台 · 管理面"
|
||||
foot={foot}
|
||||
below={
|
||||
<div className="admin-back">
|
||||
<a href="/home">← 返回用户界面</a>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<main className="main">
|
||||
<RequireAuth>{children}</RequireAuth>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminThemeButton() {
|
||||
return <ThemeButton />;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { NAV_GROUPS } from "@/lib/nav";
|
||||
import { Sidebar, useBrandEgg } from "./Sidebar";
|
||||
import { RequireAuth } from "./RequireAuth";
|
||||
|
||||
/** 应用外壳:主侧栏(可折叠,localStorage 记忆)+ 主区 */
|
||||
export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const [folded, setFolded] = useState(false);
|
||||
const { onBrandClick, brandState, toast } = useBrandEgg();
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem("hunjing-sidebar");
|
||||
if (saved === "collapsed") setFolded(true);
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleFold = () => {
|
||||
setFolded((prev) => {
|
||||
const next = !prev;
|
||||
try {
|
||||
localStorage.setItem("hunjing-sidebar", next ? "collapsed" : "expanded");
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={folded ? "app app--fold" : "app"}>
|
||||
{toast && <div className="egg-toast is-on">{toast}</div>}
|
||||
<Sidebar
|
||||
groups={NAV_GROUPS}
|
||||
activeHref={pathname}
|
||||
folded={folded}
|
||||
onToggleFold={toggleFold}
|
||||
onBrandClick={onBrandClick}
|
||||
brandState={brandState}
|
||||
/>
|
||||
<main className="main">
|
||||
<RequireAuth>{children}</RequireAuth>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { api, ApiError, type CursorPage } from "@/lib/api";
|
||||
|
||||
/**
|
||||
* 登录守卫:进入应用外壳时校验会话(GET /v1/characters),
|
||||
* 未登录(401/403)跳转登录页。校验期间显示轻量占位,不闪烁内容。
|
||||
*/
|
||||
export function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const check = useQuery({
|
||||
queryKey: ["auth-check"],
|
||||
queryFn: () => api<CursorPage<unknown>>("/v1/characters?limit=1"),
|
||||
retry: false,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
|
||||
if (check.isError) {
|
||||
const status = check.error instanceof ApiError ? check.error.status : 0;
|
||||
if (status === 401 || status === 403) {
|
||||
router.replace("/login");
|
||||
return (
|
||||
<div style={{ display: "grid", placeItems: "center", minHeight: "60vh" }}>
|
||||
<p style={{ fontSize: 14, color: "var(--text-3)" }}>未登入,正在前往登录页…</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div style={{ display: "grid", placeItems: "center", minHeight: "60vh" }}>
|
||||
<p style={{ fontSize: 14, color: "var(--err)" }}>会话校验失败,请刷新重试</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (check.isPending) {
|
||||
return (
|
||||
<div style={{ display: "grid", placeItems: "center", minHeight: "60vh" }}>
|
||||
<p style={{ fontSize: 14, color: "var(--text-3)" }}>正在校验会话…</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronsLeft } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { NavGroup } from "@/lib/nav";
|
||||
import { ThemeButton } from "@/lib/theme";
|
||||
|
||||
/** 镜启彩蛋诗签(承接 main 分支旧版设计) */
|
||||
const EGG_VERSES = [
|
||||
"镜中有镜,心外无心。",
|
||||
"汝来看此镜时,此镜亦照汝。",
|
||||
"一念起,万镜生。",
|
||||
"镜非鉴物,乃鉴心也。",
|
||||
"幻由心生,镜随心转。",
|
||||
"铜镜本无字,照尽往来人。",
|
||||
"且向镜中看,那个是谁?",
|
||||
];
|
||||
|
||||
interface SidebarProps {
|
||||
groups: NavGroup[];
|
||||
activeHref?: string;
|
||||
folded: boolean;
|
||||
onToggleFold: () => void;
|
||||
onBrandClick: (e: React.MouseEvent) => void;
|
||||
brandState: { tick: boolean; pulse: boolean };
|
||||
brandSub?: string;
|
||||
foot?: { char: string; name: string };
|
||||
below?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Sidebar({ groups, activeHref, folded, onToggleFold, onBrandClick, brandState, brandSub = "Hunjing", foot, below }: SidebarProps) {
|
||||
return (
|
||||
<aside className="sidebar">
|
||||
<div
|
||||
className={`brand${brandState.tick ? " tick" : ""}${brandState.pulse ? " egg-pulse" : ""}`}
|
||||
onClick={onBrandClick}
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
aria-label="魂镜 · 单击回目录,五连击触发镜启彩蛋"
|
||||
>
|
||||
<div className="brand__mark" aria-hidden="true" />
|
||||
<div className="brand__text">
|
||||
<div className="brand__name">魂镜</div>
|
||||
<div className="brand__sub">{brandSub}</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="side-nav" aria-label="主导航">
|
||||
{groups.map((group) => (
|
||||
<div className="nav-group" key={group.title}>
|
||||
<div className="nav-group__title">{group.title}</div>
|
||||
{group.items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const active = item.href === activeHref;
|
||||
return (
|
||||
<a
|
||||
key={item.href + item.label}
|
||||
className={active ? "nav-item is-active" : "nav-item"}
|
||||
href={item.href}
|
||||
aria-current={active ? "page" : undefined}
|
||||
>
|
||||
<Icon />
|
||||
<span className="cn">{item.label}</span>
|
||||
<span className="nav-plain">{item.plain}</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
<div className="side-foot">
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn side-fold-btn"
|
||||
onClick={onToggleFold}
|
||||
aria-label={folded ? "展开侧栏" : "收起侧栏"}
|
||||
title="收起 / 展开侧栏(记忆偏好)"
|
||||
>
|
||||
<ChevronsLeft />
|
||||
</button>
|
||||
{foot ? (
|
||||
<>
|
||||
<span className={`avatar avatar--sm avatar--user`}>{foot.char}</span>
|
||||
<span className="side-foot__name">{foot.name}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="avatar avatar--sm avatar--user">舟</span>
|
||||
<span className="side-foot__name">云舟</span>
|
||||
</>
|
||||
)}
|
||||
<ThemeButton />
|
||||
</div>
|
||||
{below}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
/** 品牌彩蛋逻辑:单击延迟回目录;首击 + 4 次连点 = 镜启 */
|
||||
export function useBrandEgg() {
|
||||
const router = useRouter();
|
||||
const seq = useRef(0);
|
||||
const last = useRef(0);
|
||||
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [toast, setToast] = useState<string | null>(null);
|
||||
const [pulse, setPulse] = useState(false);
|
||||
const [tick, setTick] = useState(false);
|
||||
|
||||
useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);
|
||||
|
||||
const onBrandClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (timer.current) clearTimeout(timer.current);
|
||||
const now = Date.now();
|
||||
const fast = now - last.current < 380;
|
||||
last.current = now;
|
||||
setTick(true);
|
||||
setTimeout(() => setTick(false), 300);
|
||||
if (fast) {
|
||||
seq.current += 1;
|
||||
if (seq.current >= 4) {
|
||||
seq.current = 0;
|
||||
setPulse(true);
|
||||
setTimeout(() => setPulse(false), 1400);
|
||||
const verse = EGG_VERSES[Math.floor(Math.random() * EGG_VERSES.length)];
|
||||
setToast(`镜启 · ${verse}`);
|
||||
setTimeout(() => setToast(null), 2500);
|
||||
}
|
||||
return;
|
||||
}
|
||||
seq.current = 0;
|
||||
timer.current = setTimeout(() => router.push("/"), 380);
|
||||
};
|
||||
|
||||
return { onBrandClick, brandState: { tick, pulse }, toast };
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ThemeButton } from "@/lib/theme";
|
||||
|
||||
interface TopbarProps {
|
||||
crumbs: React.ReactNode;
|
||||
extra?: React.ReactNode;
|
||||
}
|
||||
|
||||
/** 顶栏:面包屑 + 原型标记 + 主题切换 */
|
||||
export function Topbar({ crumbs, extra }: TopbarProps) {
|
||||
return (
|
||||
<header className="topbar">
|
||||
<div className="crumbs">{crumbs}</div>
|
||||
{extra}
|
||||
<div className="topbar__spacer" />
|
||||
<span className="prototype-tag">静态原型 · 待联调</span>
|
||||
<ThemeButton />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
/* ---------- 面包屑 ---------- */
|
||||
export function Crumbs({ cn, here }: { cn: string; here?: string }) {
|
||||
return (
|
||||
<>
|
||||
<span className="cn">{cn}</span>
|
||||
{here && (
|
||||
<>
|
||||
<span className="crumbs__sep">/</span>
|
||||
<span className="here">{here}</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 状态徽章:文化状态 · 直白状态 · 稳定值 ---------- */
|
||||
type BadgeTone = "ok" | "info" | "warn" | "err" | "muted";
|
||||
|
||||
export function Badge({
|
||||
tone = "muted",
|
||||
children,
|
||||
stable,
|
||||
style,
|
||||
}: {
|
||||
tone?: BadgeTone;
|
||||
children: ReactNode;
|
||||
stable?: string;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
return (
|
||||
<span className={`badge badge--${tone}`} style={style}>
|
||||
<span className="badge__dot" />
|
||||
{children}
|
||||
{stable && <span className="mono">{stable}</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* API 客户端(开发档联调):
|
||||
* - 同源 /api 前缀经 Next rewrites 代理至后端,Session Cookie 自动携带;
|
||||
* - 写请求带 Hunjing-CSRF-Token(双提交,契约头名);
|
||||
* - 错误统一映射为 ApiError(status + code + message),不匹配错误字符串。
|
||||
*/
|
||||
|
||||
const BASE = "/api";
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
public status: number,
|
||||
public code: string | null,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
}
|
||||
}
|
||||
|
||||
function csrfToken(): string | null {
|
||||
try {
|
||||
return localStorage.getItem("hunjing-csrf");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function storeCsrf(token: string) {
|
||||
try {
|
||||
localStorage.setItem("hunjing-csrf", token);
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
|
||||
export function storeSession(session: { role?: string; email?: string }) {
|
||||
try {
|
||||
localStorage.setItem("hunjing-role", session.role ?? "user");
|
||||
localStorage.setItem("hunjing-email", session.email ?? "");
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
|
||||
export function readSession(): { role: string; email: string } {
|
||||
try {
|
||||
return {
|
||||
role: localStorage.getItem("hunjing-role") ?? "user",
|
||||
email: localStorage.getItem("hunjing-email") ?? "",
|
||||
};
|
||||
} catch {
|
||||
return { role: "user", email: "" };
|
||||
}
|
||||
}
|
||||
|
||||
export function clearCsrf() {
|
||||
try {
|
||||
localStorage.removeItem("hunjing-csrf");
|
||||
localStorage.removeItem("hunjing-role");
|
||||
localStorage.removeItem("hunjing-email");
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
|
||||
export async function api<T>(
|
||||
path: string,
|
||||
init: RequestInit & { json?: unknown } = {},
|
||||
): Promise<T> {
|
||||
const headers = new Headers(init.headers);
|
||||
if (init.json !== undefined) {
|
||||
headers.set("Content-Type", "application/json");
|
||||
}
|
||||
const method = (init.method ?? "GET").toUpperCase();
|
||||
if (method !== "GET" && method !== "HEAD") {
|
||||
const csrf = csrfToken();
|
||||
if (csrf) headers.set("Hunjing-CSRF-Token", csrf);
|
||||
}
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
...init,
|
||||
headers,
|
||||
body: init.json !== undefined ? JSON.stringify(init.json) : init.body,
|
||||
credentials: "same-origin",
|
||||
});
|
||||
if (res.status === 204) return undefined as T;
|
||||
const payload = await res.json().catch(() => null);
|
||||
if (!res.ok) {
|
||||
const detail = (payload as { detail?: string } | null)?.detail;
|
||||
const code =
|
||||
(payload as { error?: { code?: string } } | null)?.error?.code ?? null;
|
||||
throw new ApiError(res.status, code, detail ?? `请求失败(${res.status})`);
|
||||
}
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
/* ---------- 联调用的领域类型(源自运行时 OpenAPI;正式契约冻结后切 openapi-typescript 生成) ---------- */
|
||||
|
||||
export interface SessionInfo {
|
||||
csrf_token: string;
|
||||
user_id: string;
|
||||
role: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface Character {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
origin: string;
|
||||
based_on_real_person: boolean;
|
||||
identity_baseline: Record<string, unknown>;
|
||||
drives: unknown[];
|
||||
current_version_id: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CursorPage<T> {
|
||||
data: T[];
|
||||
next_cursor: string | null;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
/* ---------- 联调领域类型(按后端实测形状;正式契约冻结后切生成类型) ---------- */
|
||||
|
||||
export interface Memory {
|
||||
id: string;
|
||||
character_id: string | null;
|
||||
kind: string;
|
||||
sensitivity: string;
|
||||
realm: string | null;
|
||||
payload: Record<string, unknown>;
|
||||
source: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface MemoryCandidate {
|
||||
id: string;
|
||||
character_id: string | null;
|
||||
kind: string;
|
||||
sensitivity: string;
|
||||
realm: string | null;
|
||||
payload: Record<string, unknown>;
|
||||
source: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ContentBlockLike {
|
||||
type: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
branch_id: string | null;
|
||||
role: string;
|
||||
content: ContentBlockLike[];
|
||||
parser_version: string | null;
|
||||
created_at: string;
|
||||
canonical_scope?: string | null;
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
id: string;
|
||||
title: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export interface Branch {
|
||||
id: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export interface PluginMeta {
|
||||
name: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export interface CommunityContent {
|
||||
id: string;
|
||||
kind: string;
|
||||
title: string | null;
|
||||
body: Record<string, unknown>;
|
||||
visibility: string;
|
||||
status: string;
|
||||
author_domain_id: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Listing {
|
||||
id: string;
|
||||
title: string;
|
||||
price_minor: number | null;
|
||||
currency: string | null;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
id: string;
|
||||
name: string;
|
||||
owner_domain_id: string;
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
id: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AvatarAsset {
|
||||
id: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export interface FinanceSummary {
|
||||
totals_minor: Record<string, number>;
|
||||
balanced: boolean;
|
||||
}
|
||||
|
||||
export interface ToolMeta {
|
||||
name: string;
|
||||
description: string;
|
||||
risk_level: string;
|
||||
requires_approval: boolean;
|
||||
enabled: boolean;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
Activity,
|
||||
BookOpen,
|
||||
Database,
|
||||
FileText,
|
||||
GitBranch,
|
||||
Globe,
|
||||
Home,
|
||||
Layers,
|
||||
Lock,
|
||||
Monitor,
|
||||
Package,
|
||||
Route,
|
||||
Server,
|
||||
Shield,
|
||||
ShoppingBag,
|
||||
SlidersHorizontal,
|
||||
SquareKanban,
|
||||
User,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
|
||||
export interface NavItem {
|
||||
href: string;
|
||||
label: string;
|
||||
plain: string;
|
||||
icon: React.ComponentType;
|
||||
}
|
||||
|
||||
export interface NavGroup {
|
||||
title: string;
|
||||
items: NavItem[];
|
||||
}
|
||||
|
||||
export const NAV_GROUPS: NavGroup[] = [
|
||||
{
|
||||
title: "常用",
|
||||
items: [
|
||||
{ href: "/home", label: "镜庭", plain: "首页", icon: Home },
|
||||
{ href: "/conversation", label: "镜树", plain: "对话", icon: GitBranch },
|
||||
{ href: "/characters", label: "镜生", plain: "角色", icon: User },
|
||||
{ href: "/memory", label: "息壤", plain: "记忆", icon: BookOpen },
|
||||
{ href: "/avatar", label: "形仪", plain: "形象", icon: Layers },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "创造",
|
||||
items: [
|
||||
{ href: "/agent-workspace", label: "天工阁", plain: "任务", icon: SquareKanban },
|
||||
{ href: "/plugins", label: "百工坊", plain: "插件", icon: Package },
|
||||
{ href: "/collaboration", label: "同契台", plain: "协作", icon: Users },
|
||||
{ href: "/model-gateway", label: "神识阁", plain: "模型", icon: Route },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "世界",
|
||||
items: [
|
||||
{ href: "/community", label: "雅集", plain: "社区", icon: Globe },
|
||||
{ href: "/marketplace", label: "云市", plain: "市场", icon: ShoppingBag },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "治理与账户",
|
||||
items: [
|
||||
{ href: "/governance", label: "司命台", plain: "治理", icon: Shield },
|
||||
{ href: "/devices", label: "器谱", plain: "设备", icon: Monitor },
|
||||
{ href: "/settings", label: "设置", plain: "账户", icon: SlidersHorizontal },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const ADMIN_NAV: NavGroup[] = [
|
||||
{
|
||||
title: "运营",
|
||||
items: [
|
||||
{ href: "/admin#queue", label: "治理队列", plain: "", icon: SquareKanban },
|
||||
{ href: "/admin#tickets", label: "支持工单", plain: "3", icon: FileText },
|
||||
{ href: "/admin#users", label: "用户", plain: "", icon: Users },
|
||||
{ href: "/admin#perms", label: "权限管理", plain: "", icon: Shield },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "部署",
|
||||
items: [
|
||||
{ href: "/admin#health", label: "服务健康", plain: "", icon: Server },
|
||||
{ href: "/admin#backup", label: "迁移与备份", plain: "", icon: Database },
|
||||
{ href: "/admin#queue", label: "插件治理", plain: "", icon: Package },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "观测",
|
||||
items: [
|
||||
{ href: "/admin#slo", label: "SLO 与指标", plain: "", icon: Activity },
|
||||
{ href: "/admin#audit", label: "审计日志", plain: "", icon: Shield },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "边界",
|
||||
items: [{ href: "/governance", label: "治理边界说明", plain: "", icon: Lock }],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,373 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { api, type CursorPage } from "./api";
|
||||
|
||||
/* ---------- 通用四态外壳:加载 / 未登入 / 失败 / 内容 ---------- */
|
||||
export function PanelState({
|
||||
pending,
|
||||
error,
|
||||
onRetry,
|
||||
children,
|
||||
empty,
|
||||
}: {
|
||||
pending: boolean;
|
||||
error: unknown;
|
||||
onRetry?: () => void;
|
||||
children: React.ReactNode;
|
||||
empty?: boolean;
|
||||
}) {
|
||||
if (pending) {
|
||||
return <p style={{ fontSize: 13, color: "var(--text-3)", padding: "18px 4px" }}>正在读取…</p>;
|
||||
}
|
||||
if (error) {
|
||||
const status = (error as { status?: number }).status ?? 0;
|
||||
if (status === 401 || status === 403) {
|
||||
return (
|
||||
<div style={{ padding: "20px 4px", textAlign: "center" }}>
|
||||
<p style={{ fontSize: 13.5, color: "var(--text-2)", marginBottom: 12 }}>尚未登入,无法读取你的镜域数据。</p>
|
||||
<a className="btn btn--primary btn--sm" href="/login">去登入</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div style={{ padding: "18px 4px" }}>
|
||||
<p style={{ fontSize: 13, color: "var(--err)", marginBottom: 6 }}>读取失败</p>
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", marginBottom: 10 }}>
|
||||
{(error as { message?: string }).message ?? "请稍后重试"}
|
||||
</p>
|
||||
{onRetry && <button className="btn btn--outline btn--sm" onClick={onRetry}>重试</button>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (empty) {
|
||||
return (
|
||||
<p style={{ fontSize: 13, color: "var(--text-3)", padding: "18px 4px" }}>
|
||||
暂无数据——这里将显示你的真实内容。
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export function useUnauthorized(v: unknown) {
|
||||
return (v as { status?: number })?.status === 401;
|
||||
}
|
||||
|
||||
/* ---------- 会话(镜树) ---------- */
|
||||
export function useCreateConversation() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: { character_id?: string; title: string }) =>
|
||||
api<{ id: string; title: string }>("/v1/conversations", { method: "POST", json: body }),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["conversation"] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useConversation(id: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ["conversation", id],
|
||||
enabled: !!id,
|
||||
queryFn: () => api<Record<string, unknown>>(`/v1/conversations/${id}`),
|
||||
});
|
||||
}
|
||||
|
||||
export function useMessages(id: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ["messages", id],
|
||||
enabled: !!id,
|
||||
queryFn: () => api<CursorPage<import("./api").Message>>(`/v1/conversations/${id}/messages?limit=50`),
|
||||
});
|
||||
}
|
||||
|
||||
export interface SendMessageResult {
|
||||
message: { id: string };
|
||||
run: { id: string } | null;
|
||||
}
|
||||
|
||||
export function useSendMessage(conversationId: string | null) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (text: string) =>
|
||||
api<SendMessageResult>(`/v1/conversations/${conversationId}/messages`, {
|
||||
method: "POST",
|
||||
json: { content: [{ type: "text", text }], start_run: true },
|
||||
headers: { "Idempotency-Key": crypto.randomUUID() },
|
||||
}),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["messages", conversationId] }),
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Run SSE:块级事件消费(重放持久化流,sequence 单调) ---------- */
|
||||
export interface StreamBlock {
|
||||
id: string;
|
||||
type: string;
|
||||
status: "streaming" | "completed" | "failed" | "cancelled";
|
||||
text: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface RunStreamState {
|
||||
runStatus: string;
|
||||
blocks: StreamBlock[];
|
||||
finalStatus: string | null;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
export function useRunEvents(runId: string | null, onDone?: (finalStatus: string | null) => void) {
|
||||
const [state, setState] = useState<RunStreamState>({ runStatus: "connecting", blocks: [], finalStatus: null, done: false });
|
||||
// onDone 经 ref 稳定化:避免内联回调导致事件流反复重连重置
|
||||
const onDoneRef = useRef(onDone);
|
||||
onDoneRef.current = onDone;
|
||||
|
||||
useEffect(() => {
|
||||
if (!runId) return;
|
||||
const ac = new AbortController();
|
||||
setState({ runStatus: "connecting", blocks: [], finalStatus: null, done: false });
|
||||
|
||||
/** 契约事件归约(第 5 节):块级 started/delta/completed/failed + 终态 */
|
||||
const applyEvent = (e: { type: string; data: Record<string, unknown> }) => {
|
||||
setState((prev) => {
|
||||
const next = { ...prev, blocks: [...prev.blocks] };
|
||||
switch (e.type) {
|
||||
case "run.status.changed":
|
||||
next.runStatus = String(e.data.status ?? "");
|
||||
break;
|
||||
case "content.block.started": {
|
||||
const b = (e.data.block ?? {}) as { id?: string; type?: string };
|
||||
next.blocks.push({ id: String(b.id ?? crypto.randomUUID()), type: String(b.type ?? "text"), status: "streaming", text: "" });
|
||||
break;
|
||||
}
|
||||
case "content.block.delta": {
|
||||
const d = e.data as { block_id?: string; delta?: { text?: string } };
|
||||
const idx = next.blocks.findIndex((x) => x.id === d.block_id);
|
||||
const chunk = String(d.delta?.text ?? "");
|
||||
if (idx >= 0) next.blocks[idx] = { ...next.blocks[idx], text: next.blocks[idx].text + chunk };
|
||||
break;
|
||||
}
|
||||
case "content.block.completed": {
|
||||
const b = (e.data.block ?? {}) as { id?: string };
|
||||
const idx = next.blocks.findIndex((x) => x.id === b.id);
|
||||
if (idx >= 0) next.blocks[idx] = { ...next.blocks[idx], status: "completed" };
|
||||
break;
|
||||
}
|
||||
case "content.block.failed": {
|
||||
const b = (e.data.block ?? {}) as { id?: string; type?: string };
|
||||
const err = String(e.data.error ?? "unknown");
|
||||
const idx = next.blocks.findIndex((x) => x.id === b.id);
|
||||
const failed = { id: String(b.id ?? crypto.randomUUID()), type: String(b.type ?? "text"), status: "failed" as const, text: "", error: err };
|
||||
if (idx >= 0) next.blocks[idx] = failed;
|
||||
else next.blocks.push(failed);
|
||||
break;
|
||||
}
|
||||
case "message.completed":
|
||||
next.finalStatus = String(e.data.final_status ?? "");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (
|
||||
e.type === "message.completed" ||
|
||||
(e.type === "run.status.changed" && ["completed", "failed", "cancelled"].includes(next.runStatus))
|
||||
) {
|
||||
next.done = true;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const API_ORIGIN = process.env.NEXT_PUBLIC_API_ORIGIN ?? "http://127.0.0.1:8000";
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", `${API_ORIGIN}/v1/runs/${runId}/events`);
|
||||
xhr.withCredentials = true;
|
||||
xhr.setRequestHeader("Accept", "text/event-stream");
|
||||
let processed = 0;
|
||||
xhr.onprogress = () => {
|
||||
// XHR 增量流式:responseText 为累积文本,按已处理游标切帧
|
||||
const text = xhr.responseText;
|
||||
const fresh = text.slice(processed);
|
||||
processed = text.length;
|
||||
const frames = fresh.split("\n\n");
|
||||
for (const frame of frames) {
|
||||
const line = frame.split("\n").find((l) => l.startsWith("data: "));
|
||||
if (!line) continue;
|
||||
try {
|
||||
applyEvent(JSON.parse(line.slice(6)));
|
||||
} catch {
|
||||
/* 忽略无法解析的帧 */
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.onload = () => setState((prev) => (prev.done ? prev : { ...prev, done: true }));
|
||||
xhr.onerror = () => {
|
||||
setState((prev) => ({ ...prev, done: true, runStatus: prev.runStatus === "connecting" ? "recovering" : prev.runStatus }));
|
||||
};
|
||||
ac.signal.addEventListener("abort", () => xhr.abort());
|
||||
xhr.send();
|
||||
|
||||
const doneCheck = setInterval(() => {
|
||||
setState((prev) => {
|
||||
if (prev.done) {
|
||||
clearInterval(doneCheck);
|
||||
xhr.abort();
|
||||
onDoneRef.current?.(prev.finalStatus);
|
||||
return prev;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}, 250);
|
||||
|
||||
return () => {
|
||||
ac.abort();
|
||||
clearInterval(doneCheck);
|
||||
};
|
||||
}, [runId]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/* ---------- 工具调用:发起与审批 ---------- */
|
||||
export function useCreateToolCall(runId: string | null) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ tool, input }: { tool: string; input: Record<string, unknown> }) =>
|
||||
api(`/v1/runs/${runId}/tool-calls`, {
|
||||
method: "POST",
|
||||
json: { tool_name: tool, input },
|
||||
headers: { "Idempotency-Key": crypto.randomUUID() },
|
||||
}),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["tool-calls"] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useApproveToolCall() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, approve }: { id: string; approve: boolean }) =>
|
||||
api(`/v1/tool-calls/${id}/approvals`, { method: "POST", json: { approve } }),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["tool-calls"] }),
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 记忆(息壤) ---------- */
|
||||
export function useMemories() {
|
||||
return useQuery({
|
||||
queryKey: ["memories"],
|
||||
queryFn: () => api<CursorPage<import("./api").Memory>>("/v1/memories?limit=50"),
|
||||
});
|
||||
}
|
||||
|
||||
export function useMemoryCandidates() {
|
||||
return useQuery({
|
||||
queryKey: ["memory-candidates"],
|
||||
queryFn: () => api<{ data: import("./api").MemoryCandidate[] }>("/v1/memories/candidates"),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDecideCandidate() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, approve }: { id: string; approve: boolean }) =>
|
||||
api(`/v1/memories/candidates/${id}/decisions`, { method: "POST", json: { approve } }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["memory-candidates"] });
|
||||
qc.invalidateQueries({ queryKey: ["memories"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 社区(雅集) ---------- */
|
||||
export function useContents() {
|
||||
return useQuery({
|
||||
queryKey: ["community-contents"],
|
||||
queryFn: () => api<CursorPage<import("./api").CommunityContent>>("/v1/community/contents?limit=20"),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateContent() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: { kind: string; title: string }) =>
|
||||
api("/v1/community/contents", { method: "POST", json: body }),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["community-contents"] }),
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 市场(云市) ---------- */
|
||||
export function useListings() {
|
||||
return useQuery({
|
||||
queryKey: ["listings"],
|
||||
queryFn: () => api<{ data: import("./api").Listing[] }>("/v1/marketplace/listings"),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateListing() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: { title: string; price_minor: number; currency: string }) =>
|
||||
api("/v1/marketplace/listings", { method: "POST", json: body }),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["listings"] }),
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 协作(同契台) ---------- */
|
||||
export function useCreateWorkspace() {
|
||||
return useMutation({
|
||||
mutationFn: (body: { name: string }) =>
|
||||
api<import("./api").Workspace>("/v1/workspaces", { method: "POST", json: body }),
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 插件(百工坊) ---------- */
|
||||
export function usePlugins() {
|
||||
return useQuery({
|
||||
queryKey: ["plugins"],
|
||||
queryFn: () => api<{ data: import("./api").PluginMeta[] }>("/v1/plugins"),
|
||||
});
|
||||
}
|
||||
|
||||
export function useInstallPlugin() {
|
||||
return useMutation({
|
||||
mutationFn: (name: string) =>
|
||||
api(`/v1/plugins/${name}/installations`, { method: "POST", json: {} }),
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 工具与审批(天工阁 / 司命台) ---------- */
|
||||
export function useToolCalls() {
|
||||
return useQuery({
|
||||
queryKey: ["tool-calls"],
|
||||
queryFn: () => api<CursorPage<import("./api").ToolCall>>("/v1/tool-calls?limit=30"),
|
||||
});
|
||||
}
|
||||
|
||||
export function useTools() {
|
||||
return useQuery({
|
||||
queryKey: ["tools"],
|
||||
queryFn: () => api<{ data: import("./api").ToolMeta[] }>("/v1/tools"),
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 形仪 ---------- */
|
||||
export function useAvatars() {
|
||||
return useQuery({
|
||||
queryKey: ["avatars"],
|
||||
queryFn: () => api<{ data: import("./api").AvatarAsset[] }>("/v1/avatars"),
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 财务(设置 / 云市) ---------- */
|
||||
export function useFinanceSummary() {
|
||||
return useQuery({
|
||||
queryKey: ["finance-summary"],
|
||||
queryFn: () => api<import("./api").FinanceSummary>("/v1/finance/summary"),
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 登出 ---------- */
|
||||
export function useLogout() {
|
||||
return useMutation({
|
||||
mutationFn: () => api<void>("/v1/auth/sessions/current", { method: "DELETE" }),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
export function QueryProvider({ children }: { children: React.ReactNode }) {
|
||||
const [client] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: 1, refetchOnWindowFocus: false, staleTime: 15_000 },
|
||||
},
|
||||
}),
|
||||
);
|
||||
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { Moon } from "lucide-react";
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light";
|
||||
|
||||
const ThemeContext = createContext<{ theme: Theme; toggle: () => void }>({
|
||||
theme: "dark",
|
||||
toggle: () => {},
|
||||
});
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, setTheme] = useState<Theme>("dark");
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem("hunjing-theme");
|
||||
if (saved === "light" || saved === "dark") {
|
||||
setTheme(saved);
|
||||
document.documentElement.setAttribute("data-theme", saved);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setTheme((prev) => {
|
||||
const next: Theme = prev === "dark" ? "light" : "dark";
|
||||
document.documentElement.setAttribute("data-theme", next);
|
||||
try {
|
||||
localStorage.setItem("hunjing-theme", next);
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return <ThemeContext.Provider value={{ theme, toggle }}>{children}</ThemeContext.Provider>;
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
return useContext(ThemeContext);
|
||||
}
|
||||
|
||||
export function ThemeButton() {
|
||||
const { theme, toggle } = useTheme();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn"
|
||||
onClick={toggle}
|
||||
aria-label={theme === "dark" ? "切换到月白主题" : "切换到墨色主题"}
|
||||
>
|
||||
<Moon />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import type {} from "react";
|
||||
|
||||
declare module "react" {
|
||||
interface CSSProperties {
|
||||
/** CSS 自定义属性(原型内联样式中大量使用 var(--token)) */
|
||||
[key: `--${string}`]: string | number | undefined;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user