¶Ô±ÈÐÂÎļþ |
| | |
| | | /** @type {import('next').NextConfig} */ |
| | | const nextConfig = { |
| | | async headers() { |
| | | return [ |
| | | { |
| | | // å¹é
ææAPIè·¯ç± |
| | | source: "/:path*", |
| | | headers: [ |
| | | { key: "Access-Control-Allow-Credentials", value: "true" }, |
| | | { key: "Access-Control-Allow-Origin", value: "*" }, |
| | | { key: "Access-Control-Allow-Methods", value: "GET,DELETE,PATCH,POST,PUT" }, |
| | | { key: "Access-Control-Allow-Headers", value: "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Authorization" }, |
| | | ] |
| | | } |
| | | ] |
| | | } |
| | | } |
| | | |
| | | module.exports = nextConfig |
| | |
| | | "react": "^19.0.0", |
| | | "react-dom": "^19.0.0", |
| | | "react-markdown": "^10.1.0", |
| | | "rehype-raw": "^7.0.0", |
| | | "rehype-sanitize": "^6.0.0", |
| | | "remark-gfm": "^4.0.1", |
| | | "styled-components": "^6.1.16", |
| | | "zustand": "^5.0.3" |
| | |
| | | "devDependencies": { |
| | | "@eslint/eslintrc": "^3", |
| | | "@tailwindcss/postcss": "^4", |
| | | "@tailwindcss/typography": "^0.5.16", |
| | | "@types/node": "^20", |
| | | "@types/react": "^19", |
| | | "@types/react-dom": "^19", |
| | |
| | | "tailwindcss": "4.0.16" |
| | | } |
| | | }, |
| | | "node_modules/@tailwindcss/typography": { |
| | | "version": "0.5.16", |
| | | "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", |
| | | "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", |
| | | "dev": true, |
| | | "license": "MIT", |
| | | "dependencies": { |
| | | "lodash.castarray": "^4.4.0", |
| | | "lodash.isplainobject": "^4.0.6", |
| | | "lodash.merge": "^4.6.2", |
| | | "postcss-selector-parser": "6.0.10" |
| | | }, |
| | | "peerDependencies": { |
| | | "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" |
| | | } |
| | | }, |
| | | "node_modules/@tanstack/react-virtual": { |
| | | "version": "3.13.6", |
| | | "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.6.tgz", |
| | |
| | | "postcss-value-parser": "^4.0.2" |
| | | } |
| | | }, |
| | | "node_modules/cssesc": { |
| | | "version": "3.0.0", |
| | | "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", |
| | | "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", |
| | | "dev": true, |
| | | "license": "MIT", |
| | | "bin": { |
| | | "cssesc": "bin/cssesc" |
| | | }, |
| | | "engines": { |
| | | "node": ">=4" |
| | | } |
| | | }, |
| | | "node_modules/csstype": { |
| | | "version": "3.1.3", |
| | | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", |
| | |
| | | }, |
| | | "engines": { |
| | | "node": ">=10.13.0" |
| | | } |
| | | }, |
| | | "node_modules/entities": { |
| | | "version": "6.0.0", |
| | | "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", |
| | | "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", |
| | | "license": "BSD-2-Clause", |
| | | "engines": { |
| | | "node": ">=0.12" |
| | | }, |
| | | "funding": { |
| | | "url": "https://github.com/fb55/entities?sponsor=1" |
| | | } |
| | | }, |
| | | "node_modules/es-abstract": { |
| | |
| | | "node": ">= 0.4" |
| | | } |
| | | }, |
| | | "node_modules/hast-util-from-parse5": { |
| | | "version": "8.0.3", |
| | | "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", |
| | | "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", |
| | | "license": "MIT", |
| | | "dependencies": { |
| | | "@types/hast": "^3.0.0", |
| | | "@types/unist": "^3.0.0", |
| | | "devlop": "^1.0.0", |
| | | "hastscript": "^9.0.0", |
| | | "property-information": "^7.0.0", |
| | | "vfile": "^6.0.0", |
| | | "vfile-location": "^5.0.0", |
| | | "web-namespaces": "^2.0.0" |
| | | }, |
| | | "funding": { |
| | | "type": "opencollective", |
| | | "url": "https://opencollective.com/unified" |
| | | } |
| | | }, |
| | | "node_modules/hast-util-parse-selector": { |
| | | "version": "4.0.0", |
| | | "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", |
| | | "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", |
| | | "license": "MIT", |
| | | "dependencies": { |
| | | "@types/hast": "^3.0.0" |
| | | }, |
| | | "funding": { |
| | | "type": "opencollective", |
| | | "url": "https://opencollective.com/unified" |
| | | } |
| | | }, |
| | | "node_modules/hast-util-raw": { |
| | | "version": "9.1.0", |
| | | "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", |
| | | "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", |
| | | "license": "MIT", |
| | | "dependencies": { |
| | | "@types/hast": "^3.0.0", |
| | | "@types/unist": "^3.0.0", |
| | | "@ungap/structured-clone": "^1.0.0", |
| | | "hast-util-from-parse5": "^8.0.0", |
| | | "hast-util-to-parse5": "^8.0.0", |
| | | "html-void-elements": "^3.0.0", |
| | | "mdast-util-to-hast": "^13.0.0", |
| | | "parse5": "^7.0.0", |
| | | "unist-util-position": "^5.0.0", |
| | | "unist-util-visit": "^5.0.0", |
| | | "vfile": "^6.0.0", |
| | | "web-namespaces": "^2.0.0", |
| | | "zwitch": "^2.0.0" |
| | | }, |
| | | "funding": { |
| | | "type": "opencollective", |
| | | "url": "https://opencollective.com/unified" |
| | | } |
| | | }, |
| | | "node_modules/hast-util-sanitize": { |
| | | "version": "5.0.2", |
| | | "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz", |
| | | "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==", |
| | | "license": "MIT", |
| | | "dependencies": { |
| | | "@types/hast": "^3.0.0", |
| | | "@ungap/structured-clone": "^1.0.0", |
| | | "unist-util-position": "^5.0.0" |
| | | }, |
| | | "funding": { |
| | | "type": "opencollective", |
| | | "url": "https://opencollective.com/unified" |
| | | } |
| | | }, |
| | | "node_modules/hast-util-to-jsx-runtime": { |
| | | "version": "2.3.6", |
| | | "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", |
| | |
| | | "url": "https://opencollective.com/unified" |
| | | } |
| | | }, |
| | | "node_modules/hast-util-to-parse5": { |
| | | "version": "8.0.0", |
| | | "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", |
| | | "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", |
| | | "license": "MIT", |
| | | "dependencies": { |
| | | "@types/hast": "^3.0.0", |
| | | "comma-separated-tokens": "^2.0.0", |
| | | "devlop": "^1.0.0", |
| | | "property-information": "^6.0.0", |
| | | "space-separated-tokens": "^2.0.0", |
| | | "web-namespaces": "^2.0.0", |
| | | "zwitch": "^2.0.0" |
| | | }, |
| | | "funding": { |
| | | "type": "opencollective", |
| | | "url": "https://opencollective.com/unified" |
| | | } |
| | | }, |
| | | "node_modules/hast-util-to-parse5/node_modules/property-information": { |
| | | "version": "6.5.0", |
| | | "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", |
| | | "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", |
| | | "license": "MIT", |
| | | "funding": { |
| | | "type": "github", |
| | | "url": "https://github.com/sponsors/wooorm" |
| | | } |
| | | }, |
| | | "node_modules/hast-util-whitespace": { |
| | | "version": "3.0.0", |
| | | "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", |
| | |
| | | "license": "MIT", |
| | | "dependencies": { |
| | | "@types/hast": "^3.0.0" |
| | | }, |
| | | "funding": { |
| | | "type": "opencollective", |
| | | "url": "https://opencollective.com/unified" |
| | | } |
| | | }, |
| | | "node_modules/hastscript": { |
| | | "version": "9.0.1", |
| | | "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", |
| | | "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", |
| | | "license": "MIT", |
| | | "dependencies": { |
| | | "@types/hast": "^3.0.0", |
| | | "comma-separated-tokens": "^2.0.0", |
| | | "hast-util-parse-selector": "^4.0.0", |
| | | "property-information": "^7.0.0", |
| | | "space-separated-tokens": "^2.0.0" |
| | | }, |
| | | "funding": { |
| | | "type": "opencollective", |
| | |
| | | "funding": { |
| | | "type": "opencollective", |
| | | "url": "https://opencollective.com/unified" |
| | | } |
| | | }, |
| | | "node_modules/html-void-elements": { |
| | | "version": "3.0.0", |
| | | "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", |
| | | "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", |
| | | "license": "MIT", |
| | | "funding": { |
| | | "type": "github", |
| | | "url": "https://github.com/sponsors/wooorm" |
| | | } |
| | | }, |
| | | "node_modules/ignore": { |
| | |
| | | "funding": { |
| | | "url": "https://github.com/sponsors/sindresorhus" |
| | | } |
| | | }, |
| | | "node_modules/lodash.castarray": { |
| | | "version": "4.4.0", |
| | | "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", |
| | | "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", |
| | | "dev": true, |
| | | "license": "MIT" |
| | | }, |
| | | "node_modules/lodash.isplainobject": { |
| | | "version": "4.0.6", |
| | | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", |
| | | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", |
| | | "dev": true, |
| | | "license": "MIT" |
| | | }, |
| | | "node_modules/lodash.merge": { |
| | | "version": "4.6.2", |
| | |
| | | "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", |
| | | "license": "MIT" |
| | | }, |
| | | "node_modules/parse5": { |
| | | "version": "7.3.0", |
| | | "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", |
| | | "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", |
| | | "license": "MIT", |
| | | "dependencies": { |
| | | "entities": "^6.0.0" |
| | | }, |
| | | "funding": { |
| | | "url": "https://github.com/inikulin/parse5?sponsor=1" |
| | | } |
| | | }, |
| | | "node_modules/path-exists": { |
| | | "version": "4.0.0", |
| | | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", |
| | |
| | | }, |
| | | "engines": { |
| | | "node": "^10 || ^12 || >=14" |
| | | } |
| | | }, |
| | | "node_modules/postcss-selector-parser": { |
| | | "version": "6.0.10", |
| | | "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", |
| | | "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", |
| | | "dev": true, |
| | | "license": "MIT", |
| | | "dependencies": { |
| | | "cssesc": "^3.0.0", |
| | | "util-deprecate": "^1.0.2" |
| | | }, |
| | | "engines": { |
| | | "node": ">=4" |
| | | } |
| | | }, |
| | | "node_modules/postcss-value-parser": { |
| | |
| | | }, |
| | | "funding": { |
| | | "url": "https://github.com/sponsors/ljharb" |
| | | } |
| | | }, |
| | | "node_modules/rehype-raw": { |
| | | "version": "7.0.0", |
| | | "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", |
| | | "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", |
| | | "license": "MIT", |
| | | "dependencies": { |
| | | "@types/hast": "^3.0.0", |
| | | "hast-util-raw": "^9.0.0", |
| | | "vfile": "^6.0.0" |
| | | }, |
| | | "funding": { |
| | | "type": "opencollective", |
| | | "url": "https://opencollective.com/unified" |
| | | } |
| | | }, |
| | | "node_modules/rehype-sanitize": { |
| | | "version": "6.0.0", |
| | | "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", |
| | | "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==", |
| | | "license": "MIT", |
| | | "dependencies": { |
| | | "@types/hast": "^3.0.0", |
| | | "hast-util-sanitize": "^5.0.0" |
| | | }, |
| | | "funding": { |
| | | "type": "opencollective", |
| | | "url": "https://opencollective.com/unified" |
| | | } |
| | | }, |
| | | "node_modules/remark-gfm": { |
| | |
| | | "punycode": "^2.1.0" |
| | | } |
| | | }, |
| | | "node_modules/util-deprecate": { |
| | | "version": "1.0.2", |
| | | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", |
| | | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", |
| | | "dev": true, |
| | | "license": "MIT" |
| | | }, |
| | | "node_modules/vfile": { |
| | | "version": "6.0.3", |
| | | "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", |
| | |
| | | "dependencies": { |
| | | "@types/unist": "^3.0.0", |
| | | "vfile-message": "^4.0.0" |
| | | }, |
| | | "funding": { |
| | | "type": "opencollective", |
| | | "url": "https://opencollective.com/unified" |
| | | } |
| | | }, |
| | | "node_modules/vfile-location": { |
| | | "version": "5.0.3", |
| | | "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", |
| | | "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", |
| | | "license": "MIT", |
| | | "dependencies": { |
| | | "@types/unist": "^3.0.0", |
| | | "vfile": "^6.0.0" |
| | | }, |
| | | "funding": { |
| | | "type": "opencollective", |
| | |
| | | "url": "https://opencollective.com/unified" |
| | | } |
| | | }, |
| | | "node_modules/web-namespaces": { |
| | | "version": "2.0.1", |
| | | "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", |
| | | "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", |
| | | "license": "MIT", |
| | | "funding": { |
| | | "type": "github", |
| | | "url": "https://github.com/sponsors/wooorm" |
| | | } |
| | | }, |
| | | "node_modules/which": { |
| | | "version": "2.0.2", |
| | | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", |
| | |
| | | "react": "^19.0.0", |
| | | "react-dom": "^19.0.0", |
| | | "react-markdown": "^10.1.0", |
| | | "rehype-raw": "^7.0.0", |
| | | "rehype-sanitize": "^6.0.0", |
| | | "remark-gfm": "^4.0.1", |
| | | "styled-components": "^6.1.16", |
| | | "zustand": "^5.0.3" |
| | |
| | | "devDependencies": { |
| | | "@eslint/eslintrc": "^3", |
| | | "@tailwindcss/postcss": "^4", |
| | | "@tailwindcss/typography": "^0.5.16", |
| | | "@types/node": "^20", |
| | | "@types/react": "^19", |
| | | "@types/react-dom": "^19", |
| | |
| | | 'use client'; |
| | | |
| | | import { useState, useEffect } from 'react'; |
| | | import { useState, useEffect, useRef, useCallback } from 'react'; |
| | | import Link from 'next/link'; |
| | | import { useRouter } from 'next/navigation'; |
| | | import Image from 'next/image'; |
| | | import ReactMarkdown from 'react-markdown'; |
| | | import remarkGfm from 'remark-gfm'; |
| | | import rehypeRaw from 'rehype-raw'; |
| | | import rehypeSanitize from 'rehype-sanitize'; |
| | | |
| | | interface Message { |
| | | role: 'user' | 'assistant'; |
| | | content: string; |
| | | timestamp: number; |
| | | id: string; |
| | | conversation_id?: string; |
| | | feedback?: 'like' | 'dislike' | null; |
| | | metadata?: { |
| | | usage?: { |
| | | prompt_tokens: number; |
| | | completion_tokens: number; |
| | | total_tokens: number; |
| | | total_price: string; |
| | | }; |
| | | }; |
| | | } |
| | | |
| | | const BASE_URL = 'http://121.43.139.99:7000'; |
| | | |
| | | // é»è®¤ç¨æ·å¤´å |
| | | const DEFAULT_USER_AVATAR = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23999999'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z'/%3E%3C/svg%3E"; |
| | | const AI_AVATAR = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%234F46E5'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 1c4.96 0 9 4.04 9 9s-4.04 9-9 9-9-4.04-9-9 4.04-9 9-9zm0 3.5c-2.48 0-4.5 2.02-4.5 4.5h1.5c0-1.66 1.34-3 3-3s3 1.34 3 3h1.5c0-2.48-2.02-4.5-4.5-4.5zM8 13h2v2H8v-2zm6 0h2v2h-2v-2z'/%3E%3C/svg%3E"; |
| | | |
| | | export default function Page() { |
| | | const router = useRouter(); |
| | | const [token, setToken] = useState<string | null>(null); |
| | | const [apiKey, setApiKey] = useState<string>(''); |
| | | const [showApiKeyInput, setShowApiKeyInput] = useState(false); |
| | | const [message, setMessage] = useState(''); |
| | | const [messages, setMessages] = useState<Message[]>([]); |
| | | const [isStreaming, setIsStreaming] = useState(false); |
| | | const [error, setError] = useState<string | null>(null); |
| | | const [showError, setShowError] = useState(false); |
| | | const [conversationId, setConversationId] = useState<string | null>(null); |
| | | const [isMessageComplete, setIsMessageComplete] = useState(true); |
| | | const [currentMessageId, setCurrentMessageId] = useState<string | null>(null); |
| | | |
| | | // æ·»å ä¸ä¸ªæ°çç¶ææ¥æ§å¶æ¶æ¯çæ¾ç¤º |
| | | const [showMessages, setShowMessages] = useState(false); |
| | | |
| | | const messagesEndRef = useRef<HTMLDivElement>(null); |
| | | const errorTimeoutRef = useRef<any>(null); |
| | | |
| | | useEffect(() => { |
| | | // è·åtoken |
| | | const storedToken = localStorage.getItem('token'); |
| | | setToken(storedToken); |
| | | |
| | | // å¦ææ²¡ætokenï¼ç´æ¥è·³è½¬å°ç»å½é¡µé¢ |
| | | if (!storedToken) { |
| | | router.push('/login'); |
| | | // è·ååå¨çAPI Key |
| | | const storedApiKey = localStorage.getItem('api-key'); |
| | | if (storedApiKey) { |
| | | setApiKey(storedApiKey); |
| | | } else { |
| | | setShowApiKeyInput(true); |
| | | } |
| | | }, []); |
| | | |
| | | // å¦ææ²¡ætokenï¼ä¸æ¸²æä»»ä½å
容 |
| | | if (!token) { |
| | | return null; |
| | | } |
| | | useEffect(() => { |
| | | // æ»å¨å°ææ°æ¶æ¯ |
| | | messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); |
| | | }, [messages]); |
| | | |
| | | // å¤çé误æ¾ç¤ºåèªå¨æ¶å¤± |
| | | const showErrorMessage = useCallback((message: string) => { |
| | | setError(message); |
| | | // å
设置为falseç¡®ä¿å¨ç»è½éæ°è§¦å |
| | | setShowError(false); |
| | | // ä½¿ç¨ requestAnimationFrame ç¡®ä¿ç¶æåå被æ£ç¡®æ¸²æ |
| | | requestAnimationFrame(() => { |
| | | requestAnimationFrame(() => { |
| | | setShowError(true); |
| | | }); |
| | | }); |
| | | |
| | | if (errorTimeoutRef.current) { |
| | | clearTimeout(errorTimeoutRef.current); |
| | | } |
| | | |
| | | errorTimeoutRef.current = setTimeout(() => { |
| | | setShowError(false); |
| | | setTimeout(() => { |
| | | setError(null); |
| | | }, 400); |
| | | }, 3000); |
| | | }, []); |
| | | |
| | | // ç»ä»¶å¸è½½æ¶æ¸
é¤å®æ¶å¨ |
| | | useEffect(() => { |
| | | const cleanup = () => { |
| | | if (errorTimeoutRef.current) { |
| | | clearTimeout(errorTimeoutRef.current); |
| | | } |
| | | }; |
| | | return cleanup; |
| | | }, []); |
| | | |
| | | const handleApiKeySubmit = () => { |
| | | if (apiKey.trim()) { |
| | | localStorage.setItem('api-key', apiKey); |
| | | setShowApiKeyInput(false); |
| | | setError(null); |
| | | } |
| | | }; |
| | | |
| | | const handleSendMessage = async () => { |
| | | if (!message.trim() || !isMessageComplete) return; |
| | | if (!apiKey) { |
| | | showErrorMessage('请å
设置API Key'); |
| | | return; |
| | | } |
| | | |
| | | setIsStreaming(true); |
| | | setIsMessageComplete(false); |
| | | |
| | | // åå»ºæ°æ¶æ¯ |
| | | const userMessage: Message = { |
| | | role: 'user', |
| | | content: message.trim(), |
| | | timestamp: Date.now(), |
| | | id: 'user-' + Date.now().toString() |
| | | }; |
| | | |
| | | const assistantMessage: Message = { |
| | | role: 'assistant', |
| | | content: '', |
| | | timestamp: Date.now(), |
| | | id: 'temp-' + Date.now().toString() |
| | | }; |
| | | |
| | | // 使ç¨å½æ°å¼æ´æ°ç¡®ä¿ç¶ææ´æ°çä¸è´æ§ |
| | | setMessages(prevMessages => { |
| | | const newMessages = [...prevMessages]; |
| | | // 妿å卿ªå®æçå©ææ¶æ¯ï¼å
ç§»é¤å® |
| | | if (!isMessageComplete && currentMessageId) { |
| | | const lastMessage = newMessages[newMessages.length - 1]; |
| | | if (lastMessage && lastMessage.role === 'assistant' && lastMessage.id === currentMessageId) { |
| | | newMessages.pop(); |
| | | } |
| | | } |
| | | return [...newMessages, userMessage, assistantMessage]; |
| | | }); |
| | | |
| | | setCurrentMessageId(assistantMessage.id); |
| | | setMessage(''); |
| | | |
| | | try { |
| | | const response = await fetch(`${BASE_URL}/v1/chat-messages`, { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Authorization': `Bearer ${apiKey}`, |
| | | 'Content-Type': 'application/json', |
| | | }, |
| | | mode: 'cors', |
| | | body: JSON.stringify({ |
| | | inputs: {}, |
| | | query: userMessage.content, |
| | | response_mode: 'streaming', |
| | | conversation_id: conversationId || '', |
| | | user: 'abc-123', |
| | | files: [] |
| | | }) |
| | | }); |
| | | |
| | | if (!response.ok) { |
| | | const errorText = await response.text(); |
| | | let errorMessage; |
| | | try { |
| | | const errorJson = JSON.parse(errorText); |
| | | errorMessage = errorJson.error || `Error: ${response.status}`; |
| | | } catch { |
| | | errorMessage = `Error: ${response.status}`; |
| | | } |
| | | throw new Error(errorMessage); |
| | | } |
| | | |
| | | const reader = response.body?.getReader(); |
| | | if (!reader) throw new Error('No reader available'); |
| | | |
| | | const decoder = new TextDecoder(); |
| | | let buffer = ''; |
| | | |
| | | while (true) { |
| | | const { done, value } = await reader.read(); |
| | | if (done) { |
| | | setIsMessageComplete(true); |
| | | break; |
| | | } |
| | | |
| | | buffer += decoder.decode(value, { stream: true }); |
| | | const lines = buffer.split('\n'); |
| | | buffer = lines.pop() || ''; |
| | | |
| | | for (const line of lines) { |
| | | if (line.trim() === '') continue; |
| | | if (line.startsWith('data: ')) { |
| | | try { |
| | | const data = JSON.parse(line.slice(6)); |
| | | |
| | | // 忽ç¥pingäºä»¶ |
| | | if (data.event === 'ping') continue; |
| | | |
| | | switch (data.event) { |
| | | case 'message': |
| | | setMessages(prev => { |
| | | const newMessages = [...prev]; |
| | | const lastMessage = newMessages[newMessages.length - 1]; |
| | | if (lastMessage?.role === 'assistant' && lastMessage.id === currentMessageId) { |
| | | lastMessage.content = data.answer || lastMessage.content; |
| | | if (data.message_id) { |
| | | lastMessage.id = data.message_id; |
| | | setCurrentMessageId(data.message_id); |
| | | } |
| | | } |
| | | return newMessages; |
| | | }); |
| | | break; |
| | | |
| | | case 'message_end': |
| | | if (data.conversation_id) { |
| | | setConversationId(data.conversation_id); |
| | | setMessages(prev => { |
| | | const newMessages = [...prev]; |
| | | const lastMessage = newMessages[newMessages.length - 1]; |
| | | if (lastMessage?.role === 'assistant' && lastMessage.id === currentMessageId) { |
| | | lastMessage.conversation_id = data.conversation_id; |
| | | if (data.metadata) { |
| | | lastMessage.metadata = data.metadata; |
| | | } |
| | | } |
| | | return newMessages; |
| | | }); |
| | | } |
| | | setIsMessageComplete(true); |
| | | break; |
| | | |
| | | case 'error': |
| | | console.error('Error event received:', data); |
| | | setIsMessageComplete(true); |
| | | throw new Error(data.message || 'åéæ¶æ¯æ¶åºé'); |
| | | } |
| | | } catch (e) { |
| | | console.error('Error parsing SSE data:', e); |
| | | if (!messages[messages.length - 1]?.content) { |
| | | setIsMessageComplete(true); |
| | | throw e; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } catch (err) { |
| | | console.error('Chat error:', err); |
| | | showErrorMessage(err instanceof Error ? err.message : 'åéæ¶æ¯æ¶åºé'); |
| | | |
| | | setMessages(prev => { |
| | | const newMessages = [...prev]; |
| | | const lastMessage = newMessages[newMessages.length - 1]; |
| | | if (lastMessage?.role === 'assistant') { |
| | | lastMessage.content = 'æ±æï¼æ¶æ¯åé失败ï¼è¯·ç¨åéè¯ã'; |
| | | } |
| | | return newMessages; |
| | | }); |
| | | } finally { |
| | | setIsStreaming(false); |
| | | } |
| | | }; |
| | | |
| | | // æ´æ°åéæé®çç¦ç¨ç¶æ |
| | | const isSendDisabled = isStreaming || !message.trim() || !isMessageComplete; |
| | | |
| | | const handleKeyPress = (e: React.KeyboardEvent) => { |
| | | if (e.key === 'Enter' && !e.shiftKey) { |
| | | e.preventDefault(); |
| | | handleSendMessage(); |
| | | } |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | // 页é¢å è½½åå»¶è¿æ¾ç¤ºæ¶æ¯å表ï¼é¿å
éªç |
| | | const timer = setTimeout(() => { |
| | | setShowMessages(true); |
| | | }, 100); |
| | | return () => clearTimeout(timer); |
| | | }, []); |
| | | |
| | | return ( |
| | | <div className="min-h-screen bg-[#0A1033] text-white"> |
| | | {/* é¡¶é¨å¯¼èª */} |
| | | <div className="bg-[#131C41] p-4 border-b border-[#6ADBFF]/20"> |
| | | <div className="max-w-4xl mx-auto flex justify-between items-center"> |
| | | <Link href="/" className="text-[#6ADBFF] hover:underline flex items-center gap-2"> |
| | | <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> |
| | | <path fillRule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" /> |
| | | </svg> |
| | | è¿åé¦é¡µ |
| | | </Link> |
| | | <h1 className="text-xl font-bold">AI婿坹è¯</h1> |
| | | </div> |
| | | </div> |
| | | |
| | | {/* è天åºå */} |
| | | <div className="max-w-4xl mx-auto p-4"> |
| | | <div className="space-y-4 mb-20"> |
| | | <div className="flex justify-start"> |
| | | <div className="max-w-[80%] rounded-lg p-4 bg-[#131C41]"> |
| | | <p>ä½ å¥½ï¼æ¬¢è¿æ¥å°è天室ã</p> |
| | | <div className="min-h-screen bg-gradient-to-b from-gray-50 to-white text-gray-900 flex flex-col"> |
| | | {/* API Key Modal */} |
| | | {showApiKeyInput && ( |
| | | <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> |
| | | <div className="bg-white rounded-2xl p-6 w-[400px] space-y-4 shadow-xl"> |
| | | <div className="flex justify-between items-center"> |
| | | <h2 className="text-xl font-semibold">设置 API Key</h2> |
| | | <button |
| | | onClick={() => setShowApiKeyInput(false)} |
| | | className="text-gray-400 hover:text-gray-600" |
| | | > |
| | | <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> |
| | | <path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" /> |
| | | </svg> |
| | | </button> |
| | | </div> |
| | | <div className="space-y-2"> |
| | | <input |
| | | type="password" |
| | | value={apiKey} |
| | | onChange={(e) => setApiKey(e.target.value)} |
| | | placeholder="请è¾å
¥æ¨ç API Key" |
| | | className="w-full px-4 py-2 rounded-xl border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-100/50" |
| | | /> |
| | | </div> |
| | | <div className="flex justify-end gap-2"> |
| | | <button |
| | | onClick={() => setShowApiKeyInput(false)} |
| | | className="px-4 py-2 text-gray-600 hover:text-gray-900" |
| | | > |
| | | åæ¶ |
| | | </button> |
| | | <button |
| | | onClick={handleApiKeySubmit} |
| | | className="px-4 py-2 bg-blue-500 text-white rounded-xl hover:bg-blue-600" |
| | | > |
| | | ç¡®å® |
| | | </button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | )} |
| | | |
| | | {/* è¾å
¥åºå */} |
| | | <div className="fixed bottom-0 left-0 right-0 bg-[#131C41] border-t border-[#6ADBFF]/20 p-4"> |
| | | <div className="max-w-4xl mx-auto flex gap-4"> |
| | | <input |
| | | type="text" |
| | | placeholder="è¾å
¥æ¶æ¯..." |
| | | className="flex-1 bg-[#1E2B63] rounded-lg px-4 py-2 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#6ADBFF]/50" |
| | | /> |
| | | <button |
| | | className="bg-gradient-to-r from-[#6ADBFF] to-[#5E72EB] text-white px-6 py-2 rounded-lg font-medium hover:opacity-90 transition-opacity" |
| | | > |
| | | åé |
| | | </button> |
| | | {/* é误æç¤º */} |
| | | {error && ( |
| | | <div |
| | | className={`fixed top-20 left-1/2 transform -translate-x-1/2 |
| | | flex items-center gap-2 px-4 py-2 text-sm text-white |
| | | transition-all duration-400 ease-out |
| | | ${showError |
| | | ? 'opacity-100 translate-y-0 scale-100' |
| | | : 'opacity-0 -translate-y-2 scale-95' |
| | | } |
| | | before:content-[''] before:absolute before:inset-0 before:bg-red-500 |
| | | before:rounded-lg before:opacity-90 before:-z-10 |
| | | after:content-[''] after:absolute after:inset-0 after:bg-red-500/50 |
| | | after:blur-md after:rounded-lg after:-z-20`} |
| | | > |
| | | <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"> |
| | | <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" /> |
| | | </svg> |
| | | <span className="relative">{error}</span> |
| | | </div> |
| | | )} |
| | | |
| | | {/* API Key æé® */} |
| | | <button |
| | | onClick={() => setShowApiKeyInput(true)} |
| | | className="fixed top-24 right-6 z-10 w-9 h-9 flex items-center justify-center rounded-full bg-white/80 hover:bg-white shadow-sm border border-gray-100 transition-colors" |
| | | title="设置API Key" |
| | | > |
| | | <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> |
| | | <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" /> |
| | | </svg> |
| | | </button> |
| | | |
| | | {/* è天åºå */} |
| | | <div className="flex-1 flex flex-col h-screen"> |
| | | <div className="flex-1 overflow-y-auto pt-20 pb-32"> |
| | | <div className="max-w-4xl mx-auto px-6"> |
| | | <div className={`space-y-6 ${showMessages ? 'opacity-100' : 'opacity-0'} transition-opacity duration-200`}> |
| | | {messages.map((msg, index) => ( |
| | | <div |
| | | key={msg.id} |
| | | className={`flex items-start gap-4 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`} |
| | | > |
| | | <div className="relative flex-shrink-0"> |
| | | <div className="w-8 h-8 rounded-lg overflow-hidden shadow-inner bg-gray-50"> |
| | | <Image |
| | | src={msg.role === 'assistant' ? "/images/logo.jpg" : DEFAULT_USER_AVATAR} |
| | | alt={msg.role === 'assistant' ? "AI婿" : "ç¨æ·"} |
| | | width={32} |
| | | height={32} |
| | | className="w-full h-full object-cover" |
| | | /> |
| | | </div> |
| | | {msg.role === 'assistant' && ( |
| | | <div className="absolute -bottom-1 -right-1 w-3 h-3 bg-green-400 rounded-full border-2 border-white"></div> |
| | | )} |
| | | </div> |
| | | <div className={`flex-1 min-w-0 ${msg.role === 'user' ? 'text-right' : ''}`}> |
| | | <div className={`${ |
| | | msg.role === 'assistant' |
| | | ? 'bg-white rounded-xl shadow-sm border border-gray-100' |
| | | : 'bg-[#E8F4FF] rounded-xl' |
| | | } inline-block max-w-[85%] relative min-h-[42px]`}> |
| | | {msg.role === 'assistant' && !isMessageComplete && currentMessageId === msg.id && ( |
| | | <div className="absolute left-0 top-0 w-full h-full flex items-center justify-center"> |
| | | <div className="w-5 h-5 border-2 border-blue-500/30 border-t-blue-500/80 rounded-full animate-spin"></div> |
| | | </div> |
| | | )} |
| | | <div className={`flex items-center px-4 min-h-[42px] ${ |
| | | msg.role === 'assistant' && !isMessageComplete && currentMessageId === msg.id ? 'invisible' : '' |
| | | }`}> |
| | | <div className="prose prose-sm max-w-none text-[14px] leading-[1.3] |
| | | prose-p:my-0 prose-p:leading-[1.3] |
| | | prose-headings:font-medium prose-headings:text-gray-800 |
| | | prose-h1:text-lg prose-h1:my-2 |
| | | prose-h2:text-base prose-h2:my-2 |
| | | prose-h3:text-base prose-h3:my-1.5 |
| | | prose-ul:my-1.5 prose-ul:pl-4 prose-li:my-0.5 |
| | | prose-ol:my-1.5 prose-ol:pl-4 |
| | | prose-code:px-1 prose-code:py-0.5 prose-code:bg-gray-100 prose-code:rounded prose-code:text-gray-800 prose-code:before:content-[''] prose-code:after:content-[''] |
| | | prose-pre:my-2 prose-pre:p-2.5 prose-pre:bg-gray-800 prose-pre:rounded-lg |
| | | prose-a:text-blue-500 prose-a:no-underline hover:prose-a:underline |
| | | prose-blockquote:my-2 prose-blockquote:pl-3 prose-blockquote:border-l-4 prose-blockquote:border-gray-200 |
| | | prose-strong:font-medium prose-strong:text-gray-800 |
| | | prose-table:my-2 prose-tr:border-gray-200 prose-td:py-1 prose-td:px-2"> |
| | | {msg.content.split('\n').map((line, i) => ( |
| | | <div key={i} className="my-0"> |
| | | {line.trim() && ( |
| | | <ReactMarkdown |
| | | remarkPlugins={[remarkGfm]} |
| | | rehypePlugins={[rehypeRaw, rehypeSanitize]} |
| | | > |
| | | {line.trim()} |
| | | </ReactMarkdown> |
| | | )} |
| | | </div> |
| | | ))} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div className="mt-0.5 text-xs text-gray-400"> |
| | | {msg.role === 'user' && new Date(msg.timestamp).toLocaleTimeString()} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | ))} |
| | | <div ref={messagesEndRef} /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | {/* è¾å
¥åºå */} |
| | | <div className="fixed bottom-0 left-0 right-0 bg-gradient-to-t from-white via-white to-white/95 pt-4 pb-6"> |
| | | <div className="max-w-4xl mx-auto px-6"> |
| | | <div className="relative"> |
| | | <div className="absolute -top-6 left-1/2 -translate-x-1/2 w-48 h-[1px] bg-gradient-to-r from-transparent via-gray-200 to-transparent"></div> |
| | | |
| | | <div className="flex gap-4 items-start"> |
| | | <div className="flex-1 relative"> |
| | | <textarea |
| | | value={message} |
| | | onChange={(e) => setMessage(e.target.value)} |
| | | onKeyDown={handleKeyPress} |
| | | placeholder="è¾å
¥æ¶æ¯..." |
| | | disabled={isStreaming} |
| | | className="w-full resize-none rounded-xl border-0 bg-gray-50 px-4 py-3 text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-100/50 focus:bg-white min-h-[48px] max-h-32 shadow-inner transition-all duration-300 ease-in-out hover:bg-gray-100/70 disabled:opacity-50 disabled:cursor-not-allowed" |
| | | style={{ height: '48px' }} |
| | | /> |
| | | <div className="absolute right-4 bottom-2 text-xs text-gray-400 bg-gray-50 px-2"> |
| | | æEnteråéï¼Shift+Enteræ¢è¡ |
| | | </div> |
| | | </div> |
| | | <button |
| | | onClick={handleSendMessage} |
| | | disabled={isSendDisabled} |
| | | className="h-12 px-5 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-medium rounded-xl transition-all duration-300 flex items-center gap-2 shadow-lg shadow-blue-500/20 hover:shadow-blue-500/30 hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none" |
| | | > |
| | | <span>{isStreaming ? 'åå¤ä¸...' : (isMessageComplete ? 'åé' : 'å¤çä¸...')}</span> |
| | | <svg xmlns="http://www.w3.org/2000/svg" className={`h-4 w-4 transform rotate-45 ${isStreaming ? 'animate-pulse' : ''}`} viewBox="0 0 20 20" fill="currentColor"> |
| | | <path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" /> |
| | | </svg> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | ); |
| | | } |
| | | |
| | |
| | | const isHomePage = pathname === '/'; |
| | | const isAIScenePage = pathname === '/ai-scene'; |
| | | const isAISceneChatPage = pathname.startsWith('/ai-scene/chat'); |
| | | const isChatPage = pathname === '/chat'; // æ·»å è天页é¢å¤æ |
| | | |
| | | // 设置客æ·ç«¯ç¶æ |
| | | useEffect(() => { |
| | |
| | | <main className={`flex-1 ${isHomePage || isAIScenePage ? '' : 'bg-gradient-to-b from-[var(--ai-surface)] to-white'} pt-0 mt-0`}> |
| | | {children} |
| | | </main> |
| | | {!isLoginPage && !isRegisterPage && !isAISceneChatPage && ( |
| | | {!isLoginPage && !isRegisterPage && !isAISceneChatPage && !isChatPage && ( |
| | | <footer className="relative z-20 bg-gradient-to-br from-[#0A1033] via-[#1E2B63] to-[#131C41] text-white py-10 overflow-hidden"> |
| | | {/* ç§ææå¨æèæ¯å
ç´ */} |
| | | <div className="absolute inset-0 overflow-hidden pointer-events-none"> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | /** @type {import('tailwindcss').Config} */ |
| | | module.exports = { |
| | | content: [ |
| | | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', |
| | | './src/components/**/*.{js,ts,jsx,tsx,mdx}', |
| | | './src/app/**/*.{js,ts,jsx,tsx,mdx}', |
| | | ], |
| | | theme: { |
| | | extend: { |
| | | backgroundImage: { |
| | | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', |
| | | 'gradient-conic': |
| | | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', |
| | | }, |
| | | }, |
| | | }, |
| | | plugins: [ |
| | | require('@tailwindcss/typography'), |
| | | ], |
| | | } |