From 6379adfd761b0a6cddd03d42dd96f1a619cda2c0 Mon Sep 17 00:00:00 2001
From: hongjli <3117313295@qq.com>
Date: 星期四, 24 四月 2025 16:48:42 +0800
Subject: [PATCH] 聊天页面

---
 tailwind.config.js                            |   20 +
 package-lock.json                             |  273 +++++++++++++++++++
 package.json                                  |    3 
 next.config.js                                |   19 +
 src/components/layout/ClientLayoutContent.tsx |    3 
 src/app/chat/page.tsx                         |  510 +++++++++++++++++++++++++++++++++---
 6 files changed, 780 insertions(+), 48 deletions(-)

diff --git a/next.config.js b/next.config.js
new file mode 100644
index 0000000..5977389
--- /dev/null
+++ b/next.config.js
@@ -0,0 +1,19 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+  async headers() {
+    return [
+      {
+        // 鍖归厤鎵�鏈堿PI璺敱
+        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 
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 947ab44..2340ca5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,8 @@
         "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"
@@ -23,6 +25,7 @@
       "devDependencies": {
         "@eslint/eslintrc": "^3",
         "@tailwindcss/postcss": "^4",
+        "@tailwindcss/typography": "^0.5.16",
         "@types/node": "^20",
         "@types/react": "^19",
         "@types/react-dom": "^19",
@@ -1304,6 +1307,22 @@
         "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",
@@ -2497,6 +2516,19 @@
         "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",
@@ -2724,6 +2756,18 @@
       },
       "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": {
@@ -3822,6 +3866,79 @@
         "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",
@@ -3849,6 +3966,35 @@
         "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",
@@ -3856,6 +4002,23 @@
       "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",
@@ -3879,6 +4042,16 @@
       "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": {
@@ -4800,6 +4973,20 @@
       "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",
@@ -6120,6 +6307,18 @@
       "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",
@@ -6203,6 +6402,20 @@
       },
       "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": {
@@ -6376,6 +6589,35 @@
       },
       "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": {
@@ -7501,6 +7743,13 @@
         "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",
@@ -7509,6 +7758,20 @@
       "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",
@@ -7529,6 +7792,16 @@
         "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",
diff --git a/package.json b/package.json
index fa31aae..131c9d7 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,8 @@
     "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"
@@ -24,6 +26,7 @@
   "devDependencies": {
     "@eslint/eslintrc": "^3",
     "@tailwindcss/postcss": "^4",
+    "@tailwindcss/typography": "^0.5.16",
     "@types/node": "^20",
     "@types/react": "^19",
     "@types/react-dom": "^19",
diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx
index 415bf60..21ff564 100644
--- a/src/app/chat/page.tsx
+++ b/src/app/chat/page.tsx
@@ -1,70 +1,486 @@
 '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');
+    // 鑾峰彇瀛樺偍鐨凙PI 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 纭繚鐘舵�佸彉鍖栬姝g‘娓叉煋
+    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">
+                    鎸塃nter鍙戦�侊紝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>
   );
 }
+
diff --git a/src/components/layout/ClientLayoutContent.tsx b/src/components/layout/ClientLayoutContent.tsx
index bf997c2..64b02f9 100644
--- a/src/components/layout/ClientLayoutContent.tsx
+++ b/src/components/layout/ClientLayoutContent.tsx
@@ -22,6 +22,7 @@
   const isHomePage = pathname === '/';
   const isAIScenePage = pathname === '/ai-scene';
   const isAISceneChatPage = pathname.startsWith('/ai-scene/chat');
+  const isChatPage = pathname === '/chat'; // 娣诲姞鑱婂ぉ椤甸潰鍒ゆ柇
 
   // 璁剧疆瀹㈡埛绔姸鎬�
   useEffect(() => {
@@ -98,7 +99,7 @@
         <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">
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..b19d36d
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,20 @@
+/** @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'),
+  ],
+} 
\ No newline at end of file

--
Gitblit v1.9.3