Browse Source

第一版大模型ui

sir lin 2 months ago
parent
commit
776262cc25

+ 4 - 2
.env.development

@@ -6,15 +6,17 @@ VITE_APP_ENV = 'development'
 
 # 开发环境
 VITE_APP_BASE_API = '/dev-api'
+# 开发环境websocket
+VITE_APP_WEB_SOCKET_API = 'http://172.16.196.241:8080/dev-api/ws'
 
 # 应用访问路径 例如使用前缀 /admin/
 VITE_APP_CONTEXT_PATH = '/'
 
 # 监控地址
-VITE_APP_MONITOR_ADMIN = 'http://localhost:9090/admin/applications'
+VITE_APP_MONITOR_ADMIN = 'http://172.16.196.241:9090/admin/applications'
 
 # SnailJob 控制台地址
-VITE_APP_SNAILJOB_ADMIN = 'http://localhost:8800/snail-job'
+VITE_APP_SNAILJOB_ADMIN = 'http://172.16.196.241:8800/snail-job'
 
 VITE_APP_PORT = 80
 

+ 2 - 0
.env.production

@@ -15,6 +15,8 @@ VITE_APP_SNAILJOB_ADMIN = '/snail-job'
 
 # 生产环境
 VITE_APP_BASE_API = '/prod-api'
+# 生产环境websocket
+VITE_APP_WEB_SOCKET_API = 'ws://172.16.196.241:8080/ws'
 
 # 是否在打包时开启压缩,支持 gzip 和 brotli
 VITE_BUILD_COMPRESS = gzip

+ 1 - 1
README.md

@@ -21,7 +21,7 @@ npm run dev
 # 构建生产环境
 npm run build:prod
 
-# 前端访问地址 http://localhost:80
+# 前端访问地址 http://172.16.196.241:80
 ```
 
 ## 本框架与RuoYi的业务差异

+ 5 - 0
package.json

@@ -35,6 +35,10 @@
     "image-conversion": "2.1.1",
     "js-cookie": "3.0.5",
     "jsencrypt": "3.3.2",
+    "katex": "^0.16.22",
+    "markdown-it": "^14.1.0",
+    "markdown-it-link-attributes": "^4.0.1",
+    "markdown-it-texmath": "^1.0.0",
     "nprogress": "0.2.0",
     "pinia": "2.2.6",
     "screenfull": "6.0.2",
@@ -65,6 +69,7 @@
     "eslint-plugin-prettier": "5.2.3",
     "eslint-plugin-vue": "9.32.0",
     "globals": "16.0.0",
+    "naive-ui": "^2.41.0",
     "prettier": "3.5.2",
     "sass": "1.84.0",
     "typescript": "~5.7.3",

+ 73 - 0
src/api/ai/app/index.ts

@@ -0,0 +1,73 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { AiAppVO, AiAppForm, AiAppQuery } from '@/api/app/types';
+
+/**
+ * 查询AI应用列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listAiApp = (query?: AiAppQuery): AxiosPromise<AiAppVO[]> => {
+  return request({
+    url: '/aiApp/list',
+    method: 'get',
+    params: query
+  });
+};
+
+
+
+/**
+ * 查询AI应用列表
+ * @param query
+ * @returns {*}
+ */
+
+
+
+/**
+ * 查询AI应用详细
+ * @param id
+ */
+export const getAiApp = (id: string | number): AxiosPromise<AiAppVO> => {
+  return request({
+    url: '/aiApp/getById/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增AI应用
+ * @param data
+ */
+export const addAiApp = (data: AiAppForm) => {
+  return request({
+    url: '/aiApp',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改AI应用
+ * @param data
+ */
+export const updateAiApp = (data: AiAppForm) => {
+  return request({
+    url: '/aiApp/update',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 删除AI应用
+ * @param id
+ */
+export const delAiApp = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/aiApp/' + id,
+    method: 'delete'
+  });
+};

+ 130 - 0
src/api/ai/app/types.ts

@@ -0,0 +1,130 @@
+export interface AiAppVO {
+  /**
+   * 主键ID
+   */
+  id: string | number;
+
+  /**
+   * 应用名称
+   */
+  name: string;
+
+  /**
+   * SN
+   */
+  sn: string;
+
+  /**
+   * 简要信息
+   */
+  briefInfo: string;
+
+  /**
+   * 应用类型
+   */
+  appType: string;
+
+  /**
+   * 是否是内置app
+   */
+  isBuildIn: number;
+
+  /**
+   * 是否启用
+   */
+  enable: number;
+
+}
+
+export interface AiAppForm extends BaseEntity {
+  /**
+   * 主键ID
+   */
+  id?: string | number;
+
+  /**
+   * 应用名称
+   */
+  name?: string;
+
+  /**
+   * SN
+   */
+  sn?: string;
+
+  /**
+   * 简要信息
+   */
+  briefInfo?: string;
+
+  /**
+   * 应用类型
+   */
+  appType?: string;
+
+  /**
+   * 是否是内置app
+   */
+  isBuildIn?: number;
+
+  /**
+   * 是否启用
+   */
+  enable?: number;
+
+}
+
+export interface AiAppQuery extends PageQuery {
+
+  /**
+   * 应用名称
+   */
+  name?: string;
+
+  /**
+   * SN
+   */
+  sn?: string;
+
+  /**
+   * 简要信息
+   */
+  briefInfo?: string;
+
+  /**
+   * 应用类型
+   */
+  appType?: string;
+
+  /**
+   * 是否是内置app
+   */
+  isBuildIn?: number;
+
+  /**
+   * 是否启用
+   */
+  enable?: number;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+
+  /**
+   * 排序字段
+   */
+  orderByColumn: string;
+  /**
+   * 排序方式
+   */
+  isAsc: string;
+
+  /**
+   * 分页页数
+   */
+  page: number;
+}
+
+
+

+ 97 - 0
src/api/ai/session/index.ts

@@ -0,0 +1,97 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { AiAppVO, AiAppForm, AiAppQuery } from '@/api/ai/app/types';
+
+/**
+ * 聊天记录清除
+ * @param activeChatId
+ * @returns {*}
+ */
+
+export const deleteBySessionId = (activeChatId: any): AxiosPromise<any> => {
+  return request({
+    url: '/aiAppMessage/deleteBySessionId/'+activeChatId,
+    method: 'get',
+  });
+};
+/**
+ * 删除消息
+ * @param id
+ */
+export const delMsgById = (id: any): AxiosPromise<any> => {
+  return request({
+    url: '/aiAppMessage/delMsgById/'+id,
+    method: 'get',
+  });
+};
+
+export const getListByPage = (params: any): AxiosPromise<any> => {
+  return request({
+    url: '/aiAppMessage/getListByPage',
+    method: 'get',
+    params:params
+  });
+};
+
+/**
+ * 获取app应用
+ * @param appSn
+ */
+export const getBySn = (appSn: any): AxiosPromise<any> => {
+  return request({
+    url: '/aiApp/getBySn/'+appSn,
+    method: 'get',
+  });
+};
+
+
+/**
+ * 获取session
+ * @param appSn
+ */
+export const getByIdSession = (id: any): AxiosPromise<any> => {
+  return request({
+    url: '/aiAppSession/getById/'+id,
+    method: 'get',
+  });
+};
+/**
+ * 删除session
+ * @param appSn
+ */
+export const deleteSession = (id: any): AxiosPromise<any> => {
+  return request({
+    url: '/aiAppSession/delete/'+id,
+    method: 'get',
+  });
+};
+/**
+ * 更新session
+ * @param data
+ */
+export const update = (data: any): AxiosPromise<any> => {
+  return request({
+    url: '/aiAppSession/update',
+    method: 'post',
+    data:data
+  });
+};
+/**
+ * 新建session
+ * @param data
+ */
+export const add = (data: any): AxiosPromise<any> => {
+  return request({
+    url: '/aiAppSession/add',
+    method: 'post',
+    data:data
+  });
+};
+
+export const getListSessionByPage = (params: any): AxiosPromise<any> => {
+  return request({
+    url: '/aiAppSession/getListByPage',
+    method: 'get',
+    params:params
+  });
+};

+ 87 - 0
src/components/jb-ai-chat/README.md

@@ -0,0 +1,87 @@
+# JBolt AI 聊天组件文档
+## 主要组件和功能说明:
+
+1. 核心组件 AiChat:
+
+    * 主要的聊天应用控制器
+    * 管理WebSocket连接和消息收发
+    * 处理聊天状态(等待、接收中、就绪、断开)
+    * 集成和管理其他子组件
+
+
+2. 基础组件 BaseElement:
+
+    * 所有组件的基类
+    * 提供组件间数据注入和通信机制
+    * 实现组件树的依赖传递
+
+
+3. 消息面板 ChatMessagePanel:
+
+    * 管理消息列表的显示
+    * 处理消息的添加、删除和滚动
+    * 支持历史消息加载
+    * 实现消息的流式显示
+
+
+4. 操作面板 ChatOptPanel:
+
+    * 提供消息输入框
+    * 实现发送按钮和设置按钮
+    * 处理文件上传功能
+    * 支持消息字数统计
+
+
+5. 消息组件 ChatMessage:
+
+    * 显示单条消息内容
+    * 支持Markdown渲染
+    * 提供复制和删除功能
+    * 显示消息发送时间和状态
+
+
+6. 参考资料组件 ReferenceItem:
+
+    * 显示引用的资料内容
+    * 支持不同类型的引用(文本、网站、文档)
+    * 提供可展开/收起的内容展示
+
+
+7.提示组件:
+
+    * ChatTip: 显示聊天状态提示
+    * MsgTip: 显示消息相关的提示信息
+
+
+
+## 关键功能特性:
+
+1. WebSocket通信:
+
+    * 支持实时消息收发
+    * 自动重连机制
+    * 支持消息标识和追踪
+
+
+2. 消息处理:
+
+    * 支持流式响应
+    * Markdown格式解析
+    * 引用资料展示
+    * 消息复制和删除
+
+
+3. 用户界面:
+
+    * 响应式设计
+    * 自适应高度的输入框
+    * 平滑滚动效果
+    * 优雅的加载状态显示
+
+
+4. 数据管理:
+
+    * 历史消息加载
+    * 会话状态管理
+    * 全局参数配置
+    * 错误处理机制

+ 2108 - 0
src/components/jb-ai-chat/base.css

@@ -0,0 +1,2108 @@
+/* -----------    基础 ---------------*/
+.hidden {
+	display: none;
+}
+
+.fx {
+	display: flex;
+	flex-direction: row;
+}
+
+.fx-col {
+	display: flex;
+	flex-direction: column;
+}
+
+.fx-1 {
+	flex: 1 1 auto;
+	overflow: auto;
+}
+
+.fx-2 {
+	flex: 2 2 auto;
+	overflow: auto;
+}
+
+.fx-3 {
+	flex: 3 3 auto;
+	overflow: auto;
+}
+
+.noshrink {
+	flex-shrink: 0;
+}
+
+/**主轴排列**/
+.fx-m_start {
+	justify-content: flex-start;
+}
+
+.fx-m_end {
+	justify-content: flex-end;
+}
+
+.fx-m_center {
+	justify-content: center;
+}
+
+.fx-m_between {
+	justify-content: space-between;
+}
+
+.fx-m_around {
+	justify-content: space-around;
+}
+
+/**交叉轴排列**/
+.fx-c_start {
+	align-items: flex-start;
+}
+
+.fx-c_end {
+	align-items: flex-end;
+}
+
+.fx-c_center {
+	align-items: center;
+}
+
+.fx-c_baseline {
+	align-items: baseline;
+}
+
+.fx-c_stretch {
+	align-items: stretch;
+}
+
+.fx-center {
+	justify-content: center;
+	align-items: center;
+}
+
+.fx-wrap {
+	flex-wrap: wrap;
+}
+
+.fx-w_start {
+	align-content: flex-start;
+}
+
+.fx-w_end {
+	align-content: flex-end;
+}
+
+.fx-w_center {
+	align-content: center;
+}
+
+.fx-w_between {
+	align-content: space-between;
+}
+
+.fx-w_around {
+	align-content: space-around;
+}
+
+.fx-w_stretch {
+	align-content: stretch;
+}
+
+/******* grid 网格布局相关 ***********/
+.grid {
+	display: grid;
+}
+
+.col-2 {
+	grid-template-columns: repeat(2, 1fr);
+}
+
+.col-3 {
+	grid-template-columns: repeat(3, 1fr);
+}
+
+.col-4 {
+	grid-template-columns: repeat(4, 1fr);
+}
+
+.col-5 {
+	grid-template-columns: repeat(5, 1fr);
+}
+
+.col-6 {
+	grid-template-columns: repeat(6, 1fr);
+}
+
+.col-7 {
+	grid-template-columns: repeat(7, 1fr);
+}
+
+.col-8 {
+	grid-template-columns: repeat(8, 1fr);
+}
+
+/*水平间距*/
+.hgap-5 {
+	grid-column-gap: 5px;
+}
+
+.hgap-10 {
+	grid-column-gap: 10px;
+}
+
+.hgap-15 {
+	grid-column-gap: 15px;
+}
+
+.hgap-20 {
+	grid-column-gap: 20px;
+}
+
+.hgap-25 {
+	grid-column-gap: 25px;
+}
+
+.hgap-30 {
+	grid-column-gap: 30px;
+}
+
+.hgap-35 {
+	grid-column-gap: 35px;
+}
+
+.hgap-40 {
+	grid-column-gap: 40px;
+}
+
+.hgap-45 {
+	grid-column-gap: 45px;
+}
+
+.hgap-50 {
+	grid-column-gap: 50px;
+}
+
+.grid-h_start {
+	justify-items: start;
+}
+
+.grid-h_center {
+	justify-items: center;
+}
+
+.grid-h_end {
+	justify-items: end;
+}
+
+/* -----------    ChatMessage样式 ------------------------*/
+.md-assistant {
+	color-scheme: light;
+	-ms-text-size-adjust: 100%;
+	-webkit-text-size-adjust: 100%;
+	margin: 0;
+	color: rgb(51, 65, 85);
+	font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
+	font-size: 16px;
+	line-height: 1.5;
+	word-wrap: break-word;
+	white-space: normal;
+}
+
+.md-assistant .octicon {
+	display: inline-block;
+	fill: currentColor;
+	vertical-align: text-bottom;
+}
+
+.md-assistant h1:hover .anchor .octicon-link:before,
+.md-assistant h2:hover .anchor .octicon-link:before,
+.md-assistant h3:hover .anchor .octicon-link:before,
+.md-assistant h4:hover .anchor .octicon-link:before,
+.md-assistant h5:hover .anchor .octicon-link:before,
+.md-assistant h6:hover .anchor .octicon-link:before {
+	width: 16px;
+	height: 16px;
+	content: ' ';
+	display: inline-block;
+	background-color: currentColor;
+	-webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
+	mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
+}
+
+.md-assistant details,
+.md-assistant figcaption,
+.md-assistant figure {
+	display: block;
+}
+
+.md-assistant summary {
+	display: list-item;
+}
+
+.md-assistant [hidden] {
+	display: none !important;
+}
+
+.md-assistant a {
+	background-color: transparent;
+	color: #0969da;
+	text-decoration: none;
+}
+
+.md-assistant abbr[title] {
+	border-bottom: none;
+	-webkit-text-decoration: underline dotted;
+	text-decoration: underline dotted;
+}
+
+.md-assistant b,
+.md-assistant strong {
+	font-weight: 600;
+}
+
+.md-assistant dfn {
+	font-style: italic;
+}
+
+.md-assistant h1 {
+	margin: .67em 0;
+	font-weight: 600;
+	padding-bottom: .3em;
+	font-size: 2em;
+	border-bottom: 1px solid #d1d9e0b3;
+}
+
+.md-assistant mark {
+	background-color: #fff8c5;
+	color: #1f2328;
+}
+
+.md-assistant small {
+	font-size: 90%;
+}
+
+.md-assistant sub,
+.md-assistant sup {
+	font-size: 75%;
+	line-height: 0;
+	position: relative;
+	vertical-align: baseline;
+}
+
+.md-assistant sub {
+	bottom: -0.25em;
+}
+
+.md-assistant sup {
+	top: -0.5em;
+}
+
+.md-assistant img {
+	border-style: none;
+	max-width: 100%;
+	box-sizing: content-box;
+}
+
+.md-assistant code,
+.md-assistant kbd,
+.md-assistant pre,
+.md-assistant samp {
+	font-family: monospace;
+	font-size: 1em;
+}
+
+.md-assistant figure {
+	margin: 1em 2.5rem;
+}
+
+.md-assistant hr {
+	box-sizing: content-box;
+	overflow: hidden;
+	background: transparent;
+	border-bottom: 1px solid #d1d9e0b3;
+	height: .25em;
+	padding: 0;
+	margin: 1.5rem 0;
+	background-color: #d1d9e0;
+	border: 0;
+}
+
+.md-assistant input {
+	font: inherit;
+	margin: 0;
+	overflow: visible;
+	font-family: inherit;
+	font-size: inherit;
+	line-height: inherit;
+}
+
+.md-assistant [type=button],
+.md-assistant [type=reset],
+.md-assistant [type=submit] {
+	-webkit-appearance: button;
+	appearance: button;
+}
+
+.md-assistant [type=checkbox],
+.md-assistant [type=radio] {
+	box-sizing: border-box;
+	padding: 0;
+}
+
+.md-assistant [type=number]::-webkit-inner-spin-button,
+.md-assistant [type=number]::-webkit-outer-spin-button {
+	height: auto;
+}
+
+.md-assistant [type=search]::-webkit-search-cancel-button,
+.md-assistant [type=search]::-webkit-search-decoration {
+	-webkit-appearance: none;
+	appearance: none;
+}
+
+.md-assistant ::-webkit-input-placeholder {
+	color: inherit;
+	opacity: .54;
+}
+
+.md-assistant ::-webkit-file-upload-button {
+	-webkit-appearance: button;
+	appearance: button;
+	font: inherit;
+}
+
+.md-assistant a:hover {
+	text-decoration: underline;
+}
+
+.md-assistant ::placeholder {
+	color: #59636e;
+	opacity: 1;
+}
+
+.md-assistant hr::before {
+	display: table;
+	content: "";
+}
+
+.md-assistant hr::after {
+	display: table;
+	clear: both;
+	content: "";
+}
+
+.md-assistant table {
+	border-spacing: 0;
+	border-collapse: collapse;
+	display: block;
+	width: max-content;
+	max-width: 100%;
+	overflow: auto;
+	font-variant: tabular-nums;
+}
+
+.md-assistant td,
+.md-assistant th {
+	padding: 0;
+}
+
+.md-assistant details summary {
+	cursor: pointer;
+}
+
+.md-assistant a:focus,
+.md-assistant [role=button]:focus,
+.md-assistant input[type=radio]:focus,
+.md-assistant input[type=checkbox]:focus {
+	outline: 2px solid #0969da;
+	outline-offset: -2px;
+	box-shadow: none;
+}
+
+.md-assistant a:focus:not(:focus-visible),
+.md-assistant [role=button]:focus:not(:focus-visible),
+.md-assistant input[type=radio]:focus:not(:focus-visible),
+.md-assistant input[type=checkbox]:focus:not(:focus-visible) {
+	outline: solid 1px transparent;
+}
+
+.md-assistant a:focus-visible,
+.md-assistant [role=button]:focus-visible,
+.md-assistant input[type=radio]:focus-visible,
+.md-assistant input[type=checkbox]:focus-visible {
+	outline: 2px solid #0969da;
+	outline-offset: -2px;
+	box-shadow: none;
+}
+
+.md-assistant a:not([class]):focus,
+.md-assistant a:not([class]):focus-visible,
+.md-assistant input[type=radio]:focus,
+.md-assistant input[type=radio]:focus-visible,
+.md-assistant input[type=checkbox]:focus,
+.md-assistant input[type=checkbox]:focus-visible {
+	outline-offset: 0;
+}
+
+.md-assistant kbd {
+	display: inline-block;
+	padding: 0.25rem;
+	font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
+	line-height: 10px;
+	color: #1f2328;
+	vertical-align: middle;
+	background-color: #f6f8fa;
+	border: solid 1px #d1d9e0b3;
+	border-bottom-color: #d1d9e0b3;
+	border-radius: 6px;
+	box-shadow: inset 0 -1px 0 #d1d9e0b3;
+}
+
+.md-assistant h1,
+.md-assistant h2,
+.md-assistant h3,
+.md-assistant h4,
+.md-assistant h5,
+.md-assistant h6 {
+	margin-top: 1.5rem;
+	margin-bottom: 1rem;
+	font-weight: 600;
+	line-height: 1.25;
+}
+
+.md-assistant h2 {
+	font-weight: 600;
+	padding-bottom: .3em;
+	font-size: 1.5em;
+	border-bottom: 1px solid #d1d9e0b3;
+}
+
+.md-assistant h3 {
+	font-weight: 600;
+	font-size: 1.25em;
+}
+
+.md-assistant h4 {
+	font-weight: 600;
+	font-size: 1em;
+}
+
+.md-assistant h5 {
+	font-weight: 600;
+	font-size: .875em;
+}
+
+.md-assistant h6 {
+	font-weight: 600;
+	font-size: .85em;
+	color: #59636e;
+}
+
+.md-assistant p {
+	margin-top: 0;
+	margin-bottom: 10px;
+}
+
+.md-assistant blockquote {
+	margin: 0;
+	padding: 0 1em;
+	color: #59636e;
+	border-left: .25em solid #d1d9e0;
+}
+
+.md-assistant ul{
+	margin-top: 0;
+	margin-bottom: 0;
+	padding-left: 1em;
+}
+.md-assistant ol {
+    margin-top: 0;
+    margin-bottom: 0;
+    padding-left: 1em;
+}
+
+.md-assistant ol ol,
+.md-assistant ul ol {
+	list-style-type: lower-roman;
+}
+
+.md-assistant ul ul ol,
+.md-assistant ul ol ol,
+.md-assistant ol ul ol,
+.md-assistant ol ol ol {
+	list-style-type: lower-alpha;
+}
+
+.md-assistant dd {
+	margin-left: 0;
+}
+
+.md-assistant tt,
+.md-assistant code,
+.md-assistant samp {
+	font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
+	font-size: 12px;
+}
+
+.md-assistant pre {
+	margin-top: 0;
+	margin-bottom: 0;
+	font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
+	font-size: 12px;
+	word-wrap: normal;
+}
+
+.md-assistant .octicon {
+	display: inline-block;
+	overflow: visible !important;
+	vertical-align: text-bottom;
+	fill: currentColor;
+}
+
+.md-assistant input::-webkit-outer-spin-button,
+.md-assistant input::-webkit-inner-spin-button {
+	margin: 0;
+	appearance: none;
+}
+
+.md-assistant .mr-2 {
+	margin-right: 0.5rem !important;
+}
+
+.md-assistant::before {
+	display: table;
+	content: "";
+}
+
+.md-assistant::after {
+	display: table;
+	clear: both;
+	content: "";
+}
+
+.md-assistant>*:first-child {
+	margin-top: 0 !important;
+}
+
+.md-assistant>*:last-child {
+	margin-bottom: 0 !important;
+}
+
+.md-assistant a:not([href]) {
+	color: inherit;
+	text-decoration: none;
+}
+
+.md-assistant .absent {
+	color: #d1242f;
+}
+
+.md-assistant .anchor {
+	float: left;
+	padding-right: 0.25rem;
+	margin-left: -20px;
+	line-height: 1;
+}
+
+.md-assistant .anchor:focus {
+	outline: none;
+}
+
+.md-assistant p,
+.md-assistant blockquote,
+.md-assistant ul,
+.md-assistant dl,
+.md-assistant table,
+.md-assistant pre,
+.md-assistant details {
+	margin-top: 0;
+	margin-bottom: 1rem;
+}
+.md-assistant ol details{
+    margin-top: 0;
+}
+.md-assistant blockquote> :first-child {
+	margin-top: 0;
+}
+
+.md-assistant blockquote> :last-child {
+	margin-bottom: 0;
+}
+
+.md-assistant h1 .octicon-link,
+.md-assistant h2 .octicon-link,
+.md-assistant h3 .octicon-link,
+.md-assistant h4 .octicon-link,
+.md-assistant h5 .octicon-link,
+.md-assistant h6 .octicon-link {
+	color: #1f2328;
+	vertical-align: middle;
+	visibility: hidden;
+}
+
+.md-assistant h1:hover .anchor,
+.md-assistant h2:hover .anchor,
+.md-assistant h3:hover .anchor,
+.md-assistant h4:hover .anchor,
+.md-assistant h5:hover .anchor,
+.md-assistant h6:hover .anchor {
+	text-decoration: none;
+}
+
+.md-assistant h1:hover .anchor .octicon-link,
+.md-assistant h2:hover .anchor .octicon-link,
+.md-assistant h3:hover .anchor .octicon-link,
+.md-assistant h4:hover .anchor .octicon-link,
+.md-assistant h5:hover .anchor .octicon-link,
+.md-assistant h6:hover .anchor .octicon-link {
+	visibility: visible;
+}
+
+.md-assistant h1 tt,
+.md-assistant h1 code,
+.md-assistant h2 tt,
+.md-assistant h2 code,
+.md-assistant h3 tt,
+.md-assistant h3 code,
+.md-assistant h4 tt,
+.md-assistant h4 code,
+.md-assistant h5 tt,
+.md-assistant h5 code,
+.md-assistant h6 tt,
+.md-assistant h6 code {
+	padding: 0 .2em;
+	font-size: inherit;
+}
+
+.md-assistant summary h1,
+.md-assistant summary h2,
+.md-assistant summary h3,
+.md-assistant summary h4,
+.md-assistant summary h5,
+.md-assistant summary h6 {
+	display: inline-block;
+}
+
+.md-assistant summary h1 .anchor,
+.md-assistant summary h2 .anchor,
+.md-assistant summary h3 .anchor,
+.md-assistant summary h4 .anchor,
+.md-assistant summary h5 .anchor,
+.md-assistant summary h6 .anchor {
+	margin-left: -40px;
+}
+
+.md-assistant summary h1,
+.md-assistant summary h2 {
+	padding-bottom: 0;
+	border-bottom: 0;
+}
+
+.md-assistant ul.no-list,
+.md-assistant ol.no-list {
+	padding: 0;
+	list-style-type: none;
+}
+
+.md-assistant ol[type="a s"] {
+	list-style-type: lower-alpha;
+}
+
+.md-assistant ol[type="A s"] {
+	list-style-type: upper-alpha;
+}
+
+.md-assistant ol[type="i s"] {
+	list-style-type: lower-roman;
+}
+
+.md-assistant ol[type="I s"] {
+	list-style-type: upper-roman;
+}
+
+.md-assistant ol[type="1"] {
+	list-style-type: decimal;
+}
+
+.md-assistant div>ol:not([type]) {
+	list-style-type: decimal;
+}
+
+.md-assistant ul ul,
+.md-assistant ul ol,
+.md-assistant ol ol,
+.md-assistant ol ul {
+	margin-top: 0;
+	margin-bottom: 0;
+}
+
+.md-assistant li>p {
+	margin-top: 1rem;
+}
+
+.md-assistant li+li {
+	margin-top: .25em;
+}
+
+.md-assistant dl {
+	padding: 0;
+}
+
+.md-assistant dl dt {
+	padding: 0;
+	margin-top: 1rem;
+	font-size: 1em;
+	font-style: italic;
+	font-weight: 600;
+}
+
+.md-assistant dl dd {
+	padding: 0 1rem;
+	margin-bottom: 1rem;
+}
+
+.md-assistant table th {
+	font-weight: 600;
+}
+
+.md-assistant table th,
+.md-assistant table td {
+	padding: 6px 13px;
+	border: 1px solid #d1d9e0;
+}
+
+.md-assistant table td> :last-child {
+	margin-bottom: 0;
+}
+
+.md-assistant table tr {
+	background-color: #ffffff;
+	border-top: 1px solid #d1d9e0b3;
+}
+
+.md-assistant table tr:nth-child(2n) {
+	background-color: #f6f8fa;
+}
+
+.md-assistant table img {
+	background-color: transparent;
+}
+
+.md-assistant img[align=right] {
+	padding-left: 20px;
+}
+
+.md-assistant img[align=left] {
+	padding-right: 20px;
+}
+
+.md-assistant .emoji {
+	max-width: none;
+	vertical-align: text-top;
+	background-color: transparent;
+}
+
+.md-assistant span.frame {
+	display: block;
+	overflow: hidden;
+}
+
+.md-assistant span.frame>span {
+	display: block;
+	float: left;
+	width: auto;
+	padding: 7px;
+	margin: 13px 0 0;
+	overflow: hidden;
+	border: 1px solid #d1d9e0;
+}
+
+.md-assistant span.frame span img {
+	display: block;
+	float: left;
+}
+
+.md-assistant span.frame span span {
+	display: block;
+	padding: 5px 0 0;
+	clear: both;
+	color: #1f2328;
+}
+
+.md-assistant span.align-center {
+	display: block;
+	overflow: hidden;
+	clear: both;
+}
+
+.md-assistant span.align-center>span {
+	display: block;
+	margin: 13px auto 0;
+	overflow: hidden;
+	text-align: center;
+}
+
+.md-assistant span.align-center span img {
+	margin: 0 auto;
+	text-align: center;
+}
+
+.md-assistant span.align-right {
+	display: block;
+	overflow: hidden;
+	clear: both;
+}
+
+.md-assistant span.align-right>span {
+	display: block;
+	margin: 13px 0 0;
+	overflow: hidden;
+	text-align: right;
+}
+
+.md-assistant span.align-right span img {
+	margin: 0;
+	text-align: right;
+}
+
+.md-assistant span.float-left {
+	display: block;
+	float: left;
+	margin-right: 13px;
+	overflow: hidden;
+}
+
+.md-assistant span.float-left span {
+	margin: 13px 0 0;
+}
+
+.md-assistant span.float-right {
+	display: block;
+	float: right;
+	margin-left: 13px;
+	overflow: hidden;
+}
+
+.md-assistant span.float-right>span {
+	display: block;
+	margin: 13px auto 0;
+	overflow: hidden;
+	text-align: right;
+}
+
+.md-assistant code,
+.md-assistant tt {
+	padding: .2em .4em;
+	margin: 0;
+	font-size: 85%;
+	white-space: break-spaces;
+	background-color: #818b981f;
+	border-radius: 6px;
+}
+
+.md-assistant code br,
+.md-assistant tt br {
+	display: none;
+}
+
+.md-assistant del code {
+	text-decoration: inherit;
+}
+
+.md-assistant samp {
+	font-size: 85%;
+}
+
+.md-assistant pre code {
+	font-size: 100%;
+}
+
+.md-assistant pre>code {
+	padding: 0;
+	margin: 0;
+	word-break: normal;
+	white-space: pre;
+	background: transparent;
+	border: 0;
+}
+
+.md-assistant .highlight {
+	margin-bottom: 1rem;
+}
+
+.md-assistant .highlight pre {
+	margin-bottom: 0;
+	word-break: normal;
+}
+
+.md-assistant .highlight pre,
+.md-assistant pre {
+	padding: 1rem;
+	overflow: auto;
+	font-size: 85%;
+	line-height: 1.45;
+	color: #1f2328;
+	background-color: #f6f8fa;
+	border-radius: 6px;
+}
+
+.md-assistant pre code,
+.md-assistant pre tt {
+	display: inline;
+	max-width: auto;
+	padding: 0;
+	margin: 0;
+	overflow: visible;
+	line-height: inherit;
+	word-wrap: normal;
+	background-color: transparent;
+	border: 0;
+}
+
+.md-assistant .csv-data td,
+.md-assistant .csv-data th {
+	padding: 5px;
+	overflow: hidden;
+	font-size: 12px;
+	line-height: 1;
+	text-align: left;
+	white-space: nowrap;
+}
+
+.md-assistant .csv-data .blob-num {
+	padding: 10px 0.5rem 9px;
+	text-align: right;
+	background: #ffffff;
+	border: 0;
+}
+
+.md-assistant .csv-data tr {
+	border-top: 0;
+}
+
+.md-assistant .csv-data th {
+	font-weight: 600;
+	background: #f6f8fa;
+	border-top: 0;
+}
+
+.md-assistant [data-footnote-ref]::before {
+	content: "[";
+}
+
+.md-assistant [data-footnote-ref]::after {
+	content: "]";
+}
+
+.md-assistant .footnotes {
+	font-size: 12px;
+	color: #59636e;
+	border-top: 1px solid #d1d9e0;
+}
+
+.md-assistant .footnotes ol {
+	padding-left: 1rem;
+}
+
+.md-assistant .footnotes ol ul {
+	display: inline-block;
+	padding-left: 1rem;
+	margin-top: 1rem;
+}
+
+.md-assistant .footnotes li {
+	position: relative;
+}
+
+.md-assistant .footnotes li:target::before {
+	position: absolute;
+	top: calc(0.5rem * -1);
+	right: calc(0.5rem * -1);
+	bottom: calc(0.5rem * -1);
+	left: calc(1.5rem * -1);
+	pointer-events: none;
+	content: "";
+	border: 2px solid #0969da;
+	border-radius: 6px;
+}
+
+.md-assistant .footnotes li:target {
+	color: #1f2328;
+}
+
+.md-assistant .footnotes .data-footnote-backref g-emoji {
+	font-family: monospace;
+}
+
+.md-assistant body:has(:modal) {
+	padding-right: var(--dialog-scrollgutter) !important;
+}
+
+.md-assistant .pl-c {
+	color: #59636e;
+}
+
+.md-assistant .pl-c1,
+.md-assistant .pl-s .pl-v {
+	color: #0550ae;
+}
+
+.md-assistant .pl-e,
+.md-assistant .pl-en {
+	color: #6639ba;
+}
+
+.md-assistant .pl-smi,
+.md-assistant .pl-s .pl-s1 {
+	color: #1f2328;
+}
+
+.md-assistant .pl-ent {
+	color: #0550ae;
+}
+
+.md-assistant .pl-k {
+	color: #cf222e;
+}
+
+.md-assistant .pl-s,
+.md-assistant .pl-pds,
+.md-assistant .pl-s .pl-pse .pl-s1,
+.md-assistant .pl-sr,
+.md-assistant .pl-sr .pl-cce,
+.md-assistant .pl-sr .pl-sre,
+.md-assistant .pl-sr .pl-sra {
+	color: #0a3069;
+}
+
+.md-assistant .pl-v,
+.md-assistant .pl-smw {
+	color: #953800;
+}
+
+.md-assistant .pl-bu {
+	color: #82071e;
+}
+
+.md-assistant .pl-ii {
+	color: #f6f8fa;
+	background-color: #82071e;
+}
+
+.md-assistant .pl-c2 {
+	color: #f6f8fa;
+	background-color: #cf222e;
+}
+
+.md-assistant .pl-sr .pl-cce {
+	font-weight: bold;
+	color: #116329;
+}
+
+.md-assistant .pl-ml {
+	color: #3b2300;
+}
+
+.md-assistant .pl-mh,
+.md-assistant .pl-mh .pl-en,
+.md-assistant .pl-ms {
+	font-weight: bold;
+	color: #0550ae;
+}
+
+.md-assistant .pl-mi {
+	font-style: italic;
+	color: #1f2328;
+}
+
+.md-assistant .pl-mb {
+	font-weight: bold;
+	color: #1f2328;
+}
+
+.md-assistant .pl-md {
+	color: #82071e;
+	background-color: #ffebe9;
+}
+
+.md-assistant .pl-mi1 {
+	color: #116329;
+	background-color: #dafbe1;
+}
+
+.md-assistant .pl-mc {
+	color: #953800;
+	background-color: #ffd8b5;
+}
+
+.md-assistant .pl-mi2 {
+	color: #d1d9e0;
+	background-color: #0550ae;
+}
+
+.md-assistant .pl-mdr {
+	font-weight: bold;
+	color: #8250df;
+}
+
+.md-assistant .pl-ba {
+	color: #59636e;
+}
+
+.md-assistant .pl-sg {
+	color: #818b98;
+}
+
+.md-assistant .pl-corl {
+	text-decoration: underline;
+	color: #0a3069;
+}
+
+.md-assistant [role=button]:focus:not(:focus-visible),
+.md-assistant [role=tabpanel][tabindex="0"]:focus:not(:focus-visible),
+.md-assistant button:focus:not(:focus-visible),
+.md-assistant summary:focus:not(:focus-visible),
+.md-assistant a:focus:not(:focus-visible) {
+	outline: none;
+	box-shadow: none;
+}
+
+.md-assistant [tabindex="0"]:focus:not(:focus-visible),
+.md-assistant details-dialog:focus:not(:focus-visible) {
+	outline: none;
+}
+
+.md-assistant g-emoji {
+	display: inline-block;
+	min-width: 1ch;
+	font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+	font-size: 1em;
+	font-style: normal !important;
+	font-weight: 400;
+	line-height: 1;
+	vertical-align: -0.075em;
+}
+
+.md-assistant g-emoji img {
+	width: 1em;
+	height: 1em;
+}
+
+.md-assistant .task-list-item {
+	list-style-type: none;
+}
+
+.md-assistant .task-list-item label {
+	font-weight: 400;
+}
+
+.md-assistant .task-list-item.enabled label {
+	cursor: pointer;
+}
+
+.md-assistant .task-list-item+.task-list-item {
+	margin-top: 0.25rem;
+}
+
+.md-assistant .task-list-item .handle {
+	display: none;
+}
+
+.md-assistant .task-list-item-checkbox {
+	margin: 0 .2em .25em -1.4em;
+	vertical-align: middle;
+}
+
+.md-assistant ul:dir(rtl) .task-list-item-checkbox {
+	margin: 0 -1.6em .25em .2em;
+}
+
+.md-assistant ol:dir(rtl) .task-list-item-checkbox {
+	margin: 0 -1.6em .25em .2em;
+}
+
+.md-assistant .contains-task-list:hover .task-list-item-convert-container,
+.md-assistant .contains-task-list:focus-within .task-list-item-convert-container {
+	display: block;
+	width: auto;
+	height: 24px;
+	overflow: visible;
+	clip: auto;
+}
+
+.md-assistant ::-webkit-calendar-picker-indicator {
+	filter: invert(50%);
+}
+
+.md-assistant .markdown-alert {
+	padding: 0.5rem 1rem;
+	margin-bottom: 1rem;
+	color: inherit;
+	border-left: .25em solid #d1d9e0;
+}
+
+.md-assistant .markdown-alert> :first-child {
+	margin-top: 0;
+}
+
+.md-assistant .markdown-alert> :last-child {
+	margin-bottom: 0;
+}
+
+.md-assistant .markdown-alert .markdown-alert-title {
+	display: flex;
+	font-weight: 500;
+	align-items: center;
+	line-height: 1;
+}
+
+.md-assistant .markdown-alert.markdown-alert-note {
+	border-left-color: #0969da;
+}
+
+.md-assistant .markdown-alert.markdown-alert-note .markdown-alert-title {
+	color: #0969da;
+}
+
+.md-assistant .markdown-alert.markdown-alert-important {
+	border-left-color: #8250df;
+}
+
+.md-assistant .markdown-alert.markdown-alert-important .markdown-alert-title {
+	color: #8250df;
+}
+
+.md-assistant .markdown-alert.markdown-alert-warning {
+	border-left-color: #9a6700;
+}
+
+.md-assistant .markdown-alert.markdown-alert-warning .markdown-alert-title {
+	color: #9a6700;
+}
+
+.md-assistant .markdown-alert.markdown-alert-tip {
+	border-left-color: #1a7f37;
+}
+
+.md-assistant .markdown-alert.markdown-alert-tip .markdown-alert-title {
+	color: #1a7f37;
+}
+
+.md-assistant .markdown-alert.markdown-alert-caution {
+	border-left-color: #cf222e;
+}
+
+.md-assistant .markdown-alert.markdown-alert-caution .markdown-alert-title {
+	color: #d1242f;
+}
+
+.md-assistant>*:first-child>.heading-element:first-child {
+	margin-top: 0 !important;
+}
+
+.md-assistant .highlight pre:has(+.zeroclipboard-container) {
+	min-height: 52px;
+}
+
+/*---- ReferenceItem 样式 -----------*/
+.ja.reference-item {
+	display: flex;
+	align-items: flex-start;
+	gap: 12px;
+	padding: 16px;
+	background: rgba(249, 250, 251, 0.8);
+	border-radius: 12px;
+	margin: 8px 0;
+	border: 1px solid rgba(229, 231, 235, 0.5);
+	transition: all 0.2s ease;
+	backdrop-filter: blur(8px);
+	box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
+}
+
+.ja.reference-item:hover {
+	background: rgba(249, 250, 251, 0.95);
+	border-color: rgba(209, 213, 219, 0.8);
+	box-shadow: 0 4px 6px rgba(0, 0, 0, 0.04);
+}
+
+.ja.reference-item .icon {
+	flex-shrink: 0;
+	margin-top: 3px;
+	opacity: 0.8;
+	transition: opacity 0.2s ease;
+}
+
+.ja.reference-item:hover .icon {
+	opacity: 1;
+}
+
+.ja.reference-item .content-wrapper {
+	flex: 1;
+	min-width: 0;
+}
+
+.ja.reference-item .content {
+	color: #374151;
+	font-size: 0.8rem;
+	line-height: 1.6;
+	margin: 0;
+	word-break: break-word;
+}
+
+.ja.reference-item .text-content {
+	cursor: pointer;
+}
+
+.ja.reference-item .text-content:hover {
+	color: #1f2937;
+}
+
+.ja.reference-item .secondary-text {
+	color: #6b7280;
+	font-size: 0.875rem;
+	margin-top: 6px;
+	display: flex;
+	align-items: center;
+	gap: 8px;
+}
+
+.ja.reference-item .secondary-text::before {
+	content: '';
+	display: inline-block;
+	width: 4px;
+	height: 4px;
+	background: #d1d5db;
+	border-radius: 50%;
+}
+
+.ja.reference-item a {
+	color: inherit;
+	text-decoration: none;
+	transition: color 0.2s ease;
+}
+
+.ja.reference-item a:hover {
+	color: #2563eb;
+}
+
+.ja.reference-item .clickable {
+	cursor: pointer;
+}
+
+.ja.reference-item .clickable:hover {
+	color: #2563eb;
+}
+
+
+/* ---------- chat-tip 样式 --------------------*/
+.ja.tip {
+	display: none;
+	justify-content: center;
+	align-items: center;
+	opacity: 0;
+	transition: opacity 0.3s ease;
+	padding: 0.5rem 1rem;
+}
+
+.ja.tip.show {
+	display: flex;
+	opacity: 1;
+}
+
+.ja.tip-content {
+	padding-top: 1rem;
+}
+
+.ja.tip-content-wrapper {
+	padding: 0.5rem 1rem;
+	background-color: rgba(0, 0, 0, .1);
+	color: #666;
+	border-radius: 4px;
+	display: flex;
+	align-items: center;
+	gap: 8px;
+}
+
+.ja.tip .dots span {
+	animation: dots 1.5s infinite;
+	opacity: 0;
+}
+
+.ja.tip .dots span:nth-child(2) {
+	animation-delay: 0.5s;
+}
+
+.ja.tip .dots span:nth-child(3) {
+	animation-delay: 1s;
+}
+
+@keyframes dots {
+	0% {
+		opacity: 0;
+	}
+
+	50% {
+		opacity: 1;
+	}
+
+	100% {
+		opacity: 0;
+	}
+}
+
+/* HTML: <div class="loader"></div> */
+.ja.tip .loader {
+	width: 40px;
+	height: 20px;
+	--c: no-repeat radial-gradient(farthest-side, #c6c6c6 93%, #0000);
+	background: var(--c) 0 0,
+		var(--c) 50% 0,
+		var(--c) 100% 0;
+	background-size: 8px 8px;
+	position: relative;
+	animation: l4-0 1s linear infinite alternate;
+}
+
+.ja.tip .loader:before {
+	content: "";
+	position: absolute;
+	width: 8px;
+	height: 12px;
+	border-radius: 4px;
+	background: #0a74ff;
+	left: 0;
+	top: 0;
+	animation: l4-1 1s linear infinite alternate,
+		l4-2 0.5s cubic-bezier(0, 200, .8, 200) infinite,
+		l4-3 2s linear infinite;
+}
+
+@keyframes l4-0 {
+	0% {
+		background-position: 0 100%, 50% 0, 100% 0
+	}
+
+	8%,
+	42% {
+		background-position: 0 0, 50% 0, 100% 0
+	}
+
+	50% {
+		background-position: 0 0, 50% 100%, 100% 0
+	}
+
+	58%,
+	92% {
+		background-position: 0 0, 50% 0, 100% 0
+	}
+
+	100% {
+		background-position: 0 0, 50% 0, 100% 100%
+	}
+}
+
+@keyframes l4-1 {
+	100% {
+		left: calc(100% - 8px)
+	}
+}
+
+@keyframes l4-2 {
+	100% {
+		top: -0.2px
+	}
+}
+
+@keyframes l4-3 {
+
+	0%,
+	100% {
+		background: #0a74ff;
+		height: 12px;
+	}
+
+	25% {
+		background: rgba(112, 47, 251, 0.65);
+		height: 18px;
+	}
+
+	50% {
+		background: #f27e30;
+		height: 16px;
+	}
+
+	75% {
+		background: #2c98ea;
+		height: 14px;
+	}
+}
+
+
+/*---------chat message 样式*/
+
+.ja_msg {
+	display: flex;
+	align-items: flex-start;
+	animation: message-fade-in 0.3s ease-in-out;
+}
+
+.ja_msg.user {
+	justify-content: flex-end;
+}
+
+.ja_msg.assistant {
+	justify-content: flex-start;
+}
+
+@keyframes message-fade-in {
+	from {
+		opacity: 0;
+		transform: translateY(10px);
+	}
+
+	to {
+		opacity: 1;
+		transform: translateY(0);
+	}
+}
+
+.ja_msg .logo {
+	width: 2rem;
+	height: 2rem;
+	border-radius: 50%;
+	flex-shrink: 0;
+	--tw-bg-opacity: 1;
+
+}
+
+.ja_msg .logo.assistant {
+	background-color: rgb(209 213 219 / 1);
+	margin-right: 1rem;
+}
+
+.ja_msg .logo.user {
+	background-color: rgb(99, 102, 241);
+	margin-left: 1rem;
+}
+
+.ja_msg .container_wrapper {
+    position: relative;
+	padding: 0.875rem 1.25rem;
+	min-height: 1.5rem;
+	transition: all .2s cubic-bezier(0.4, 0, 0.2, 1);
+	max-width: calc(100% - 8rem);
+	--tw-ring-offset-shadow: 0 0 #0000;
+	--tw-ring-shadow: 0 0 #0000;
+	--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+	box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
+}
+
+.ja_msg .container_wrapper.assistant:hover {
+	box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.1) 0px 2px 4px -2px
+}
+
+.ja_msg .container_wrapper.assistant {
+	color: rgb(51, 65, 85);
+	background-color: #fff;
+	border-bottom-left-radius: 1rem;
+	border-top-right-radius: 1rem;
+	border-bottom-right-radius: 1rem;
+	border: 1px solid rgb(241 245 249 / 0.8);
+}
+
+.ja_msg .container_wrapper.user {
+	background-image: linear-gradient(to right bottom, rgb(99, 102, 241), rgb(129, 140, 248));
+	color: white;
+	backdrop-filter: blur(4px);
+	border-bottom-right-radius: 1rem;
+	border-top-left-radius: 1rem;
+	border-bottom-left-radius: 1rem;
+
+}
+.ja_msg .container_wrapper .resizer{
+    position: absolute;
+    right: 0;
+    top: 0;
+    height: 100%;
+    width: 4px; /* 稍微加宽,更容易点击 */
+    background-color: transparent;
+    display: none;
+    cursor: ew-resize;
+}
+.ja_msg .container_wrapper .resizer:hover,.ja_msg .container_wrapper .resizer.active{
+    background-color: rgba(52, 152, 219, 0.3);
+}
+.ja_msg . {
+	position: relative;
+	white-space: pre-line;
+	word-break: break-all;
+}
+
+.ja_msg .fragment img {
+	max-width: 90%;
+}
+
+.ja_msg .fragment iframe {
+    width: 100%;
+    min-width: 20em;
+    max-height: 70vh;
+    border:none
+}
+
+
+.ja_msg .container_wrapper:has(iframe) {
+
+    width: 50%;
+    min-width: 20em;
+    transition: none;
+}
+.ja_msg .container_wrapper:has(iframe) .resizer{
+    display: block;
+}
+@media screen and (max-width: 768px) {
+    .ja_msg .fragment iframe {
+        min-width: 18em;
+    }
+    .ja_msg .container_wrapper:has(iframe) {
+        min-width: 18em;
+    }
+}
+.ja_msg .thinking {
+	color: #666;
+	display: flex;
+	align-items: center;
+	gap: 4px;
+}
+
+.ja_msg .thinking .dots {
+	display: flex;
+}
+
+.ja_msg .thinking .dots span {
+	width: 4px;
+	height: 4px;
+	border-radius: 50%;
+	background-color: currentColor;
+	margin: 0 2px;
+	opacity: 0;
+	animation: dot-fade 1.4s infinite;
+}
+
+.ja_msg .thinking .dots span:nth-child(2) {
+	animation-delay: 0.2s;
+}
+
+.ja_msg .thinking .dots span:nth-child(3) {
+	animation-delay: 0.4s;
+}
+
+@keyframes dot-fade {
+
+	0%,
+	100% {
+		opacity: 0;
+	}
+
+	50% {
+		opacity: 1;
+	}
+}
+
+.ja_msg .refers {
+	margin-top: 16px;
+	padding-top: 8px;
+
+}
+
+.ja_msg .hide {
+	display: none;
+}
+
+.ja_msg .refers_label {
+	color: rgb(107 114 128 / 1);
+	font-size: 14px;
+	line-height: 1.25;
+}
+.ja_msg .refers_count {
+	color: rgb(107 114 128 / 1);
+	font-size: 14px;
+	line-height: 1.25;
+	margin-left: 0px;
+}
+.ja_msg .refers_btn{
+	margin-left: 8px;
+	transform: rotate(0deg);
+	transition: transform .3s;
+}
+.ja_msg .refers.expand .refers_btn{
+	transform: rotate(180deg);
+}
+.ja_msg .refers_header {
+	display: inline-block;
+	cursor: pointer;
+}
+.ja_msg .refers_list {
+	overflow: hidden;
+	max-height: 0;
+	transition: max-height 0.3s ease-in-out;
+}
+.ja_msg .refers.expand .refers_list{
+	max-height: 9999px;
+	display: block;
+}
+
+
+
+.message-action-btn {
+	transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+	transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+	transition-duration: 150ms;
+	border-radius: 6px;
+	background: none;
+	border: none;
+	padding: 0.375rem;
+	cursor: pointer;
+}
+
+.message-action-btn:hover {
+	background: #f3f4f6;
+}
+
+.message-action-btn:hover path {
+	fill: #475569;
+}
+
+.message-action-btn.error:hover path {
+	fill: red;
+}
+.message-action-btn.again:hover path {
+	fill: rgba(0, 0, 0, 0.83);
+}
+
+/*--------opt panel --------- 样式*/
+.ja_input-panel {
+	flex: 1;
+	overflow: hidden;
+	display: flex;
+	flex-direction: column;
+	border-top: 1px solid rgb(229, 231, 235);
+	padding: .75rem;
+}
+
+.ja_input-panel .container:hover {
+	box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.1) 0px 2px 4px -2px;
+}
+
+.ja_input-panel .container {
+	background: rgba(248, 250, 252, 0.7);
+	box-sizing: border-box;
+	transition-property: all;
+	transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+	transition-duration: 150ms;
+	border-radius: 1rem;
+	box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
+}
+
+.ja_input_wrapper {
+	display: flex;
+}
+
+.extra_btns {
+	display: flex;
+	flex: none;
+	flex-shrink: 0;
+	gap: 0.5rem;
+	padding: .5rem;
+	align-items: center;
+    justify-content: center;
+}
+
+.extra_btns button:hover {
+	background: #f3f4f6;
+}
+
+.extra_btns button {
+	transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+	transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+	transition-duration: 150ms;
+	border-radius: 9999px;
+	line-height: 18px;
+	background: none;
+	border: none;
+	width: 34px;
+	height: 34px;
+}
+
+.ja_input-panel .input {
+	border: none;
+	outline: none;
+	resize: none;
+	width: 100%;
+	box-sizing: border-box;
+	padding: .5rem .5rem .5rem 1rem;
+	font-size: 1rem;
+	line-height: 1.5rem;
+	font-family: inherit;
+	font-feature-settings: inherit;
+	font-variation-settings: inherit;
+	letter-spacing: inherit;
+	color: #333;
+	flex: 1;
+	height: 2.5rem;
+	transition: height 0.3s ease;
+	background: transparent;
+}
+
+/*为.input设置滚动条*/
+.ja_input-panel .input::-webkit-scrollbar {
+	width: 6px;
+	height: 6px;
+}
+
+/* 滚动条轨道样式 */
+.ja_input-panel .input::-webkit-scrollbar-track {
+	background: #f1f1f1;
+	border-radius: 4px;
+}
+
+.ja_input-panel .input::-webkit-scrollbar-track {
+	background: #f5f5f5;
+	border-radius: 3px;
+	box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.1);
+}
+
+.ja_input-panel .input::-webkit-scrollbar-thumb {
+	background: #c1c1c1;
+	border-radius: 3px;
+	transition: background 0.3s ease;
+}
+
+.ja_input-panel .input.auto-height {
+	height: 5rem;
+	overflow-y: auto;
+}
+
+.ja_input-panel .input:focus {
+	border: none;
+	outline: none;
+}
+
+.ja_input-panel .btn-panel {
+	flex-shrink: 0;
+	display: flex;
+	flex-direction: column;
+	margin-left: 1rem;
+	justify-content: space-between;
+}
+
+.ja_input-panel .send_btn {
+	transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+	transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+	transition-duration: 150ms;
+	border-radius: 9999px;
+	line-height: 18px;
+	background: none;
+	border: none;
+	width: 34px;
+	height: 34px;
+}
+.ja_input-panel .send_btn svg {
+	fill: #6b7280
+}
+.ja_input-panel .send_btn.active svg{
+	fill:#0217f6;
+}
+.ja_input-panel .send_btn:hover {
+	background: #f3f4f6;
+}
+
+.ja_input-panel .send_btn.disabled {
+	background-color: rgb(229, 231, 235);
+	cursor: not-allowed;
+}
+
+.ja_input-panel .send_btn.cancel {
+	background-color: #dc2626;
+}
+
+.ja_input-panel .send_btn.cancel:hover {
+	background-color: #b91c1c;
+}
+
+.ja_input-panel .send_btn span {
+	display: inline-block;
+	margin-left: 4px
+}
+
+.hide {
+	display: none !important;
+}
+
+.ja_input-panel .setting_btn {
+	transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+	transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+	transition-duration: 150ms;
+	border-radius: 9999px;
+	line-height: 18px;
+	background: none;
+	border: none;
+	width: 34px;
+	height: 34px;
+}
+
+.ja_input-panel .setting_btn path {
+	fill: #6b7280;
+}
+
+.ja_input-panel .setting_btn:hover {
+	background: #f3f4f6;
+}
+
+.ja_input-panel .input_extra {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+}
+
+.ja_input-panel .tooltip {
+	color: #bdc3d0;
+	font-size: 1rem;
+	display: flex;
+	align-items: center;
+}
+
+.msg-tips-user {
+	display: none;
+}
+
+.msg-tips-box {
+	min-width: 13rem;
+	max-width: 100%;
+	border-top: 1px solid rgba(189, 195, 208, 0.21);
+	margin-top: 8px;
+	padding-top: 8px;
+	font-size: 12px;
+	color: rgb(148, 163, 184);
+	text-align: right;
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+}
+
+
+/* ----------------以下是删除弹窗中的样式 */
+/*模态框背景*/
+.modal {
+	display: none;
+	/* 默认隐藏 */
+	position: fixed;
+	z-index: 1000;
+	left: 0;
+	top: 0;
+	width: 100%;
+	height: 100%;
+	overflow: auto;
+	background-color: rgba(0, 0, 0, 0.2);
+	/* 使用rgba来稍作透明 */
+	transition: background-color 0.3s ease-out;
+	/* 添加背景颜色过渡 */
+}
+
+/* 模态框内容 */
+.modal-content {
+	background-color: #fefefe;
+	margin: 25% auto;
+	padding: 20px;
+	width: 80%;
+	/* 可根据需要调整 */
+	max-width: 300px;
+	text-align: center;
+	border-radius: 10px;
+	box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2),
+		/* 较浅的外部阴影 */
+		0 6px 20px rgba(0, 0, 0, 0.19);
+	/* 更深一些的内部阴影 */
+	transition: all 0.3s ease-out;
+	/* 对所有可过渡属性添加过渡效果 */
+}
+
+.modal-content p {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+/* 按钮样式 */
+#confirmDelete,
+#cancelDelete {
+	background-color: #4CAF50;
+	/* 绿色 */
+	border: 1px solid #4CAF50FF;
+	color: white;
+	padding: 5px 10px;
+	text-align: center;
+	text-decoration: none;
+	display: inline-block;
+	font-size: 14px;
+	margin: 4px 2px;
+	cursor: pointer;
+	border-radius: 4px;
+	transition: background-color 0.3s, color 0.3s, transform 0.3s;
+	/* 添加过渡效果 */
+}
+
+#cancelDelete {
+	background-color: #fff;
+	/* 白色 */
+	color: #888888;
+	border: 1px solid rgba(121, 121, 121, 0.41);
+}
+
+/* 鼠标悬停时改变按钮的颜色 */
+#confirmDelete:hover {
+	background-color: #45a049;
+	transform: translateY(-2px);
+}
+
+#cancelDelete:hover {
+	background-color: #ddd;
+	color: #777;
+	transform: translateY(-2px);
+}
+
+/*----------------*/
+/*复制弹窗*/
+.notification {
+	position: fixed;
+	top:15%;
+	left: 49%;
+	font-size: 14px;
+	background-color:#FFF;
+	color: rgba(0, 0, 0, 0.66);
+	padding: 10px 30px;
+	border-radius: 5px;
+	display: none;
+	box-shadow: 0 4px 8px rgba(0, 0, 0, 0.25);
+	z-index: 1000;
+}
+.notif-box{
+	display: flex;
+	align-items: center;
+}
+/***----重新生成提示框--------------------*/
+.tooltip {
+	visibility: hidden;
+	position: absolute;
+	top: -20px;
+	left: 60px;
+	background-color: #6d72f3;
+	color: #fff;
+	text-align: center;
+	padding: 5px;
+	border-radius: 4px;
+	font-size: 12px;
+	z-index: 1;
+}
+
+.again:hover .tooltip {
+	visibility: visible;
+}

+ 94 - 0
src/components/jb-ai-chat/base.js

@@ -0,0 +1,94 @@
+import baseCss from './base.css?raw'
+const baseStyle = new CSSStyleSheet();
+baseStyle.replace(baseCss)
+
+class BaseElement extends HTMLElement {
+    constructor() {
+        super();
+        this.findParent();
+
+
+    }
+
+
+    findParent() {
+        // 遍历父级直到找到目标组件
+        let parent = this.getRootNode().host;
+        while (parent && !(parent instanceof BaseElement)) {
+            if (parent.classList.contains("ai-chat-container")) {
+                parent = null;
+                break;
+            }
+            parent = parent.getRootNode().host;
+        }
+        if (parent) {
+            this.injectData = parent.injectData ?? {};
+        }
+    }
+
+    injectData = {}
+
+    get chatInstance() {
+        if (!this.injectData?.chatInstance) {
+            this.findParent();
+        }
+        return this.injectData?.chatInstance;
+    }
+
+
+    showConfirm({ title, content, confirmText, cancelText} = {}) {
+
+        return this.chatInstance.confirmDialog.showConfirm({ title, content, confirmText, cancelText});
+    }
+
+
+
+    /**
+     * 向子组件注入数据
+     * @param element
+     * @param key
+     * @param value
+     * @param recursion 是否递归注入
+     */
+    inject(element, key, value, recursion) {
+        element.receive(key, value, recursion);
+    }
+
+    /**
+     * 接收注入的数据
+     * @param key
+     * @param value
+     * @param recursion
+     */
+    receive(key, value, recursion = true) {
+        this.injectData[key] = value;
+        if (this.onReceive) {
+            this.onReceive(key, value);
+        }
+        if (recursion) {
+            this.shadowRoot.querySelectorAll('*').forEach(element => {
+                //如果 element是BaseElement的子组件,则注入数据
+                if (element instanceof BaseElement) {
+                    this.inject(element, key, value, recursion);
+                }
+            });
+        }
+    }
+
+
+
+}
+
+/**
+ * 聊天状态枚举
+ */
+class ChatStatus {
+
+    static IS_WAITING = Symbol("等待中");
+    static IS_RECEIVING = Symbol("接收中");
+    static IS_READY = Symbol("准备完毕");
+    static DISCONNECTED = Symbol("连接断开");
+
+}
+
+export {BaseElement, ChatStatus, baseStyle};

+ 197 - 0
src/components/jb-ai-chat/chat-confirm-dialog.js

@@ -0,0 +1,197 @@
+// 定义消息组件
+import {
+    BaseElement, ChatStatus, baseStyle
+} from "./base.js";
+
+
+class ChatConfirmDialog extends BaseElement {
+    constructor() {
+        super();
+        this.attachShadow({
+            mode: 'open'
+        });
+        this.shadowRoot.adoptedStyleSheets = [baseStyle];
+    }
+
+    connectedCallback() {
+        this.render();
+    }
+
+    render() {
+        this.shadowRoot.innerHTML = `
+            <style>
+                /* 弹窗样式 */
+                .popup-overlay {
+                    position: fixed;
+                    top: 0;
+                    left: 0;
+                    right: 0;
+                    bottom: 0;
+                    background-color: rgba(0, 0, 0, 0.4);
+                    backdrop-filter: blur(2px);
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    z-index: 9000;
+                    opacity: 1;
+                    transition: opacity 0.25s ease;
+                }
+
+                .popup-overlay.hide {
+                    opacity: 0;
+                    pointer-events: none;
+                }
+
+                .popup-container {
+                    background: white;
+                    border-radius: 12px;
+                    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
+                    max-width: 400px;
+                    width: 80%;
+                    padding: 24px;
+                    animation: popup-scale 0.25s ease forwards;
+                }
+
+                @keyframes popup-scale {
+                    from { transform: scale(0.95); opacity: 0; }
+                    to { transform: scale(1); opacity: 1; }
+                }
+
+                .popup-content .title {
+                    margin: 0 0 12px 0;
+                    font-size: 18px;
+                    font-weight: 600;
+                    color: rgba(0, 0, 0, 0.85);
+                }
+
+                .popup-content .content {
+                    margin: 0 0 24px 0;
+                    color: rgba(0, 0, 0, 0.65);
+                    line-height: 1.5;
+                }
+
+                .popup-actions {
+                    display: flex;
+                    justify-content: flex-end;
+                    gap: 12px;
+                }
+
+                .popup-cancel-btn {
+                    background: transparent;
+                    border: 1px solid rgba(0, 0, 0, 0.15);
+                    border-radius: 6px;
+                    padding: 8px 16px;
+                    cursor: pointer;
+                    transition: all 0.2s;
+                    font-size: 14px;
+                }
+
+                .popup-cancel-btn:hover {
+                    background: rgba(0, 0, 0, 0.05);
+                    border-color: rgba(0, 0, 0, 0.2);
+                }
+
+                .popup-cancel-btn:active {
+                    background: rgba(0, 0, 0, 0.1);
+                }
+
+                .popup-confirm-btn {
+                    background: #ff4d4f;
+                    color: white;
+                    border: none;
+                    border-radius: 6px;
+                    padding: 8px 16px;
+                    cursor: pointer;
+                    transition: all 0.2s;
+                    font-size: 14px;
+                    font-weight: 500;
+                }
+
+                .popup-confirm-btn:hover {
+                    background: #ff7875;
+                }
+
+                .popup-confirm-btn:active {
+                    background: #f5222d;
+                }
+            </style>
+            <!-- 确认弹窗 -->
+            <div class="popup-overlay hide">
+                <div class="popup-container">
+                    <div class="popup-content">
+                        <h3 class="title">清空全部消息</h3>
+                        <div class="content">您确定要清空当前会话的所有消息记录吗?此操作无法撤销。</div>
+                        <div class="popup-actions">
+                            <button class="popup-cancel-btn">取消</button>
+                            <button class="popup-confirm-btn">确认清空</button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        `
+    }
+
+    /**
+     * 显示确认对话框
+     * @param {Object} options - 对话框选项
+     * @param {string} options.title - 标题 (默认: "确认操作")
+     * @param {string} options.content - 内容 (默认: "您确定要执行此操作吗?")
+     * @param {string} options.confirmText - 确认按钮文本 (默认: "确认")
+     * @param {string} options.cancelText - 取消按钮文本 (默认: "取消")
+     * @returns {Promise<boolean>} - Promise对象,解析为true表示确认,false表示取消
+     */
+    showConfirm({ title = "确认操作", content = "您确定要执行此操作吗?", confirmText = "确认", cancelText = "取消" } = {}) {
+        return new Promise((resolve) => {
+            // 获取元素引用
+            const overlay = this.shadowRoot.querySelector('.popup-overlay');
+            const titleEl = this.shadowRoot.querySelector('.popup-content .title');
+            const contentEl = this.shadowRoot.querySelector('.popup-content .content');
+            const confirmBtn = this.shadowRoot.querySelector('.popup-confirm-btn');
+            const cancelBtn = this.shadowRoot.querySelector('.popup-cancel-btn');
+
+            // 更新内容
+            titleEl.innerHTML = title;
+            contentEl.innerHTML = content;
+            confirmBtn.textContent = confirmText;
+            cancelBtn.textContent = cancelText;
+
+            // 显示对话框
+            overlay.classList.remove('hide');
+
+            // 处理按钮点击
+            const handleConfirm = () => {
+                overlay.classList.add('hide');
+                cleanupEventListeners();
+                resolve(true);
+            };
+
+            const handleCancel = () => {
+                overlay.classList.add('hide');
+                cleanupEventListeners();
+                resolve(false);
+            };
+
+            // 处理蒙层点击(点击外部关闭对话框)
+            const handleOverlayClick = (event) => {
+                if (event.target === overlay) {
+                    handleCancel();
+                }
+            };
+
+            // 添加事件监听器
+            confirmBtn.addEventListener('click', handleConfirm);
+            cancelBtn.addEventListener('click', handleCancel);
+            overlay.addEventListener('click', handleOverlayClick);
+
+            // 清理事件监听器
+            const cleanupEventListeners = () => {
+                confirmBtn.removeEventListener('click', handleConfirm);
+                cancelBtn.removeEventListener('click', handleCancel);
+                overlay.removeEventListener('click', handleOverlayClick);
+            };
+        });
+    }
+
+}
+
+export default ChatConfirmDialog;

+ 294 - 0
src/components/jb-ai-chat/chat-message-panel.js

@@ -0,0 +1,294 @@
+import {
+    BaseElement,
+    ChatStatus
+} from "./base.js";
+
+
+// 定义消息组件
+class ChatMessagePanel extends BaseElement {
+    constructor() {
+        super();
+        this.attachShadow({
+            mode: 'open'
+        });
+        this.isLoadingHistory = false;
+    }
+
+    /**
+     * 面板
+     * @type {null}
+     */
+    panel = null;
+
+    /**
+     * 容器
+     * @type {null}
+     */
+    container = null;
+
+    connectedCallback() {
+        this.render();
+        this.setupScrollListener();
+    }
+
+    setupScrollListener() {
+        this.container.addEventListener('scroll', () => {
+            if (this.container.scrollTop === 0 && !this.isLoadingHistory) {
+                // Get the first message's ID if exists
+                const firstMsg = this.panel.firstElementChild;
+                const lastId = firstMsg ? firstMsg.id : null;
+                const oldScrollHeight = this.container.scrollHeight;
+                // Trigger history load
+
+                console.log('id', lastId)
+                this.chatInstance?.loadHistoryData(lastId).then(() => {
+                    const scrollDiff = this.container.scrollHeight - oldScrollHeight;
+                    this.container.scrollTop = scrollDiff;
+                }).catch(() => {
+                });
+
+            }
+        });
+    }
+
+    onReceive(key, value) {
+        switch (key) {
+            case "chatInstance":
+                this.chatInstance.addChatMessageListener((msg) => {
+                    this.processMsg(msg);
+                })
+        }
+    }
+
+    processMsg(msg) {
+        switch (msg.action) {
+            case 'request':
+                //发送消息
+                this.addMessage(msg).notifyFinish()
+                if (msg.data.role == "user") {
+                    this.addMessage({
+                        ...msg,
+                        data: {
+                            role: 'assistant',
+                            content: {
+                                text: ''
+                            }
+                        }
+                    })
+                }
+
+                break;
+            case 'response':
+            case 'reference': //添加引用
+            case "error":
+                let dom = this.findMessageDom(msg);
+                if (dom && !dom.finish) {
+                    dom.render(msg);
+                }
+                //更新一条消息
+                break;
+            case 'over': {
+                let dom = this.findMessageDom(msg);
+                //结束对话
+                if (dom != null && !dom.finish) {
+                    dom.notifyFinish();
+                }
+                break;
+            }
+            case 'insert':
+                this.addHistoryMessage(msg);
+                break;
+
+
+            case 'syncData': {
+                let dom = this.findMessageDom(msg);
+                //结束对话
+                if (dom != null) {
+                    dom.syncDbData(msg.data.response)
+                    dom.previousElementSibling.syncDbData(msg.data.request);
+                }
+
+                break;
+            }
+
+
+            case "regenerate": {
+                const dom = this.findMessageDom(msg);
+                //获取它的前一个元素
+                const after = dom.nextElementSibling;
+                dom.remove();
+                this.addMessage({
+                    ...msg,
+                    data: {
+                        role: 'assistant',
+                        content: {
+                            text: ''
+                        }
+                    }
+                }, after)
+            }
+
+
+        }
+    }
+
+
+    findMessageDom(msg) {
+        //更新一条消息
+        let msgs = this.panel.childNodes;
+        if (!msgs?.length) return;
+        for (let i = msgs.length - 1; i >= 0; i--) {
+            let m = msgs[i];
+            if (m.flag == msg.flag && (!msg.data?.role || m.role == msg.data.role)) {
+                //over状态没有data,所以,只需要判断flag即可
+                return m;
+            }
+        }
+        return null;
+    }
+
+
+    deleteMessage(flag) {
+        let msgs = this.panel.childNodes;
+        if (!msgs?.length) return;
+        for (let i = msgs.length - 1; i >= 0; i--) {
+            let m = msgs[i];
+            if (m.flag == flag) {
+                //over状态没有data,所以,只需要判断flag即可
+                m.remove();
+            }
+        }
+        if (!this.panel.childNodes?.length) {
+            this.noMsgTip.classList.remove("hide")
+        }
+    }
+
+    /**
+     * 添加一条消息
+     * @param msg
+     * @param next 插入到哪个元素之前
+     */
+    addMessage(msg, next) {
+        //动态构建一个 chatmessage组件并插入到container中
+        this.noMsgTip.classList.add("hide")
+        const el = document.createElement("chat-message");
+        if (next) {
+            this.panel.insertBefore(el, next);
+            setTimeout(() => {
+                el.scrollIntoView({
+                    behavior: 'smooth', // 平滑滚动
+                    block: 'start'     // 元素的顶部与视口的顶部对齐
+                });
+            }, 0)
+
+        } else {
+            this.panel.appendChild(el);
+            // 平滑滚动到底部
+            this.scrollToBottom();
+
+        }
+        //要先插入到页面,才能updateContent
+        el.render(msg);
+        return el;
+
+    }
+
+    addHistoryMessage(msg) {
+        const el = document.createElement("chat-message");
+        this.noMsgTip.classList.add("hide")
+
+        // Insert at the beginning for history messages
+        if (this.panel.firstChild) {
+            this.panel.insertBefore(el, this.panel.firstChild);
+        } else {
+            this.panel.appendChild(el);
+        }
+
+        el.render(msg);
+        el.notifyFinish();
+    }
+
+    scrollToBottom() {
+        setTimeout(() => {
+            this.container.scrollTo({
+                top: this.container.scrollHeight,
+                behavior: 'smooth'
+            })
+        }, 20);
+    }
+
+    clearAllMessages() {
+        this.panel.innerHTML = "";
+    }
+
+
+    render() {
+        this.shadowRoot.innerHTML = `
+        <style>
+            :host {
+                flex: 1;
+                overflow: hidden  !important;
+                position: relative  !important;
+                display: flex  !important;
+                flex-direction: column  !important;
+                position: relative;
+            }
+            .msg-panel {
+                flex:1;
+                overflow-y: auto;
+                padding: 1rem;
+            }
+            .tip {
+                position: absolute;
+                width:100%;
+                left:0;
+                bottom: 0;
+            }
+
+            .msg-panel::-webkit-scrollbar {
+                width: 4px !important; /* 滚动条宽度 */
+            }
+
+            .msg-panel::-webkit-scrollbar-track {
+                background: rgba(241,241,241,0.7) !important; /* 滚动条轨道背景颜色 */
+            }
+
+            .msg-panel::-webkit-scrollbar-thumb {
+                background: rgba(136,136,136,0.15) !important; /* 滚动条滑块颜色 */
+                border-radius: 4px !important; /* 滚动条滑块圆角 */
+            }
+            .msg-panel::-webkit-scrollbar-thumb:hover {
+                background: rgba(85,85,85,0.33) !important; /* 鼠标悬停时滚动条滑块颜色 */
+            }
+            .msg-panel::-webkit-scrollbar-button {
+               display: none !important; /* 隐藏滚动条箭头 */
+            }
+            .nomsg_tip {
+                display: flex;
+                height: 100%;
+                justify-content: center;
+                align-items: center;
+                font-size: 40px;
+                color: #ceddf4;
+            }
+            .hide {
+                display: none;
+            }
+
+        </style>
+        <div class="msg-panel">
+            <div id="panel"></div>
+            <div class="nomsg_tip">
+                <svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" fill="#ceddf4" viewBox="0 0 512 512"><path d="M88 0C74.7 0 64 10.7 64 24c0 38.9 23.4 59.4 39.1 73.1l1.1 1C120.5 112.3 128 119.9 128 136c0 13.3 10.7 24 24 24s24-10.7 24-24c0-38.9-23.4-59.4-39.1-73.1l-1.1-1C119.5 47.7 112 40.1 112 24c0-13.3-10.7-24-24-24zM32 192c-17.7 0-32 14.3-32 32L0 416c0 53 43 96 96 96l192 0c53 0 96-43 96-96l16 0c61.9 0 112-50.1 112-112s-50.1-112-112-112l-48 0L32 192zm352 64l16 0c26.5 0 48 21.5 48 48s-21.5 48-48 48l-16 0 0-96zM224 24c0-13.3-10.7-24-24-24s-24 10.7-24 24c0 38.9 23.4 59.4 39.1 73.1l1.1 1C232.5 112.3 240 119.9 240 136c0 13.3 10.7 24 24 24s24-10.7 24-24c0-38.9-23.4-59.4-39.1-73.1l-1.1-1C231.5 47.7 224 40.1 224 24z"/></svg>
+                <span style="margin-left: 4px;margin-top:10px">开始对话</span>
+            </div>
+        </div>
+        <chat-tip class="tip"></chat-tip>
+    `
+        this.container = this.shadowRoot.querySelector(".msg-panel");
+        this.panel = this.shadowRoot.querySelector('#panel');
+        this.noMsgTip = this.shadowRoot.querySelector('.nomsg_tip');
+    }
+}
+
+export default ChatMessagePanel;

+ 715 - 0
src/components/jb-ai-chat/chat-message.js

@@ -0,0 +1,715 @@
+// 定义消息组件
+import {
+    BaseElement, ChatStatus, baseStyle
+} from "./base.js";
+
+
+class ChatMessage extends BaseElement {
+    constructor() {
+        super();
+        this.attachShadow({
+            mode: 'open'
+        });
+        this.shadowRoot.adoptedStyleSheets = [baseStyle];
+    }
+
+    connectedCallback() {
+    }
+
+    // 组件销毁时清理定时器
+    disconnectedCallback() {
+        if (this.mdRenderTimer) {
+            clearTimeout(this.mdRenderTimer);
+            this.mdRenderTimer = null;
+        }
+        if (this.htmlRenderTimer) {
+            clearTimeout(this.htmlRenderTimer);
+            this.htmlRenderTimer = null;
+        }
+    }
+
+    container = null; //消息内容dom
+    flag = null; //消息标志,也会作为事件的id
+    id = null;
+    role = null; //消息角色
+    thinkingShown = true; //是否显示思考中的提示
+    finish = false; //是否完成
+
+    hasRendered = false;
+    renderTimer = null;
+    msgCache = [];
+    fragment = null; //markdown渲染用的片段
+    refers = null;
+    refersExpand = false; //是否展开显示参考信息
+
+
+
+    /**
+     * 当前正在渲染的类型
+     * @type {null}
+     */
+    renderingType = null;
+
+    lastRenderTime = Date.now();
+
+    /**
+     * 当前正在渲染的容器
+     * @type {null}
+     */
+    renderingContainer = null;
+
+    renderingContent = "";
+
+    /**
+     * 通知消息完成
+     */
+    notifyFinish() {
+        this.finish = true;
+        if (this.messageActions) {
+            this.messageActions.style.display='block'
+            this.shadowRoot.querySelector(".msg-tips-box").classList.remove("hide")
+        }
+        this.scrollToBottom();
+    }
+
+    /**
+     * 同步数据库数据相关信息
+     * @param data
+     */
+    syncDbData(data) {
+        this.id = data.id;
+        this.shadowRoot.querySelector('#time').textContent = data.createTime.substring(0, 16);
+        console.debug("同步后端数据", this.id)
+    }
+
+    /**
+     * 生成操作按钮HTML
+     */
+    generateActionButtons() {
+        if (this.role !== 'assistant') return '';
+
+        return `
+            <div class="message-actions" style="display: none">
+                <button class="message-action-btn copy" data-action="copy" title="复制消息">
+                    <svg  viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"  width="12" height="12">
+                        <path fill="#94a3b8" d="M480 0h248.2C753.6 0 778 10.2 796 28.2L931.8 164c18 18 28.2 42.4 28.2 67.8V672c0 53-43 96-96 96H480c-53 0-96-43-96-96V96c0-53 43-96 96-96zM160 256h160v128H192v512h384v-64h128v96c0 53-43 96-96 96H160c-53 0-96-43-96-96V352c0-53 43-96 96-96z" p-id="29254" ></path>
+                    </svg>
+                </button>
+                <button class="message-action-btn delete error" data-action="delete" title="删除消息">
+                    <svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="12" height="12">
+                        <path fill="#94a3b8" d="M857.2 167.6c22.4 0 41.2 3.6 55.6 11.2 14.8 7.6 26.4 16.8 34.8 28 8.8 11.2 14.8 23.2 18 36.4 3.2 13.2 5.2 25.2 5.2 36.4 0 5.2 0 9.2-0.4 12.4-0.4 2.8-0.4 5.6-0.4 8l0 6.8-74.4 0 0 587.2c0 14.8-2.8 29.2-9.2 43.2-6 13.6-14.8 26-26 36.4-11.2 10.4-24.8 18.8-41.2 25.2-16 6.4-34.8 9.6-55.6 9.6L262.4 1008.4c-19.6 0-38-2.8-55.2-8.8-17.2-6-32-14.4-44.4-25.2-12.4-10.8-22-24-29.2-39.2-7.2-15.2-10.8-32.8-10.8-52l0-576L52.8 307.2c-0.8-0.8-1.2-2.8-1.2-5.6-0.8-3.6-1.2-15.2-1.2-34.8 0-9.6 2.4-20.4 6.8-32.4 4.4-12 11.2-22.8 20.4-32.8 9.2-10 20.8-18.4 34.8-25.2 14.4-6.8 31.2-10 50.8-10l100 0L263.2 97.2c0-19.2 6.8-36 20.4-49.6 13.6-13.6 30-20.8 49.6-20.8l352 0c26.4 0 44.4 6.8 54.4 20.8 10 13.6 15.2 30.4 15.2 49.6l0 69.2c15.6 0.8 32.8 1.2 50.8 1.2L857.2 167.6 857.2 167.6 857.2 167.6zM333.2 167.6l352 0L685.2 97.2l-352 0L333.2 167.6 333.2 167.6zM298.4 875.6c24 0 36-15.2 36-46L334.4 310.8 264.4 310.8l0 518.8c0 15.6 2.4 27.2 7.2 34.8C276.8 871.6 285.6 875.6 298.4 875.6L298.4 875.6 298.4 875.6zM510.8 874.4c12.8 0 21.2-3.6 26-10.8 4.4-7.2 6.8-18.4 6.8-34L543.6 310.8l-69.6 0 0 518.8C473.6 859.6 486 874.4 510.8 874.4L510.8 874.4 510.8 874.4zM722.4 872c13.6 0 22.4-3.6 26.8-10.8 4.4-7.2 6.8-18.4 6.8-34L756 310.8l-70.8 0 0 516.4C685.2 857.2 697.6 872 722.4 872L722.4 872 722.4 872z"  p-id="31426"></path>
+                    </svg>
+                </button>
+                <button class="message-action-btn regenerate" data-action="regenerate" title="重新生成">
+                    <svg t="1740386672644" class="icon" viewBox="0 0 1211 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6334" width="12" height="12"><path d="M605.090909 930.909091c-144.616727 0-272.104727-73.309091-347.415273-184.738909L149.643636 746.170182C234.682182 911.173818 406.714182 1024 605.090909 1024c267.077818 0 486.4-204.520727 509.905455-465.454545L1210.181818 558.545455l-139.636364-186.181818-139.636364 186.181818 90.530909 0C998.306909 768 820.736 930.909091 605.090909 930.909091z" p-id="6335" fill="#94a3b8"></path><path d="M605.090909 0C354.117818 0 145.314909 180.596364 101.515636 418.909091L0 418.909091l139.636364 186.181818 139.636364-186.181818L196.561455 418.909091C238.871273 232.354909 405.736727 93.090909 605.090909 93.090909c125.998545 0 238.964364 55.621818 315.764364 143.639273l17.966545-4.608 95.045818 0C942.452364 92.346182 784.570182 0 605.090909 0z" p-id="6336" fill="#94a3b8"></path></svg>
+                </button>
+            </div>`;
+    }
+
+
+
+    /**
+     * 注册操作事件
+     */
+    registerActionEvents() {
+        const copyBtn = this.shadowRoot.querySelector(".message-action-btn.copy");
+        if (copyBtn) {
+            copyBtn.addEventListener('pointerdown', () => {
+                if (this.container) {
+                    const text = this.container.innerText;
+                    this.copyText(text)
+                }
+            });
+        }
+        const delBtn = this.shadowRoot.querySelector(".message-action-btn.delete");
+        if (delBtn) {
+            // 为每个按钮添加点击事件监听器
+            delBtn.addEventListener('pointerdown', () => {
+                // console.debug("开始删除", this.flag, this.id);
+                // this.chatInstance.deleteMsg(this.flag, this.id)
+                // 显示模态框
+                const confirmationModal = this.shadowRoot.querySelector('#confirmationModal');
+                if (confirmationModal) { // 确保模态框存在
+                    confirmationModal.style.display = 'block';
+                }
+
+                // 获取确认按钮并添加事件监听器
+                const confirmDeleteBtn = this.shadowRoot.querySelector('#confirmDelete');
+                if (confirmDeleteBtn) { // 确保按钮存在
+                    confirmDeleteBtn.onclick = () => {
+                        console.log('执行删除操作...');
+                        // 执行删除操作后,隐藏模态框
+                        if (confirmationModal) {
+                            confirmationModal.style.display = 'none';
+                        }
+                        // 删除逻辑
+                        console.debug("开始删除", this.flag, this.id);
+                        this.chatInstance.deleteMsg(this.flag, this.id);
+                    };
+                }
+
+                // 获取取消按钮并添加事件监听器
+                const cancelDeleteBtn = this.shadowRoot.querySelector('#cancelDelete');
+                if (cancelDeleteBtn) { // 确保按钮存在
+                    cancelDeleteBtn.onclick = () => {
+                        // 隐藏模态框
+                        if (confirmationModal) {
+                            confirmationModal.style.display = 'none';
+                        }
+                    };
+                }
+            });
+        }
+        const againBtn = this.shadowRoot.querySelector(".message-action-btn.regenerate");
+        if (againBtn) {
+            againBtn.addEventListener('pointerdown', () => {
+                this.chatInstance.regenerate(this.flag, this.id);
+            });
+        }
+
+        const refersHeader = this.shadowRoot.querySelector(".refers_header");
+        refersHeader.addEventListener('pointerdown', () => {
+            if (this.refersExpand) {
+                this.refers.classList.remove("expand");
+            } else {
+                this.refers.classList.add("expand");
+            }
+            this.refersExpand = !this.refersExpand;
+        })
+
+        this.registerResizeEvent();
+
+
+    }
+
+    /**
+     * iframe窗口可以调整大小
+     */
+    registerResizeEvent() {
+        const resizer = this.shadowRoot.querySelector(".resizer");
+        if (!resizer) {
+            return;
+        }
+        const curDocument=  this.shadowRoot;
+        let startX, startWidth;
+        let isResizing = false;
+        let enter = false;
+        const iframeContainer = this.shadowRoot.querySelector(".container_wrapper");
+        const fragment = this.shadowRoot.querySelector(".fragment");
+        let exitTimer = null;
+        //调整iframe窗口大小
+
+        resizer.addEventListener('mousedown', (e) => {
+            e.preventDefault();
+            isResizing = true;
+            enter = true;
+            startX = e.clientX;
+            startWidth = parseInt(getComputedStyle(iframeContainer).width, 10);
+
+            // 添加调整中的样式
+            resizer.classList.add('active');
+
+            // 添加鼠标移动和松开事件
+            curDocument.addEventListener('mousemove', resize);
+            curDocument.addEventListener('mouseup', stopResize);
+        });
+
+        function resize(e) {
+            if (!isResizing) return;
+
+            const newWidth = startWidth + e.clientX - startX;
+            // 设置最小宽度限制
+            if (newWidth > 100) {
+                iframeContainer.style.width = newWidth + 'px';
+            }
+        }
+        // 停止调整大小
+        function stopResize() {
+            isResizing = false;
+            resizer.classList.remove('active');
+
+            curDocument.removeEventListener('mousemove', resize);
+            curDocument.removeEventListener('mouseup', stopResize);
+            if (exitTimer) {
+                clearTimeout(exitTimer);
+                exitTimer = null;
+            }
+        }
+
+        iframeContainer.addEventListener('mouseleave', function() {
+            console.debug("离开")
+            if (isResizing && enter) {
+                enter = false;
+                exitTimer = setTimeout(() => {
+                    if (!enter) stopResize();
+                }, 500)
+
+            }
+        });
+        iframeContainer.addEventListener('mouseenter', function() {
+            enter = true;
+            if (exitTimer) {
+                clearTimeout(exitTimer);
+                exitTimer = null;
+            }
+        })
+        fragment.addEventListener("mouseenter", function (e) {
+            e.stopPropagation();
+            if (isResizing && enter) {
+                enter = false;
+                exitTimer = setTimeout(() => {
+                    if (!enter) stopResize();
+                }, 500)
+
+            }
+        })
+    }
+
+    copyText(text) {
+        // 获取要复制的文本
+
+
+        // 优先使用 Clipboard API
+        if (navigator.clipboard && window.isSecureContext) {
+            navigator.clipboard.writeText(text)
+                .then(() => {
+                    console.debug("文本复制成功", text)
+                    this.showNotification();
+
+                })
+                .catch(() => this.fallbackCopy(text));
+        } else {
+            this.fallbackCopy(text);
+        }
+    }
+
+    // 显示复制成功的提示
+    showNotification() {
+        const notif = this.shadowRoot.querySelector('#notif');
+        if (notif) { // 确保模态框存在
+            notif.style.display = 'block';
+        }
+        setTimeout(function () {
+            notif.style.display = 'none';
+        }, 3000); // 显示3秒后消失
+    }
+
+    fallbackCopy(text) {
+        // 创建临时文本区域
+        const textArea = document.createElement('textarea');
+        textArea.value = text;
+
+        // 设置样式使其不可见
+        textArea.style.cssText = `
+                position: fixed;
+                top: -9999px;
+                left: -9999px;
+                width: 2em;
+                height: 2em;
+                padding: 0;
+                border: none;
+                outline: none;
+                box-shadow: none;
+                background: transparent;
+            `;
+
+        this.shadowRoot.appendChild(textArea);
+
+        // 检查是否是 iOS 设备
+        const isIos = navigator.userAgent.match(/ipad|iphone/i);
+
+        if (isIos) {
+            // iOS 设备特殊处理
+            const range = document.createRange();
+            range.selectNodeContents(textArea);
+            const selection = window.getSelection();
+            selection.removeAllRanges();
+            selection.addRange(range);
+            textArea.setSelectionRange(0, textArea.value.length);
+        } else {
+            // 其他设备
+            textArea.select();
+        }
+
+        try {
+            const successful = document.execCommand('copy');
+            if (successful) {
+                console.debug("文本复制成功", text)
+                this.showNotification();
+
+            } else {
+                throw new Error('复制命令执行失败');
+            }
+        } catch (err) {
+            console.error('复制失败:', err);
+        }
+
+        this.shadowRoot.removeChild(textArea);
+    }
+
+    initHtmlRender() {
+        return new Promise((resolve, reject) => {
+            const iframe = document.createElement('iframe');
+            // iframe.style.width = '100vw';
+            iframe.sandbox = 'allow-scripts allow-same-origin allow-popups allow-forms';
+            iframe.onload = () => {
+                const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
+
+                // 创建基本HTML结构
+                iframeDoc.open();
+                iframeDoc.write(`
+                    <!DOCTYPE html>
+                    <html>
+                        <head>
+                        <script src="/assets/js/tailwind.js"></script>
+                        <link rel="stylesheet" href="/assets/fa/css/all.min.css">
+                        </head>
+                        <body style=""></body>
+                    </html>
+                `);
+                iframeDoc.close();
+                // 使用轮询方式检查DOM是否准备好
+                const checkBody = () => {
+                    if (iframeDoc.body) {
+                        this.renderingContainer = iframeDoc.body;
+                        resolve()
+                        const resizeRo = new ResizeObserver((entries) => {
+                            let entry = entries[0];
+                            let height = entry.target.scrollHeight;
+                            // let width = entry.target.scrollWidth;
+                            iframe.style.height = height + 'px';
+                            // iframe.style.width = width + 'px';
+                        });
+                        // 开始监控iframe的body元素:
+                        resizeRo.observe(iframeDoc.body);
+                    } else {
+                        setTimeout(checkBody, 5); // 每10ms检查一次
+                    }
+                };
+                // 开始检查
+                checkBody();
+            };
+            // 立即触发加载
+            iframe.src = 'about:blank';
+            this.fragment.appendChild(iframe);
+        })
+
+    }
+
+    async renderMsg() {
+
+
+        let msg = this.msgCache.shift();
+
+        if ((!msg && this.finish) || (msg && msg.renderType != this.renderingType && this.renderingType && this.renderingContent)) {
+            //没消息或者这次的消息和上次不一样,都需要把之前遗留的处理一下
+            switch (this.renderingType) {
+                case 'text':
+                    this.renderingContainer.innerText += this.renderingContent;
+                    break;
+                case "markdown":
+                    this.renderingContainer.innerHTML = this.chatInstance.config.onMarkdownRender(this.renderingContent);
+                    break;
+                case "html":
+                default:
+                    this.renderHtmlMsg(false);
+            }
+        }
+
+        if (!msg && this.finish) {//没消息了也结束了
+            this.renderingType = null;
+            this.renderingContent = null;
+            this.renderingContainer = null;
+            return
+        };
+        if (!msg) { //没消息了继续等待
+            setTimeout(() => {
+                this.renderMsg()
+            }, 20)
+            return;
+        }
+
+        if (msg.renderType != this.renderingType) {
+
+            if (msg.renderType == 'markdown' && !this.chatInstance.config.onMarkdownRender) {
+                console.warn("未配置markdown渲染函数,无法渲染markdown消息")
+                return;
+            }
+            this.renderingType = msg.renderType || 'text';
+            this.renderingContent = "";
+            if (this.renderingType == 'html') {
+                await this.initHtmlRender();
+            } else {
+                this.renderingContainer = document.createElement('div');
+                this.renderingContainer.classList.add(`md-${msg.role ?? 'assistant'}`);
+                this.fragment.appendChild(this.renderingContainer);
+            }
+
+        }
+
+        this.renderingContent += msg.content;
+
+        if (this.lastRenderTime + 20 < Date.now() && this.renderingContainer && this.renderingContent) {
+            switch (this.renderingType) {
+                case 'text':
+                    this.renderingContainer.innerText += this.renderingContent;
+                    this.renderingContent = '';
+                    break;
+                case "markdown":
+                    this.renderingContainer.innerHTML = this.chatInstance.config.onMarkdownRender(this.renderingContent);
+                    break;
+                case "html":
+                default:
+                    this.renderHtmlMsg();
+            }
+            this.lastRenderTime = Date.now();
+            this.scrollToBottom();
+        }
+
+        setTimeout(() => {
+            this.renderMsg();
+        }, 0)
+
+
+    }
+
+    renderHtmlMsg(ignoreScript=true) {
+        const div = document.createElement("div");
+        div.innerHTML = this.renderingContent;
+        //移除div里的script标签
+        const scripts = div.querySelectorAll("script");
+        if (ignoreScript) {
+            scripts.forEach(script => script.remove());
+        } else {
+            scripts.forEach(originalScript => {
+                // 在iframe中创建新的script元素
+                const newScript = this.renderingContainer.ownerDocument.createElement('script');
+
+                // 复制原始脚本的所有属性
+                Array.from(originalScript.attributes).forEach(attr => {
+                    newScript.setAttribute(attr.name, attr.value);
+                });
+
+                // 处理内联脚本(没有src属性的脚本)
+                if (!originalScript.src && originalScript.textContent) {
+                    newScript.textContent = originalScript.textContent;
+                }
+                // 将新脚本添加到iframe文档中
+                this.renderingContainer.appendChild(newScript);
+            });
+            this.renderingContainer?.ownerDocument?.dispatchEvent(new Event('DOMContentLoaded'));
+        }
+        this.renderingContainer.innerHTML = div.innerHTML;
+        this.renderingContainer.parentElement.scrollTo({
+            top:this.renderingContainer.parentElement.scrollHeight,
+            behavior: 'smooth'
+        });
+    }
+
+
+    scrollToBottom() {
+        this.getRootNode().host.scrollToBottom();
+    }
+
+    toJson(str) {
+        try {
+            const result = JSON.parse(str);
+
+            // 额外检查:确保解析结果是对象或数组
+            // 注意:null 也会被 JSON.parse 成功解析,但 typeof null 返回 'object'
+            const type = typeof result;
+            if( (type === 'object' && result !== null) || Array.isArray(result)) {
+                return result;
+            }
+            return false;
+
+        } catch (e) {
+            return false;
+        }
+    }
+
+    appendMsg(msg) {
+        if (!msg.data?.content?.text) {
+            return;
+        }
+        const text = msg.data.content.text;
+        const validJson = this.toJson(text);
+        if (validJson && validJson[0].renderType) {
+            //新的消息结构
+            const msgs = JSON.parse(text);
+            for (let item of msgs) {
+                item.role = msg.data.role;
+                this.msgCache.push(item)
+            }
+        } else {
+            //历史版本的消息结构,未使用json结构 或者 后端正在动态输入的消息
+            this.msgCache.push({
+                renderType: msg.data.renderType ?? 'text',
+                content: msg.data.content.text,
+                role: msg.data.role
+            })
+
+        }
+
+
+    }
+
+    renderReference(refs) {
+        if (!refs || !refs.length) {
+            return;
+        }
+        this.refers.classList.remove('hide');
+        this.refers.querySelector(".refers_count").innerText=`-${refs.length}`
+        refs.forEach(item => {
+            const reference = document.createElement('reference-item');
+            reference.setAttribute('type', item.type);
+            reference.setAttribute('content', item.content);
+            reference.setAttribute('url', item.url);
+            reference.setAttribute('file-name', item.fileName);
+            reference.setAttribute('file-id', item.fileId);
+            this.refers.querySelector('.refers_list').appendChild(reference);
+        })
+    }
+
+
+
+    render(msg) {
+        this.id = (this.id || msg.id) || null;
+        if (this.hasRendered) {
+            //追加渲染
+            if (this.thinkingShown) {
+                // 如果已经有内容,移除思考中的提示
+                const thinking = this.container.querySelector('.thinking');
+                if (thinking) {
+                    thinking.remove();
+                }
+                this.thinkingShown = false;
+            }
+            // 文本内容
+            this.appendMsg(msg)
+            if (msg.data?.msg) {
+                const msgTip = document.createElement('msg-tip');
+                msgTip.setAttribute('type', msg.action ?? 'error');
+                msgTip.textContent = msg.data.msg;
+                this.container.appendChild(msgTip);
+            }
+            if (msg.action == 'reference' && msg.data) {
+                // 参考资料
+                //判断msg.data是个对象还是数组
+                let list = Array.isArray(msg.data) ? msg.data : [msg.data];
+                this.renderReference(list)
+            }
+            if (msg.data.reference) {
+                this.renderReference(msg.data.reference)
+            }
+            return;
+        }
+        this.hasRendered = true;
+
+        //判断消息是否有内容
+        const msgIsNotEmpty = !!(msg.data?.content?.text) || !!(msg.data?.errorMsg);
+        this.flag = msg.flag;
+        this.role = msg.data.role;
+
+        const role = this.role;
+        const showAvatar = this.chatInstance.config.showAvatar;
+        const avatar = this.role == 'user' ? this.chatInstance.config.userAvatar : this.chatInstance.config.assistantAvatar;
+        this.shadowRoot.innerHTML = `
+        <style>
+            :host {
+                display: block;
+                margin-bottom: 1rem;
+                position: relative;
+                font-size: 16px;
+            }
+            .logo.custom-avatar {
+                background-image: var(--avatar-url);
+                background-size: cover;
+                background-position: center;
+                background-repeat: no-repeat;
+            }
+
+            .fragment {
+                position: relative;
+                overflow: hidden;
+            }
+        </style>
+        <div class="ja_msg ${role}">
+            ${(role == 'assistant' && showAvatar) ? `<div class="logo ${role}${avatar ? ' custom-avatar' : ''}" ${avatar ? `style="--avatar-url: url('${avatar}')"` : ''}></div>` : ''}
+            <div class="container_wrapper ${role}">
+                <div class="container">
+                    ${msgIsNotEmpty ? '' : `
+                        <div class="thinking">
+                            <span>AI正在思考中</span>
+                            <div class="dots">
+                                <span></span>
+                                <span></span>
+                                <span></span>
+                            </div>
+                        </div>
+                    `}
+                    <div class="fragment"></div>
+                </div>
+                <div class="msg-tips-${role}">
+                     <div class="msg-tips-box hide">
+                           <div id="time">${msg.createTime?.substring(0, 16)}</div>
+                           <div>
+                              AI生成,仅供参考
+                           </div>
+                     </div>
+                </div>
+
+                ${role == 'assistant' ? `
+                <div style="margin-top:0.375rem">
+                       <!-- 自定义删除对话框 -->
+                   <div id="confirmationModal" class="modal">
+                        <div class="modal-content">
+                            <p>
+                            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#F0A122FF" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2m1 15h-2v-2h2zm0-4h-2V7h2z"/></svg>
+                            &nbsp;确认删除该记录?</p>
+                            <button id="confirmDelete">确定</button>
+                            <button id="cancelDelete">取消</button>
+                        </div>
+                    </div>
+                    <!-- 自定义复制成功提示框-->
+                    <div id="notif" class="notification">
+                         <div class="notif-box">
+                                <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="-2 -2 24 24"><path fill="rgb(24 160 88)" d="M10 20C4.477 20 0 15.523 0 10S4.477 0 10 0s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m0-6.172l3.536-3.535a1 1 0 0 1 1.414 1.414l-4.243 4.243a1 1 0 0 1-1.414 0L5.05 9.707a1 1 0 0 1 1.414-1.414z" strokeWidth={2} stroke="rgb(24 160 88)"></path></svg>
+                                </jb-icon>&nbsp;&nbsp;复制成功!
+                         </div>
+                    </div>
+                    ${this.generateActionButtons()}
+                </div>
+
+                ` : ''}
+
+                <div class="refers hide">
+                    <div class="refers_header">
+                        <span class="refers_label">参考资料</span> <span class="refers_count">- 5</span>
+                        <svg fill="#6B7280" class="refers_btn" viewBox="0 0 1394 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2619" width="10" height="10"><path d="M808.665066 968.123525a139.174837 139.174837 0 0 1-222.989114 0L28.061551 224.448838A140.525626 140.525626 0 0 1 0 140.133462C0 62.746326 62.484883 0 139.567002 0h1115.228801c30.283817 0 59.739731 9.891261 83.944998 28.192273 61.569833 46.558646 73.901229 134.425289 27.538666 196.256565L808.665066 968.123525z"  p-id="2620"></path></svg>
+                    </div>
+                    <div class="refers_list"></div>
+                </div>
+                ${role == 'assistant'?`<div class="resizer"></div>`:''}
+            </div>
+            ${(role == 'user' && showAvatar) ? `<div class="logo ${role}${avatar ? ' custom-avatar' : ''}" ${avatar ? `style="--avatar-url: url('${avatar}');background-color:transparent  !important"` : ''}></div>` : ''}
+        </div>
+    `;
+        this.container = this.shadowRoot.querySelector('.container');
+        this.fragment = this.container.querySelector('.fragment');
+        this.refers = this.shadowRoot.querySelector('.refers');
+        this.messageActions = this.shadowRoot.querySelector(".message-actions");
+        this.appendMsg(msg)
+        this.renderMsg();
+        if (msg.data.reference) {
+            this.renderReference(msg.data.reference)
+        }
+
+        this.registerActionEvents()
+
+
+    }
+
+}
+
+export default ChatMessage

File diff suppressed because it is too large
+ 250 - 0
src/components/jb-ai-chat/chat-opt-panel.js


+ 70 - 0
src/components/jb-ai-chat/chat-tip.js

@@ -0,0 +1,70 @@
+// 定义消息组件
+import {BaseElement, baseStyle, ChatStatus} from "./base.js";
+
+class ChatTip extends BaseElement {
+    constructor() {
+        super();
+        this.attachShadow({ mode: 'open' });
+        this.shadowRoot.adoptedStyleSheets = [baseStyle];
+    }
+
+    tip = null;
+    content = '';
+    connectedCallback() {
+        this.render();
+    }
+
+    onReceive(key, value) {
+        switch (key) {
+            case "chatInstance":
+                this.chatInstance.addChatStatusListener((status) => {
+                    this.changeState(status);
+                })
+        }
+    }
+
+    changeState(status) {
+        if (!this.tip) return;
+
+        switch (status) {
+            case ChatStatus.IS_READY:
+            case ChatStatus.IS_RECEIVING:
+                this.tip.classList.remove('show');
+                break;
+            case ChatStatus.DISCONNECTED:
+                this.tip.classList.add('show');
+                this.content.innerHTML = `
+                <div class="tip-content-wrapper">
+                    <span class="text">网络异常,尝试连接中</span>
+                    <span class="dots"><span>.</span><span>.</span><span>.</span></span>
+                </div>
+            `;
+                break;
+            case ChatStatus.IS_WAITING:
+                this.tip.classList.add('show');
+                this.content.innerHTML = `
+                <div class="loader"></div>
+            `;
+                break;
+        }
+    }
+
+    render() {
+        this.shadowRoot.innerHTML = `
+        <style>
+            :host {
+                overflow-y: auto;
+                padding: 1rem !important;
+            }
+
+        </style>
+        <div class="ja tip">
+            <div class="tip-content"></div>
+        </div>
+    `;
+        this.tip = this.shadowRoot.querySelector('.tip');
+        this.content = this.shadowRoot.querySelector(".tip-content");
+    }
+}
+
+export default ChatTip;

+ 34 - 0
src/components/jb-ai-chat/icons.js

@@ -0,0 +1,34 @@
+export const Icons = {
+    /**添加图标 */
+    ADD: 'akar-icons:plus',
+    /** 编辑图标 */
+    EDIT: 'ant-design:edit-filled',
+    /** 删除图标 */
+    DELETE: 'fluent:delete-28-filled',
+    /** 移除图标 */
+    REMOVE: 'ep:remove-filled',
+    /** 查询图标 */
+    SEARCH: 'ant-design:search-outlined',
+    /** 重置图标 */
+    RESET: 'system-uicons:reset',
+    /** 刷新图标 */
+    REFRESH: 'solar:refresh-bold',
+    /** 拖拽排序 */
+    DRAG: 'streamline:one-finger-drag-vertical',
+    /** 设置图标 */
+    SETTING: 'ant-design:setting-filled',
+    /** 用户图标 */
+    USERS: 'ph:users-fill',
+    /** 展开图标 */
+    EXPAND: 'iconoir:expand-lines',
+    /** 收起图标 */
+    SHRINK: 'tdesign:shrink-vertical',
+    /** 排序图标 */
+    SORT: 'icon-park-solid:sort',
+    /** 查看图标 */
+    EYE: 'mdi:eye',
+    /** 全屏图标 */
+    FULLSCREEN: 'icon-park-outline:off-screen',
+    /** 下载图标 */
+    DOWNLOAD: 'material-symbols:cloud-download'
+}

+ 632 - 0
src/components/jb-ai-chat/index.js

@@ -0,0 +1,632 @@
+// 定义消息组件
+// 定义消息组件
+import {BaseElement, ChatStatus} from "./base.js";
+import ChatMessage from './chat-message.js'
+import ChatOptPanel from './chat-opt-panel.js'
+import ChatTip from './chat-tip.js'
+import ChatConfirmDialog from './chat-confirm-dialog.js'
+
+import ChatMessagePanel from './chat-message-panel.js'
+import ReferenceItem from './reference-item.js'
+import MsgTip from './msg-tip.js'
+
+
+// 注册Web Components
+customElements.define('chat-message', ChatMessage);
+customElements.define('chat-message-panel', ChatMessagePanel);
+customElements.define('chat-tip', ChatTip);
+customElements.define('msg-tip', MsgTip);
+customElements.define('reference-item', ReferenceItem);
+customElements.define('chat-opt-panel', ChatOptPanel);
+customElements.define("chat-confirm-dialog", ChatConfirmDialog)
+
+/**
+ * AI聊天配置类
+ */
+class AiChatConfig {
+
+    /**
+     * 是否显示头像
+     * @type {boolean}
+     */
+    showAvatar = true;
+
+    /**
+     * 用户头像
+     * @type {null}
+     */
+    userAvatar = null;
+    /**
+     * 机器人头像
+     * @type {null}
+     */
+    assistantAvatar = null;
+
+    /**
+     * 是否显示清空聊天记录按钮
+     * @type {boolean}
+     */
+    showClearMsgBtn = true;
+
+    /**
+     * 适配器
+     * @type {null}
+     */
+    adapter = null;
+
+    /**
+     * websocket地址
+     * @type {null}
+     */
+    websocketUrl = null;
+    /**
+     * websocket实例
+     * @type {null}
+     */
+    websocketInstance = null; // 新增:外部WebSocket实例
+
+    /**
+     * 全局参数
+     * @type {{}}
+     */
+    global = {}; //全局参数
+
+
+    /**
+     * chat实例
+     * @type {null}
+     */
+    aiChat = null;
+
+    /**
+     * 应用id
+     * @type {null}
+     */
+    app = null;
+
+    /**
+     * 会话id
+     * @type {null}
+     */
+    sessionId = null;
+
+    /**
+     * 上传文件回调
+     * @type {null}
+     */
+    onFileUpload = null;
+
+    /**
+     * markdown渲染回调
+     * @param {*} content 原文本
+     * @returns 渲染后的文本
+     */
+    onMarkdownRender = null;
+
+    /**
+     * 历史数据加载回调
+     * @param {string|null} lastId - 最后一条消息的ID,为null时表示首次加载
+     * @param {object} globalParams - 全局参数
+     * @returns {Promise<Array|false>} - 返回历史消息数组或false
+     */
+    onHistoryLoad = async function (lastId, globalParams) {
+        return false;
+    };
+
+    /**
+     * 点击设置按钮时的回调
+     * @type {null}
+     */
+    onSettingClick = null;
+
+    /**
+     * 消息接收时的回调
+     * @type {null}
+     */
+    onMsgReceive = null;
+
+    /**
+     * 消息发送时的回调
+     * @type {null}
+     */
+    onMsgSend = null;
+
+    /**
+     * 消息要删除时的回调
+     * @type {null}
+     */
+    onMsgToDelete = null;
+
+    /**
+     * 清空消息的回调
+     * @type {null}
+     */
+    onClearMsg = null;
+
+    /**
+     * 根据json设置自己的属性
+     * @param json
+     */
+    from(json) {
+        // console.log("AiChatConfig.from", json)
+        if (json) {
+            this.websocketUrl = json.websocketUrl;
+            this.app = json.app;
+            this.showAvatar = json.showAvatar??true;
+            this.userAvatar = json.userAvatar;
+            this.assistantAvatar = json.assistantAvatar;
+            this.showClearMsgBtn = json.showClearMsgBtn??true;
+            this.adapter = json.adapter;
+            this.sessionId = json.sessionId;
+            this.websocketInstance = json.websocketInstance;
+            this.onFileUpload = json.onFileUpload;
+            this.global = json.global ?? {};
+
+            if (typeof json.onHistoryLoad === 'function') {
+                this.onHistoryLoad = json.onHistoryLoad;
+
+            }
+
+            if (typeof json.onMarkdownRender === 'function') {
+                this.onMarkdownRender = json.onMarkdownRender;
+            }
+
+            if (typeof json.onSettingClick === 'function') {
+                this.onSettingClick = json.onSettingClick;
+            }
+            if (typeof json.onMsgReceive === 'function') {
+                this.onMsgReceive = json.onMsgReceive;
+            }
+            if (typeof json.onMsgSend === 'function') {
+                this.onMsgSend = json.onMsgSend;
+            }
+            if (typeof json.onMsgToDelete === 'function') {
+                this.onMsgToDelete = json.onMsgToDelete;
+            }
+            if (typeof json.onClearMsg === 'function') {
+                this.onClearMsg = json.onClearMsg;
+            }
+        }
+        return this;
+    }
+
+}
+
+
+
+
+/**
+ * AI聊天窗口类
+ */
+class AiChat {
+
+    /**
+     * 容器
+     * @type {null}
+     */
+    container = null;
+
+
+    /**
+     * 消息面板
+     * @type {null}
+     */
+    messagePanel = null;
+    /**
+     * 操作面板
+     * @type {null}
+     */
+    optPanel = null;
+
+    /**
+     * 确认对话框
+     * @type {null}
+     */
+    confirmDialog = null;
+
+    statusListener = []
+
+
+    status = ChatStatus.IS_WAITING;
+
+    /**
+     * 是否正在加载历史记录
+     * @type {boolean}
+     */
+    isLoadingHistory = false;
+
+
+    /**
+     * 正在处理的消息
+     * @type {null}
+     */
+    processingMsg = null;
+
+    /**
+     * 消息监听器
+     * @type {[]}
+     */
+    messageListener = []
+
+    /**
+     * websocket连接
+     * @type {{instance: null, isSelf: boolean}}
+     */
+    ws = {
+        instance: null,
+        isSelf: true, //是否是自己维护的链接
+    }
+
+    constructor(element, config) {
+        this.element = element;
+        this.config = new AiChatConfig().from(config);
+        this.config.aiChat = this;
+        this.onSocketOpened = this.onSocketOpened.bind(this);
+        this.onSocketClosed = this.onSocketClosed.bind(this);
+        this.onmessage = this.onmessage.bind(this);
+
+        this.init();
+    }
+
+    addChatStatusListener(listener) {
+        this.statusListener.push(listener);
+    }
+
+    /**
+     * 添加消息监听器
+     * @param listener
+     */
+    addChatMessageListener(listener) {
+        this.messageListener.push(listener);
+    }
+
+
+    /**
+     * 销毁实例
+     */
+    destroy() {
+        console.warn("AiChat实例开始销毁")
+        this.closeWebSocket();
+        this.element.removeChild(this.container)
+    }
+
+
+    init() {
+        if (this.element) {
+            this.element.innerHTML = `
+                <div class="ai-chat-container">
+                    <chat-message-panel ></chat-message-panel>
+                    <chat-opt-panel></chat-opt-panel>
+                </div>
+                <style>
+                    .ai-chat-container {
+                        height: 100%;
+                        background-image: linear-gradient(rgb(248, 250, 252), rgb(255, 255, 255));
+                        flex:1;
+                        overflow: hidden;
+                        display: flex;
+                        flex-direction: column;
+                        tab-size: 4;
+                        -webkit-text-size-adjust: 100%;
+                        font-feature-settings: normal;
+                        font-variation-settings: normal;
+                        -webkit-tap-highlight-color: transparent;
+                    }
+                    @media (width > 640px) {
+                        .ai-chat-container {
+                            font-size: 16px;
+                        }
+                    }
+                </style>
+            `;
+
+            this.container = this.element.querySelector('.ai-chat-container');
+            this.messagePanel = this.container.querySelector("chat-message-panel");
+            this.messagePanel.receive("chatInstance", this);
+            this.optPanel = this.container.querySelector("chat-opt-panel");
+            this.optPanel.receive("chatInstance", this);
+
+            //注册一个确认弹出框
+            this.confirmDialog = document.createElement("chat-confirm-dialog");
+            document.body.appendChild(this.confirmDialog);
+            this.loadHistoryData(null);
+        }
+
+        this.setupWebSocket();
+    }
+
+
+    isFirstLoadHistory = true;
+    loadHistoryData(startId) {
+        if (!this.config.onHistoryLoad || this.isLoadingHistory) return Promise.reject('没有更多数据了');
+        this.isLoadingHistory = true;
+        return new Promise((resolve, reject) => {
+            this.config.onHistoryLoad(startId, this.config.global).then((data) => {
+
+                if (!data) {
+                    reject('没有更多数据了');
+                    return;
+                }
+                this.isLoadingHistory = false;
+                data = data.reverse();
+                for (const msg of data) {
+                    this.handleMessage({ ...msg,  action: 'insert' });
+                }
+                if (this.isFirstLoadHistory) {
+                    this.isFirstLoadHistory = false;
+                    //滚动到底部
+                    setTimeout(() => {
+                        this.messagePanel.scrollToBottom();
+                    },20)
+
+                }
+                resolve();
+            })
+        })
+
+
+    }
+
+
+    closeWebSocket() {
+        if (this.ws.instance) {
+            // 清理旧的监听器
+            this.ws.instance.removeEventListener('message', this.onmessage);
+            this.ws.instance.removeEventListener('open', this.onSocketOpened);
+            this.ws.instance.removeEventListener('close', this.onSocketClosed);
+            if (this.ws.isSelf) {
+                console.warn("websocket连接即将被关闭...")
+                //自己的内部的socket链接
+                this.ws.instance.close();
+            }
+        }
+    }
+
+
+    setupWebSocket() {
+        this.closeWebSocket();
+        if (this.config.websocketInstance) {
+            // 使用外部WebSocket实例
+            this.ws.instance = this.config.websocketInstance;
+            this.ws.instance.addEventListener('message', this.onmessage);
+            // 更新连接状态
+            this.ws.instance.addEventListener('open', this.onSocketOpened);
+            this.ws.instance.addEventListener('close', this.onSocketClosed);
+            this.ws.isSelf = false;
+
+            this.updateChatStatus(this.ws.instance.readyState === WebSocket.OPEN ? ChatStatus.IS_READY : ChatStatus.IS_WAITING);
+        } else if (this.config.websocketUrl) {
+            // 使用内部WebSocket连接
+            this.connectWebSocket();
+        }
+    }
+
+    connectWebSocket() {
+
+
+        this.ws.instance = new WebSocket(this.config.websocketUrl);
+
+        this.ws.instance.addEventListener('open', this.onSocketOpened);
+
+        this.ws.instance.addEventListener('message', this.onmessage);
+
+        this.ws.instance.onclose = () => {
+            this.onSocketClosed()
+            if (this.config.websocketUrl) {  // 只有在使用URL模式时才自动重连
+                setTimeout(() => this.connectWebSocket(), 3000);
+            }
+        };
+
+        this.ws.instance.onerror = (error) => {
+            console.error('WebSocket error:', error);
+            this.updateChatStatus(ChatStatus.DISCONNECTED);
+        };
+        this.ws.isSelf = true;
+    }
+
+    /**
+     * 发送消息
+     * @param message
+     * @returns {boolean}
+     */
+    sendMessage(message) {
+        if (this.status != ChatStatus.IS_READY && message.action !== 'cancel') {
+            console.error('当前状态不是就绪状态,无法发送消息', this.status);
+            return false;
+        }
+        this.updateChatStatus(ChatStatus.IS_WAITING)
+        const msg = this.processSendMessage(message);
+        this.ws.instance.send(JSON.stringify(this.adaptMsg(msg)));
+        this.processingMsg = msg
+        this.handleMessage(msg)
+        if (this.config.onMsgSend) {
+            this.config.onMsgSend(msg)
+        }
+        return true;
+    }
+
+    /**
+     * 取消当前聊天
+     */
+    cancelCurrentChat() {
+        if (this.processingMsg) {
+            this.sendMessage({ ...this.processingMsg, 'action': "cancel" })
+        }
+    }
+
+    handleMessage(msg) {
+        switch (msg.action) {
+            case 'response':
+                this.updateChatStatus(ChatStatus.IS_RECEIVING);
+                break;
+            case 'over':
+                this.updateChatStatus(ChatStatus.IS_READY);
+                break;
+            case 'error':
+                this.updateChatStatus(ChatStatus.IS_RECEIVING);
+                break;
+        }
+        this.messageListener.forEach(listener => listener(msg))
+        if (this.config.onMsgReceive) {
+            this.config.onMsgReceive(msg)
+        }
+    }
+
+
+    /**
+     * 处理要发送的消息
+     * @param message
+     * @returns {string}
+     */
+    processSendMessage(message) {
+		console.log("发送消息");
+        let data = {
+            sessionId: this.config.sessionId,
+            app: this.config.app,
+            flag: this.generateMsgFlag(), //msg标识,方便后端返回数据后找到该数据做后续处理
+            ...message,
+            global: this.config.global
+        };
+
+
+        return data;
+    }
+
+    adaptMsg(msg) {
+        if (this.config.adapter) {
+            switch (this.config.adapter) {
+                case 'jbolt':
+                    return this.adaptJBolt(msg);
+            }
+        }
+        return msg;
+    }
+
+    /**
+     * 对jbolt进行适配
+     */
+    adaptJBolt(data) {
+        if (data.command) {
+            //解封
+            return data.data;
+        } else {
+            //包装
+            return {
+                command: 'ai_chat',
+                type: 1,
+                data: data
+            }
+        }
+    }
+
+
+    /**
+     * 生成消息标识
+     * @returns {string} 时间戳+4位随机字符串
+     */
+    generateMsgFlag() {
+        const timestamp = Date.now();
+        const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+        let randomStr = '';
+        for (let i = 0; i < 4; i++) {
+            randomStr += chars.charAt(Math.floor(Math.random() * chars.length));
+        }
+        return `${timestamp}${randomStr}`;
+    }
+
+
+    onmessage(event) {
+        try {
+            let message = JSON.parse(event.data);
+
+            if (this.config.adapter) {
+                message = this.adaptMsg(message);
+            }
+            if ((this.config.app && message.app != this.config.app) || (this.config.sessionId && message.sessionId != this.config.sessionId)) {
+                //不是自己的消息,不做任何处理
+                return;
+            }
+            // console.debug("接收到消息:", message)
+            this.handleMessage(message)
+        } catch (error) {
+            console.error('Error parsing message:', error);
+        }
+    }
+
+    onSocketOpened() {
+        console.log('WebSocket connected');
+        this.updateChatStatus(ChatStatus.IS_READY);
+    }
+
+    onSocketClosed() {
+        console.log('WebSocket disconnected');
+        this.updateChatStatus(ChatStatus.DISCONNECTED);
+    }
+
+    // 更新连接状态的方法
+    updateChatStatus(status) {
+        this.status = status;
+        this.statusListener.forEach(listener => listener(status))
+
+    }
+
+    // 更新WebSocket实例的方法
+    updateWebSocket(newInstance) {
+
+        this.config.websocketInstance = newInstance;
+        this.config.websocketUrl = null;  // 清除URL配置
+        this.setupWebSocket();
+    }
+
+    // 更新WebSocket URL的方法
+    updateWebSocketUrl(newUrl) {
+        this.config.websocketUrl = newUrl;
+        this.config.websocketInstance = null;  // 清除实例配置
+        this.setupWebSocket();
+    }
+
+    /**
+     * 更新全局参数
+     * @param newParams
+     */
+    updateGlobal(newParams) {
+        this.config.global = newParams;
+    }
+
+
+    deleteMsg(flag, msgId) {
+        console.debug("开始删除", flag, msgId)
+        if (this.config.onMsgToDelete) {
+            const res = this.config.onMsgToDelete(msgId);
+            if (res === true) {
+                this.messagePanel.deleteMessage(flag)
+            } else if (res instanceof Promise) {
+                res.then((resolvedValue) => {
+                    if (resolvedValue === true) { // Check the resolved value
+                        this.messagePanel.deleteMessage(flag);
+                    }
+                })
+            }
+        } else {
+            throw new Error('onMsgToDelete is not defined');
+        }
+    }
+
+    /**
+     * 重新生成内容
+     * @param flag
+     */
+    regenerate(flag, msgId) {
+        if (this.status != ChatStatus.IS_READY) {
+            throw new Error("当前有消息正在接收,请等待结束后再重新生成");
+        }
+        this.sendMessage({
+            action: "regenerate",
+            flag,
+            msgId: msgId
+        })
+    }
+
+}
+
+export default AiChat;

+ 37 - 0
src/components/jb-ai-chat/msg-tip.js

@@ -0,0 +1,37 @@
+// 定义消息组件
+import {BaseElement, ChatStatus, baseStyle} from "./base.js";
+/**
+ * 消息提示组件
+ */
+class MsgTip extends BaseElement {
+
+
+    //完善MsgTip 组件,组件用于在 聊天消息中插入一些提示语,提示语通过 组件的content属性传入,组件还支持几种样式,info, error, success,
+    constructor() {
+        super();
+        this.attachShadow({ mode: 'open' });
+    }
+
+    connectedCallback() {
+        this.render();
+    }
+
+    render() {
+
+        const type = this.getAttribute('type') || 'info';
+
+        this.shadowRoot.innerHTML = `
+            <style>
+                .msg-tip { padding: 12px 12px; border-radius: 8px; margin: 8px 0; font-size: 14px;word-break: break-all }
+                .msg-tip.info { background: #e6f4ff; border: 0px solid #91caff; color: #1677ff; }
+                .msg-tip.error { background: #fff2f0; border: 0px solid #ffccc7; color: #ff4d4f; }
+                .msg-tip.success { background: #f6ffed; border: 0px solid #b7eb8f; color: #52c41a; }
+            </style>
+            <div class="msg-tip ${type}"><slot></slot></div>
+        `;
+    }
+
+
+}
+
+export default MsgTip;

File diff suppressed because it is too large
+ 40 - 0
src/components/jb-ai-chat/reference-item.js


+ 10 - 0
src/main.ts

@@ -40,8 +40,12 @@ VXETable.config({
 
 // 修改 el-dialog 默认点击遮照为不关闭
 import { ElDialog } from 'element-plus';
+import { useWebSocketStore } from "@/utils/websocketAi";
 ElDialog.props.closeOnClickModal.default = false;
 
+const pinia = createPinia()
+
+
 const app = createApp(App);
 
 app.use(HighLight);
@@ -51,7 +55,13 @@ app.use(store);
 app.use(i18n);
 app.use(VXETable);
 app.use(plugins);
+app.use(pinia)
 // 自定义指令
 directive(app);
 
 app.mount('#app');
+
+//  确保在注册 Pinia 后导入或使用 Store,初始化 WebSocket 连接
+const webSocketStore = useWebSocketStore()
+
+webSocketStore.connect()

+ 12 - 0
src/router/index.ts

@@ -88,6 +88,18 @@ export const constantRoutes: RouteRecordRaw[] = [
         meta: { title: '个人中心', icon: 'user' }
       }
     ]
+  },
+  {
+    path: '/ai/appSession',
+    hidden: true,
+    children: [
+      {
+        path: 'index/:appId',
+        component: () => import('@/views/ai/appSession/index.vue'),
+        name: 'AppSession',
+        meta: { title: 'AI会话', activeMenu: '/ai/app/index', icon: '', noCache: true }
+      }
+    ]
   }
 ];
 

+ 1 - 0
src/types/env.d.ts

@@ -20,6 +20,7 @@ interface ImportMetaEnv {
   VITE_APP_CLIENT_ID: string;
   VITE_APP_WEBSOCKET: string;
   VITE_APP_SSE: string;
+  VITE_APP_WEB_SOCKET_API: string;
 }
 interface ImportMeta {
   readonly env: ImportMetaEnv;

+ 17 - 1
src/utils/ruoyi.ts

@@ -230,7 +230,23 @@ export const getNormalPath = (p: string): string => {
 export const blobValidate = (data: any) => {
   return data.type !== 'application/json';
 };
-
+/**
+ * 生成UUID
+ */
+export const generateUUID = () => {
+  let d = new Date().getTime();
+  if (window.performance && typeof window.performance.now === "function") {
+    d += performance.now(); // 使用高精度时间,如果可用
+  }
+  // 首先按原样生成带有横杠的UUID
+  let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+    let r = (d + Math.random() * 16) % 16 | 0;
+    d = Math.floor(d / 16);
+    return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
+  });
+  // 然后去掉所有的横杠
+  return uuid.replace(/-/g, '');
+}
 export default {
   handleTree
 };

+ 92 - 0
src/utils/websocketAi.js

@@ -0,0 +1,92 @@
+import {defineStore} from 'pinia'
+import {ref} from 'vue'
+
+
+export const useWebSocketStore = defineStore('websocket', () => {
+    // const { appContext } = getCurrentInstance()
+    const ws = ref(null)
+    const isConnecting = ref(false)
+    const reconnectAttempts = ref(0)
+    const isLinked = ref(false)
+
+    const reconnectDelay = 3000 // 每次重连前等待的时间(毫秒)
+    const url = import.meta.env.VITE_APP_WEB_SOCKET_API
+
+    setInterval(() => {
+        send({action: "ping"})
+    }, 5000)
+
+
+    function connect() {
+        if (isConnecting.value) return
+
+        isConnecting.value = true
+        console.log('Attempting to connect WebSocket...')
+
+        try {
+            ws.value = new WebSocket(url)
+          // ws.value = new WebSocket(url, {
+          //   perMessageDeflate: {
+          //     serverMaxWindowBits: 15
+          //   },
+          // });
+
+            ws.value.onopen = () => {
+                console.log('WebSocket 连接成功')
+                isConnecting.value = false
+                reconnectAttempts.value = 0
+            }
+
+            ws.value.onerror = (error) => {
+                console.error('WebSocket error:', error)
+                // 在错误发生时也尝试重连
+                handleDisconnect()
+            }
+
+            ws.value.onclose = (event) => {
+                console.log('WebSocket disconnected. Code:', event.code, 'Reason:', event.reason)
+                handleDisconnect()
+            }
+
+            function handleDisconnect() {
+                isConnecting.value = false
+                console.log(`Attempting to reconnect... (${reconnectAttempts.value + 1})`)
+                setTimeout(() => {
+                    reconnectAttempts.value++
+                    connect()
+                }, reconnectDelay)
+            }
+
+        } catch (error) {
+            console.error('Error creating WebSocket connection:', error)
+            isConnecting.value = false
+            // 如果创建WebSocket对象失败,立即尝试重连
+            handleDisconnect()
+        }
+    }
+
+    function send(message) {
+        if (ws.value && ws.value.readyState === WebSocket.OPEN) {
+            ws.value.send(typeof message === 'string' ? message : JSON.stringify(message))
+        } else {
+            console.error('WebSocket is not connected. Message not sent:', message)
+        }
+    }
+
+    function close() {
+        if (ws.value) {
+            ws.value.close()
+            ws.value = null // 确保 WebSocket 对象被正确清除
+        }
+    }
+
+    return {
+        ws,
+        isConnecting,
+        reconnectAttempts,
+        connect,
+        send,
+        close,
+        isLinked
+    }
+})

+ 251 - 0
src/views/ai/ai_app/index.vue

@@ -0,0 +1,251 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="应用名称" prop="name">
+              <el-input v-model="queryParams.name" placeholder="请输入应用名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="SN" prop="sn">
+              <el-input v-model="queryParams.sn" placeholder="请输入SN" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="简要信息" prop="briefInfo">
+              <el-input v-model="queryParams.briefInfo" placeholder="请输入简要信息" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="是否是内置app" prop="isBuildIn">
+              <el-input v-model="queryParams.isBuildIn" placeholder="请输入是否是内置app" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="是否启用" prop="enable">
+              <el-input v-model="queryParams.enable" placeholder="请输入是否启用" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['ai:aiApp:add']">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['ai:aiApp:edit']">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['ai:aiApp:remove']">删除</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['ai:aiApp:export']">导出</el-button>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" :data="aiAppList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="应用名称" align="center" prop="name" />
+        <el-table-column label="SN" align="center" prop="sn" />
+        <el-table-column label="应用类型" align="center" prop="appTypeName" />
+        <el-table-column label="是否启用" align="center" prop="enable" />
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['ai:aiApp:edit']"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['ai:aiApp:remove']"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="totalRow" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+    <!-- 添加或修改AI应用对话框 -->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+      <el-form ref="aiAppFormRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="应用名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入应用名称" />
+        </el-form-item>
+        <el-form-item label="SN" prop="sn">
+          <el-input v-model="form.sn" placeholder="请输入SN" />
+        </el-form-item>
+        <el-form-item label="简要信息" prop="briefInfo">
+          <el-input v-model="form.briefInfo" type="textarea" placeholder="请输入内容" />
+        </el-form-item>
+        <el-form-item label="是否是内置app" prop="isBuildIn">
+          <el-input v-model="form.isBuildIn" placeholder="请输入是否是内置app" />
+        </el-form-item>
+        <el-form-item label="是否启用" prop="enable">
+          <el-input v-model="form.enable" placeholder="请输入是否启用" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="AiApp" lang="ts">
+import { listAiApp, getAiApp, delAiApp, addAiApp, updateAiApp } from "@/api/ai/app";
+import { AiAppVO, AiAppQuery, AiAppForm } from '@/api/ai/aiApp/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const aiAppList = ref<AiAppVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const totalRow = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const aiAppFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const initFormData: AiAppForm = {
+  id: undefined,
+  name: undefined,
+  sn: undefined,
+  briefInfo: undefined,
+  appType: undefined,
+  isBuildIn: undefined,
+  enable: undefined,
+}
+const defaultSort = ref<any>({ prop: 'createTime', order: 'descending' });
+
+const data = reactive<PageData<AiAppForm, AiAppQuery>>({
+  form: {...initFormData},
+  queryParams: {
+    pageNum: 1,//废弃
+    keywords: '',// lin add
+    page: 1,
+    pageSize: 10,
+    orderByColumn: defaultSort.value.prop,
+    isAsc: defaultSort.value.order
+  },
+  rules: {
+    id: [
+      { required: true, message: "主键ID不能为空", trigger: "blur" }
+    ],
+    name: [
+      { required: true, message: "应用名称不能为空", trigger: "blur" }
+    ],
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询AI应用列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listAiApp(queryParams.value);
+  aiAppList.value = res.data.list;
+  total.value =res.data.totalPage;
+  totalRow.value = res.data.totalRow;
+
+  loading.value = false;
+}
+
+/** 取消按钮 */
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+}
+
+/** 表单重置 */
+const reset = () => {
+  form.value = {...initFormData};
+  aiAppFormRef.value?.resetFields();
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: AiAppVO[]) => {
+  ids.value = selection.map(item => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset();
+  dialog.visible = true;
+  dialog.title = "添加AI应用";
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: AiAppVO) => {
+  reset();
+  const _id = row?.id || ids.value[0]
+  const res = await getAiApp(_id);
+  Object.assign(form.value, res.data);
+  dialog.visible = true;
+  dialog.title = "修改AI应用";
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+  aiAppFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      if (form.value.id) {
+        await updateAiApp(form.value).finally(() =>  buttonLoading.value = false);
+      } else {
+        await addAiApp(form.value).finally(() =>  buttonLoading.value = false);
+      }
+      proxy?.$modal.msgSuccess("操作成功");
+      dialog.visible = false;
+      await getList();
+    }
+  });
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: AiAppVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除AI应用编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
+  await delAiApp(_ids);
+  proxy?.$modal.msgSuccess("删除成功");
+  await getList();
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.download('ai/aiApp/export', {
+    ...queryParams.value
+  }, `aiApp_${new Date().getTime()}.xlsx`)
+}
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 151 - 0
src/views/ai/app/index.vue

@@ -0,0 +1,151 @@
+<template >
+  <div class="container ">
+
+        <el-card shadow="hover" style="margin-bottom: 10px">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="应用名称" prop="name">
+              <el-input v-model="queryParams.keywords" placeholder="请输入应用名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+
+    <el-card shadow="never" style="background-color: #fcfcfc">
+      <el-row :gutter="20">
+        <el-col :span="6" v-for="(item, index) in dataList" :key="index">
+          <el-card shadow="hover" class="card "  @click="handleAppSession(item)">
+            <img v-if="item.config===null" :src="defAva" :title="item.briefInfo" class="card-image" alt="">
+            <img v-if="item.config!==null" :src="item.config.uiConfig.appLogo" :title="item.briefInfo" class="card-image" alt="">
+            <div class="card-content">
+              <span class="ai_title">{{ item.name }}</span>
+              <span v-if="item.appType==='app_design'" class="ai_subtitle" style="color: #804205"><strong>{{ item.appTypeName }}</strong></span>
+              <span v-if="item.appType==='app_simple'" class="ai_subtitle" style="color: #225e05"><strong>{{ item.appTypeName }}</strong></span>
+            </div>
+          </el-card>
+        </el-col>
+      </el-row>
+
+      <pagination v-show="total > 0" :total="totalRow" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+
+
+  </div>
+</template>
+<style>
+
+</style>
+<script setup name="Tenant" lang="ts">
+import cache from "@/plugins/cache";
+import { useUserStore } from '@/store/modules/user';
+import { listAiApp } from '@/api/ai/app';
+import { AiAppQuery, AiAppVO } from '@/api/ai/app/types';
+//默认图片
+import defAva from '@/assets/images/profile.jpg';
+import { generateUUID } from '@/utils/ruoyi';
+import { getBySn } from "@/api/ai/session";
+
+const userStore = useUserStore();
+const loading = ref(true);
+const total = ref(0);
+const totalRow = ref(0);
+const dataList = ref<AiAppVO[]>([]);
+
+const queryFormRef = ref<ElFormInstance>();
+
+const defaultSort = ref<any>({ prop: 'createTime', order: 'descending' });
+const queryParams = ref<AiAppQuery>({
+  pageNum: 1,//废弃
+  keywords: '',// lin add
+  page: 1,
+  pageSize: 10,
+  orderByColumn: defaultSort.value.prop,
+  isAsc: defaultSort.value.order
+});
+/** 查询应用列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listAiApp(queryParams.value);
+  dataList.value =res.data.list
+  total.value = res.data.totalPage;
+  totalRow.value = res.data.totalRow;
+  loading.value = false;
+};
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+}
+// 获取路由实例
+const router = useRouter();
+/**
+ * 点击应用
+ * @param item
+ */
+const handleAppSession = (item: AiAppVO) =>{
+
+  const key = generateUUID();
+  cache.session.setJSON(key, item);
+  //router.push(`/ai/appSession/index/${key}`);
+
+
+  // 创建一个指向命名路由的对象
+  const route = { name: 'AppSession', params: { appId: key } };
+  // 使用 router.resolve 解析该路由对象为一个可以使用的 URL
+  const resolvedRoute = router.resolve(route);
+
+  // 使用 window.open 在新标签页打开该 URL
+  window.open(resolvedRoute.href, '_blank');
+}
+
+
+onMounted(() => {
+  getList();
+});
+</script>
+<style scoped>
+.container {
+  padding: 20px;
+}
+
+.card {
+  border: 1px solid #ddd;
+  border-radius: 8px;
+  overflow: hidden;
+  margin-bottom: 20px;
+  cursor: pointer; /* 鼠标悬浮时变为小手 */
+}
+
+.card-image {
+  width: 100%;
+  height: 150px;
+  object-fit: cover;
+}
+
+.card-content {
+  padding: 10px;
+  display: flex;
+  flex-direction: column;
+}
+.ai_title, .ai_subtitle {
+  /* 将span元素变为块级元素,使其上下排列 */
+  display: block;
+}
+.ai_title{
+  font-size: 25px;
+  margin-top: 0;
+}
+.ai_subtitle{
+  margin-top: 20px;
+  font-size: 16px;
+  color: #494949FF;
+}
+</style>

+ 656 - 0
src/views/ai/appSession/index.vue

@@ -0,0 +1,656 @@
+<template>
+  <div class="p-2">
+    <div class="min-h-screen bg-slate-50">
+      <!-- 不可点击的遮罩层 -->
+      <div v-if="appInfo.enable==false" class="fixed inset-0 bg-gray-500 bg-opacity-50  z-40"></div>
+      <!-- 左侧导航 -->
+      <div class="fixed left-0 top-0 h-full  bg-white shadow-lg backdrop-blur-sm z-30 transition-all duration-300 "
+           :class="{
+      '-translate-x-full': (isSidebarHidden && isPC)||isSidebarHidden ,
+      'w-64':isPC}
+      ">
+        <!-- 左侧导航 -->
+
+        <!-- 搜索框 -->
+        <div class="p-4">
+          <div class="relative">
+            <el-input v-model="keywords" @keyup.enter="searchHistory" style="width: 220px" prefix-icon="Search"/>
+          </div>
+        </div>
+        <!-- 对话历史列表 -->
+        <div class="chat-list-div">
+          <div class="overflow-y-auto h-[calc(100vh-80px)] ">
+            <div v-for="(chat,index)  in chats" :key="chat.id" :class="{'bg-blue-50': chat.id === activeChatId}"
+                 @scroll="handleSessionScroll" @click="clickSession(chat.id)"
+                 @mouseenter="showDeleteButton(index)" @mouseleave="handleMouseLeave($event, index)"
+                 class="group px-4 py-4 hover:bg-slate-50 cursor-pointer transition-colors border-b border-slate-100 last:border-b-0">
+              <div class="flex items-start justify-between gap-1">
+                <div class="flex flex-col min-w-0">
+                  <span class="text-sm text-gray-700 font-medium mb-1 truncate">{{ chat.name }}</span>
+                  <span class="text-xs text-gray-400">{{ chat.createTime }}</span>
+                </div>
+                <span :class="{ 'hidden':!showDelete[index] }" v-if="isPC" >
+								  <el-button @click.stop="handleDeleteChat(index)" type="danger" icon="Delete" circle />
+							  </span>
+                <span v-else>
+								  <el-button @click.stop="handleDeleteChat(index)" type="danger" icon="Delete" circle />
+							  </span>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="absolute -right-10 top-1 md:top-4">
+          <el-icon size="20" @click="toggleSidebar" v-if="isSidebarHidden"><Expand /></el-icon>
+          <el-icon size="20" @click="toggleSidebar" v-if="!isSidebarHidden"><Fold /></el-icon>
+        </div>
+      </div>
+      <!-- 主聊天区域 -->
+      <div class=" h-screen flex flex-col bg-slate-50/50 transition-all duration-300" :class="{
+       'ml-64':!isSidebarHidden && isPC,
+       'ml-0': isSidebarHidden ||!isPC,
+       'w-[calc(100%-16rem)]':!isSidebarHidden && isPC, // 当侧边栏显示时,预留侧边栏宽度
+       'w-full': isSidebarHidden ||!isPC // 当侧边栏隐藏或屏幕小于1024px时,全宽
+     }">
+        <!-- 遮罩层-->
+        <div v-if="!isSidebarHidden && !isPC" class="fixed  inset-0 bg-gray-500 bg-opacity-50  z-10"
+             @click="toggleSidebar"></div>
+        <!-- 顶部栏 -->
+        <div
+          class="min-h-28 md:min-h-32 pt-8 pb-6 md:pt-4 md:py-4 border-b bg-white backdrop-blur-sm flex items-center justify-between px-4 md:px-16 relative shadow-sm">
+          <div class="absolute top-1 md:top-4 right-4 flex gap-2">
+            <el-tooltip
+              class="box-item"
+              effect="dark"
+              content="添加新会话"
+              placement="top-start"
+            >
+              <el-button @click="handleAddChat" icon="Plus" circle />
+            </el-tooltip>
+
+            <el-tooltip
+              class="box-item"
+              effect="dark"
+              content="清空当前聊天记录"
+              placement="top-start"
+            >
+              <el-button @click="handlePositiveClick" icon="Delete" circle />
+            </el-tooltip>
+
+
+          </div>
+          <div class="max-w-full flex-1 flex flex-col md:flex-row items-center justify-center gap-3 md:gap-6">
+            <div class="w-12 h-12 md:w-16 md:h-16 rounded-2xl flex items-center justify-center shrink-0">
+              <img :src="appInfo.appLogo" alt="Logo" class="w-full h-full object-contain">
+            </div>
+            <div class="flex flex-col items-center md:items-start  overflow-hidden max-w-full">
+              <h2
+                class="text-2xl md:text-3xl font-bold bg-gradient-to-r from-[#4f46e5] via-[#4338ca] to-[#3730a3] bg-clip-text text-transparent tracking-wider text-center md:text-left whitespace-nowrap text-ellipsis overflow-hidden max-w-full">
+                {{ appInfo.name }}
+              </h2>
+              <div class="flex items-center gap-4 md:mt-2 max-w-full">
+                <p
+                  class="text-gray-500 font-medium whitespace-nowrap text-ellipsis overflow-hidden max-w-full">
+                  {{ appInfo.briefInfo }}
+                </p>
+              </div>
+            </div>
+          </div>
+        </div>
+        <!-- 对话内容区 -->
+        <div v-if="!activeChatId"
+             style="display: flex; justify-content: center;align-items: center;flex:1;font-size: 36px;font-weight: 500;color:#d5d5d5">
+          请先创建或选择一个会话
+        </div>
+        <div v-show="activeChatId" class="chat_panel" style="position: relative" ref="chatContainer"></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup name="Tenant" lang="ts">
+
+import markdownit from 'markdown-it'
+import texmath from "markdown-it-texmath";
+import linkAttributes from "markdown-it-link-attributes"
+import katex from "katex";
+import AiChat from '@/components/jb-ai-chat/index.js'
+import { useWebSocketStore } from "@/utils/websocketAi";
+import {
+  add,
+  deleteBySessionId, deleteSession,
+  delMsgById, getByIdSession,
+  getBySn,
+  getListByPage,
+  getListSessionByPage,
+  update
+} from "@/api/ai/session";
+import cache from "@/plugins/cache";
+import { AiAppVO } from "@/api/ai/app/types";
+import { Delete, Search } from '@element-plus/icons-vue'
+//默认图片
+import defAva from '@/assets/images/profile.jpg';
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const aiApp = ref<AiAppVO>();
+const appSn = ref(null)
+const appId = ref(null)
+const chats = ref([]);
+const activeChatId = ref(null);
+const showDelete = ref({})// 控制删除按钮显示的状态
+const confirmOpen = ref({}) // 控制询问框是否打开的状态
+const currentChat = ref({
+  id: '',
+  name: '',
+  hasMsg: false,
+  config: {
+    llmConfig: {
+      systemRole: ''
+    }
+  }
+})
+//默认当前的app对象
+const appInfo = ref({
+  name: '',
+  userAvatar: '',
+  showAvatar: false,
+  assistantAvatar: '',
+  appLogo: '',
+  briefInfo: '',
+  enable: false
+})
+
+const isSidebarHidden = ref(false);
+const isPC = ref(true);
+const window = ref(globalThis.window);
+// 监听窗口大小变化
+const handleResize = () => {
+  isPC.value = window.value.innerWidth >= 1024;
+  if (!isPC.value) {
+    isSidebarHidden.value = true;
+  }
+};
+const webSocketStore = storeToRefs(useWebSocketStore())
+const chatContainer = ref(null);
+const chatInstance = ref(null);
+const chatState = ref({
+  pageSize: 20,
+  hasMore: true,
+})
+const md = markdownit({
+  html: true,
+  breaks: true,
+  linkify: true,
+  typographer: true
+});
+
+md.use(texmath, {
+  engine: katex,
+  delimiters: [
+    "dollars", "brackets"
+  ]
+})
+// 使用 linkAttributes 插件,并配置目标属性和 rel 属性
+md.use(linkAttributes, {
+  pattern: /https?:\/\/(www\.)?/i, // 匹配所有 HTTP/HTTPS 链接
+  attrs: {
+    target: '_blank', // 设置所有链接在新标签页打开
+    rel: 'noopener' // 增加这个属性可以提高安全性
+  }
+})
+
+
+const showDeleteButton = (index) => {
+  if (!confirmOpen.value[index]) {
+    showDelete.value[index] = true;
+  }
+};
+
+const handleMouseLeave = (event, index) => {
+  const target = event.relatedTarget;
+  const isMovingToPopover = target && (target.classList.contains('n-popover') || target.classList.contains('n-popover-arrow'));
+  if (target && !event.currentTarget.contains(target) && !confirmOpen.value[index] && !isMovingToPopover) {
+    showDelete.value[index] = false;
+  }
+};
+
+const openConfirm = (index) => {
+  confirmOpen.value[index] = true;
+};
+
+const closeConfirm = (index) => {
+  confirmOpen.value[index] = false;
+  showDelete.value[index] = false;
+};
+const handleCancel = (index) => {
+  // 隐藏删除按钮
+  showDelete.value[index] = false;
+  // 重置确认框打开状态
+  confirmOpen.value[index] = false;
+};
+
+function handlePositiveClick() {
+  deleteBySessionId(activeChatId.value).then(res => {
+    ElMessage({ message: '聊天记录清除成功', type: 'success' });
+    loadChatInstance()
+  }).catch((err) => {
+    console.error(err);
+  })
+}
+function handleNegativeClick() {
+  ElMessage({ message: '已取消', type: 'info' });
+}
+const keywords = ref('');
+
+watch(() => webSocketStore.ws.value, () => {
+  console.debug("websock重连")
+  if (chatInstance.value) {
+    chatInstance.value.updateWebSocket(webSocketStore.ws.value)
+  }
+})
+
+watch(() => chatContainer.value, (newV) => {
+  console.debug("chatContainer watch...")
+  //dom初始化好了
+  if (newV && activeChatId.value && !chatInstance.value) {
+    loadChatInstance()
+  }
+})
+
+watch(() => activeChatId.value, (newV) => {
+  if (newV) {
+    if (chatInstance.value) {
+      chatInstance.value.destroy();
+    }
+    getByIdSession(activeChatId.value).then(res => {
+      currentChat.value = {
+        ...res.data,
+        hasMsg: false
+      }
+    }).catch((error) => {
+      console.error(error);
+    })
+    if (chatContainer.value) {
+      chatState.value = {
+        pageSize: 20,
+        hasMore: true
+      }
+      loadChatInstance()
+    }
+    if (!isPC.value) {
+      isSidebarHidden.value = true;
+    }
+  }
+
+})
+
+function loadChatInstance() {
+  //初始化aichat
+  chatInstance.value = new AiChat(chatContainer.value, {
+    websocketInstance: webSocketStore.ws.value,
+    app: appId.value,
+    sessionId: activeChatId.value,
+    global: null,
+    showAvatar: appInfo.value.showAvatar ?? true,
+    userAvatar: appInfo.value.userAvatar ?? "",
+    assistantAvatar: appInfo.value.assistantAvatar ?? "",
+    onMsgSend: (msg) => {
+      if (currentChat.value.hasMsg == false) {
+        handleUpdateSessionName(currentChat.value.id, msg.data.content.text);
+        currentChat.value.hasMsg = true;
+      }
+    },
+    onMsgToDelete: async (id) => {
+      console.debug("删除消息", id)
+      await delMsgById(id).then(res => {
+        if (res.code == 0) {
+          ElMessage({ message: '删除成功', type: 'success' });
+          return true;
+        } else {
+          ElMessage({ message: '删除失败', type: 'error' });
+          return false;
+        }
+      });
+    },
+    onMarkdownRender: (markdown) => {
+      return md.render(markdown);
+    },
+    onHistoryLoad: async (startId, global) => {
+      if (chatState.value.hasMore == false) return;
+      let result = await getListByPage({
+        sessionId: activeChatId.value,
+        startId,
+        pageSize: chatState.value.pageSize,
+      })
+
+      let {
+        list,
+        totalPage
+      } = result.data
+      chatState.value.hasMore = chatState.value.pageSize == list?.length;
+      // console.error("加载历史记录开始", list)
+      if (!list || !list.length) {
+        // console.error(currentChat.value.hasMsg)
+        return false;
+      }
+      // messages.value = [...messages.value, ...list]
+      // console.debug("加载历史记录完成...", list)
+      currentChat.value.hasMsg = true;
+      return list.map(item => {
+        return {
+          id: item.id,
+          flag: item.role == "user" ? item.id : item.linkId,
+          data: {
+            role: item.role,
+            renderType: item.role == 'assistant' ? "markdown" : "",
+            content: {
+              text: item.content,
+            },
+            reference: item.reference,
+            errorMsg: item.errorMsg,
+          },
+          createTime: item.createTime
+        }
+      })
+    }
+  })
+}
+
+// 处理滚动事件
+function handleSessionScroll(e) {
+  const {
+    scrollHeight,
+    scrollTop,
+    clientHeight
+  } = e.target;
+  // 距离底部100px时加载更多
+  if (scrollHeight - scrollTop - clientHeight < 100 && !chatPageState.loading && chatPageState.hasMore) {
+    loadChatList(false);
+  }
+}
+
+//更新session
+function handleUpdateSessionName(sessionId, msg) {
+  var name = msg.substring(0, 20);
+  update({
+    name: name,
+    id: sessionId,
+    appId: appId.value
+  }).then((ref) => {
+    const itemToUpdate = chats.value.find(item => item.id === sessionId);
+    if (itemToUpdate) {
+      itemToUpdate.name = name;
+    }
+  });
+}
+
+//获取app
+function getApp() {
+  getBySn(appSn.value).then((res) => {
+    appInfo.value = res.data;
+    appId.value = appInfo.value.id;
+    const uiConfig = res.data.config?.uiConfig;
+    if (uiConfig) {
+      appInfo.value.showAvatar = !!uiConfig.showAvatar;
+      console.debug(uiConfig)
+      //如果没有传logo,就用默认JBoltAI头像
+      if (uiConfig.appLogo != undefined && uiConfig.appLogo != '') {
+        appInfo.value.appLogo = uiConfig.appLogo;
+      } else {
+        appInfo.value.appLogo = 'https://static.jbolt.cn/website/jboltai_logo_icon.png';
+      }
+      appInfo.value.assistantAvatar = uiConfig.assistantAvatar;
+      appInfo.value.userAvatar = uiConfig.userAvatar;
+    } else {
+      appInfo.value.appLogo = 'https://static.jbolt.cn/website/jboltai_logo_icon.png';
+    }
+
+    if (!appInfo.value.enable) { //应用已发布
+      ElMessage({ message: '应用未发布,请先发布应用', type: 'error' });
+    }
+    // 页面加载时设置标题
+    document.title = appInfo.value.name;
+    loadChatList(true);
+  });
+}
+
+
+//重新刷新session列表
+function resetChatList() {
+  chatPageState.startId = null;
+  chatPageState.hasMore = true;
+  chatPageState.loading = false;
+  chats.value = [];
+}
+
+//新建session
+function handleAddChat() {
+  add({
+    name: '新的会话',
+    appId: appId.value, //内置appId
+  }).then((res) => {
+    resetChatList();
+    loadChatList(true);
+  });
+}
+
+//关键词搜索session
+const searchHistory = () => {
+  resetChatList()
+  loadChatList(true);
+};
+
+const toggleSidebar = () => {
+  isSidebarHidden.value = !isSidebarHidden.value;
+};
+
+
+const chatPageState = reactive({
+  pageSize: 20,
+  startId: null,
+  hasMore: true,
+  loading: false,
+});
+
+//删除session对象
+function handleDeleteChat(index) {
+
+  if (activeChatId.value === chats.value[index].id) {
+    activeChatId.value = null;
+  }
+
+  // 隐藏删除按钮
+  showDelete.value[index] = false;
+  // 重置确认框打开状态
+  confirmOpen.value[index] = false;
+
+  deleteSession(chats.value[index].id).then((res) => {
+    if (res.code == 0) {
+      ElMessage({ message: '删除成功', type: 'success' });
+      chats.value.splice(index, 1);
+    } else {
+      ElMessage({ message: '删除失败', type: 'error' });
+    }
+  });
+
+}
+
+function clickSession(id){
+  activeChatId.value = id;
+}
+
+//加载session列表
+async function loadChatList(isFirst) {
+  // console.error("调用")
+  if (chatPageState.loading || chatPageState.hasMore == false) return;
+  // console.error("调用22")
+  chatPageState.loading = true;
+  try {
+    let result = await getListSessionByPage({
+      pageSize: chatPageState.pageSize,
+      startId: chatPageState.startId,
+      keywords: keywords.value,
+      appId: appId.value, //内置应用
+    })
+
+    let {
+      list,
+      totalPage
+    } = result.data;
+
+    list = list ?? [];
+    chatPageState.hasMore = list.length == chatPageState.pageSize;
+
+    chatPageState.startId = list[list.length - 1]?.id;
+    chats.value.push(...list)
+
+    if (isFirst) {
+      if (chats.value.length == 0) {
+        handleAddChat();
+      } else {
+        activeChatId.value = chats.value[0]?.id
+      }
+
+    }
+  } catch (error) {
+    console.error('加载聊天列表失败:', error);
+  } finally {
+    chatPageState.loading = false;
+  }
+}
+
+
+onMounted(() => {
+  //获取参数并且初始化aiChat组件
+  const {appId} = proxy.$route.params;
+  aiApp.value = cache.session.getJSON(appId);
+  appSn.value = aiApp.value.sn
+  getApp();
+});
+
+onBeforeMount(() => {
+  // 初始化时检查窗口大小
+  handleResize();
+  // 添加窗口大小变化监听器
+  window.value.addEventListener('resize', handleResize);
+  if (chatInstance.value) {
+    chatInstance.value.destroy()
+  }
+});
+onActivated(() => {
+  if (!chatInstance.value && activeChatId.value) {
+    chatState.value.hasMore = true
+    loadChatInstance()
+  }
+  console.error("aiChat组件被激活,当前被选中的id:" + activeChatId.value)
+})
+onDeactivated(() => {
+  if (chatInstance.value) {
+    chatInstance.value.destroy()
+    chatInstance.value = null;
+  }
+})
+
+</script>
+
+
+
+
+<style scoped>
+.chat-container::-webkit-scrollbar {
+  width: 6px;
+}
+
+.chat-container::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+.chat-container::-webkit-scrollbar-thumb {
+  background-color: rgba(156, 163, 175, 0.5);
+  border-radius: 3px;
+}
+
+input[type="range"] {
+  -webkit-appearance: none;
+  width: 100%;
+  height: 4px;
+  border-radius: 2px;
+  background: #e5e7eb;
+  outline: none;
+}
+
+input[type="range"]::-webkit-slider-thumb {
+  -webkit-appearance: none;
+  appearance: none;
+  width: 16px;
+  height: 16px;
+  border-radius: 50%;
+  background: #4f46e5;
+  cursor: pointer;
+  transition: background .2s ease-in-out;
+}
+
+input[type="range"]::-webkit-slider-thumb:hover {
+  background: #4338ca;
+}
+
+textarea:focus {
+  outline: none;
+  box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1);
+}
+
+[data-v-8aed1f8e] {
+  width: 100%;
+  margin: 0 auto;
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+    transform: translateY(10px);
+  }
+
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+@keyframes fadeOut {
+  from {
+    opacity: 1;
+    transform: translateY(0);
+  }
+
+  to {
+    opacity: 0;
+    transform: translateY(10px);
+  }
+}
+
+.animate-fade-in {
+  animation: fadeIn 0.3s ease-out forwards;
+}
+
+.animate-fade-out {
+  animation: fadeOut 0.3s ease-out forwards;
+}
+
+.chat_panel {
+  flex: 1;
+  overflow: hidden;
+}
+
+
+.keywords-div {
+  outline: none;
+  /* 移除默认的outline */
+  box-shadow: 0 2px 10px 3px rgb(0 0 0 / 5%), inset 0 1px 2px rgba(255, 255, 255, 0.1);;
+  /* 自定义聚焦时的效果 */
+}
+
+.keywords-div:focus {
+  /* 移除默认的outline */
+  box-shadow: 0 2px 10px 3px rgba(0, 0, 0, 0.2), inset 0 1px 2px rgba(255, 255, 255, 0.1);;
+  /* 自定义聚焦时的效果 */
+}
+</style>

+ 1 - 91
src/views/index.vue

@@ -1,96 +1,6 @@
 <template>
   <div class="app-container home">
-    <el-row :gutter="20">
-      <el-col :sm="24" :lg="12" style="padding-left: 20px">
-        <h2>RuoYi-Vue-Plus多租户管理系统</h2>
-        <p>
-          RuoYi-Vue-Plus 是基于 RuoYi-Vue 针对 分布式集群 场景升级(不兼容原框架)
-          <br />
-          * 前端开发框架 Vue3、TS、Element Plus<br />
-          * 后端开发框架 Spring Boot<br />
-          * 容器框架 Undertow 基于 Netty 的高性能容器<br />
-          * 权限认证框架 Sa-Token 支持多终端认证系统<br />
-          * 关系数据库 MySQL 适配 8.X 最低 5.7<br />
-          * 缓存数据库 Redis 适配 6.X 最低 4.X<br />
-          * 数据库框架 Mybatis-Plus 快速 CRUD 增加开发效率<br />
-          * 数据库框架 p6spy 更强劲的 SQL 分析<br />
-          * 多数据源框架 dynamic-datasource 支持主从与多种类数据库异构<br />
-          * 序列化框架 Jackson 统一使用 jackson 高效可靠<br />
-          * Redis客户端 Redisson 性能强劲、API丰富<br />
-          * 分布式限流 Redisson 全局、请求IP、集群ID 多种限流<br />
-          * 分布式锁 Lock4j 注解锁、工具锁 多种多样<br />
-          * 分布式幂等 Lock4j 基于分布式锁实现<br />
-          * 分布式链路追踪 SkyWalking 支持链路追踪、网格分析、度量聚合、可视化<br />
-          * 分布式任务调度 SnailJob 高性能 高可靠 易扩展<br />
-          * 文件存储 Minio 本地存储<br />
-          * 文件存储 七牛、阿里、腾讯 云存储<br />
-          * 监控框架 SpringBoot-Admin 全方位服务监控<br />
-          * 校验框架 Validation 增强接口安全性 严谨性<br />
-          * Excel框架 Alibaba EasyExcel 性能优异 扩展性强<br />
-          * 文档框架 SpringDoc、javadoc 无注解零入侵基于java注释<br />
-          * 工具类框架 Hutool、Lombok 减少代码冗余 增加安全性<br />
-          * 代码生成器 适配MP、SpringDoc规范化代码 一键生成前后端代码<br />
-          * 部署方式 Docker 容器编排 一键部署业务集群<br />
-          * 国际化 SpringMessage Spring标准国际化方案<br />
-        </p>
-        <p><b>当前版本:</b> <span>v5.3.1</span></p>
-        <p>
-          <el-tag type="danger">&yen;免费开源</el-tag>
-        </p>
-        <p>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Vue-Plus')">访问码云</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Vue-Plus')">访问GitHub</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-vue-plus/changlog')"
-            >更新日志</el-button
-          >
-        </p>
-      </el-col>
-
-      <el-col :sm="24" :lg="12" style="padding-left: 20px">
-        <h2>RuoYi-Cloud-Plus多租户微服务管理系统</h2>
-        <p>
-          RuoYi-Cloud-Plus 微服务通用权限管理系统 重写 RuoYi-Cloud 全方位升级(不兼容原框架)
-          <br />
-          * 前端开发框架 Vue3、TS、Element UI<br />
-          * 后端开发框架 Spring Boot<br />
-          * 微服务开发框架 Spring Cloud、Spring Cloud Alibaba<br />
-          * 容器框架 Undertow 基于 XNIO 的高性能容器<br />
-          * 权限认证框架 Sa-Token、Jwt 支持多终端认证系统<br />
-          * 关系数据库 MySQL 适配 8.X 最低 5.7<br />
-          * 关系数据库 Oracle 适配 11g 12c<br />
-          * 关系数据库 PostgreSQL 适配 13 14<br />
-          * 关系数据库 SQLServer 适配 2017 2019<br />
-          * 缓存数据库 Redis 适配 6.X 最低 5.X<br />
-          * 分布式注册中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
-          * 分布式配置中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
-          * 服务网关 Spring Cloud Gateway 响应式高性能网关<br />
-          * 负载均衡 Spring Cloud Loadbalancer 负载均衡处理<br />
-          * RPC远程调用 Apache Dubbo 原生态使用体验、高性能<br />
-          * 分布式限流熔断 Alibaba Sentinel 无侵入、高扩展<br />
-          * 分布式事务 Alibaba Seata 无侵入、高扩展 支持 四种模式<br />
-          * 分布式消息队列 Apache Kafka 高性能高速度<br />
-          * 分布式消息队列 Apache RocketMQ 高可用功能多样<br />
-          * 分布式消息队列 RabbitMQ 支持各种扩展插件功能多样性<br />
-          * 分布式搜索引擎 ElasticSearch 业界知名<br />
-          * 分布式链路追踪 Apache SkyWalking 链路追踪、网格分析、度量聚合、可视化<br />
-          * 分布式日志中心 ELK 业界成熟解决方案<br />
-          * 分布式监控 Prometheus、Grafana 全方位性能监控<br />
-          * 其余与 Vue 版本一致<br />
-        </p>
-        <p><b>当前版本:</b> <span>v2.3.0</span></p>
-        <p>
-          <el-tag type="danger">&yen;免费开源</el-tag>
-        </p>
-        <p>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Cloud-Plus')">访问码云</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Cloud-Plus')">访问GitHub</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-cloud-plus/changlog')"
-            >更新日志</el-button
-          >
-        </p>
-      </el-col>
-    </el-row>
-    <el-divider />
+      欢迎使用AI大模型
   </div>
 </template>
 

+ 1 - 1
vite.config.ts

@@ -24,7 +24,7 @@ export default defineConfig(({ mode, command }) => {
       open: true,
       proxy: {
         [env.VITE_APP_BASE_API]: {
-          target: 'http://localhost:8080',
+          target: 'http://172.16.196.241:8080',
           changeOrigin: true,
           ws: true,
           rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')

Some files were not shown because too many files changed in this diff