Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
P
pms-dispatch-assistant
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
姜耀祖
pms-dispatch-assistant
Commits
9a0d119d
Commit
9a0d119d
authored
Jul 24, 2025
by
赵灿灿
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
新增融合页面
parent
ab5f9df9
Pipeline
#22119
failed with stages
in 3 minutes and 54 seconds
Changes
13
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
1459 additions
and
0 deletions
+1459
-0
LangChainController.java
...chassistant/controller/langchain/LangChainController.java
+110
-0
fusion-experts-chat.html
...in/resources/static/pages/fusion/fusion-experts-chat.html
+162
-0
reload.png
src/main/resources/static/pages/fusion/images/reload.png
+0
-0
service_head.png
...ain/resources/static/pages/fusion/images/service_head.png
+0
-0
service_img1.png
...ain/resources/static/pages/fusion/images/service_img1.png
+0
-0
service_img2.png
...ain/resources/static/pages/fusion/images/service_img2.png
+0
-0
service_img3.png
...ain/resources/static/pages/fusion/images/service_img3.png
+0
-0
service_img4.png
...ain/resources/static/pages/fusion/images/service_img4.png
+0
-0
service_img5.png
...ain/resources/static/pages/fusion/images/service_img5.png
+0
-0
service_img6.png
...ain/resources/static/pages/fusion/images/service_img6.png
+0
-0
user_head.png
src/main/resources/static/pages/fusion/images/user_head.png
+0
-0
ai-chat-vue.js
src/main/resources/static/pages/fusion/js/ai-chat-vue.js
+512
-0
ai-chat.css
src/main/resources/static/pages/fusion/style/ai-chat.css
+675
-0
No files found.
src/main/java/com/infoepoch/pms/dispatchassistant/controller/langchain/LangChainController.java
View file @
9a0d119d
...
@@ -877,4 +877,114 @@ public class LangChainController {
...
@@ -877,4 +877,114 @@ public class LangChainController {
.
body
(
emitter
);
.
body
(
emitter
);
}
}
//内部专家外部专家融合智能体
@GetMapping
(
"/sseFusionIntelligent"
)
public
ResponseEntity
<
SseEmitter
>
sseFusionIntelligent
(
@RequestParam
String
chatMessage
,
@RequestParam
String
dialogId
,
@RequestParam
String
selectedExpert
,
@RequestParam
String
selectedOrg
)
{
String
condition
=
""
;
String
regionName
=
chatService
.
getAuthRegionName
();
// if("内部专家".equals(selectedExpert)) {
// if ("组织内".equals(selectedOrg) && StringUtils.isNotEmpty(regionName)) {
// condition = ",只能查找" + regionName;
// } else if ("组织外".equals(selectedOrg) && StringUtils.isNotEmpty(regionName)) {
// condition = ",排除" + regionName;
// }
// }
String
urlAddr
=
"http://10.32.41.228:44501/scene_gateway/agent/open/5386f9144ee042578d1c0f66c2598d06"
;
//String urlAddr = chatService.getUrl(selectedExpert);
//String urlAddr = "http://10.32.41.35:40517/scene_gateway/agent/83f77143b09c461993dd9a7db403eb94";
SseEmitter
emitter
=
new
SseEmitter
(
0L
);
QuestionRequest
questionRequest
=
new
QuestionRequest
();
questionRequest
.
setKeyword
(
chatMessage
+
condition
);
questionRequest
.
setRequestId
(
getRequestId
());
questionRequest
.
setDialogId
(
dialogId
);
Conversations
conversations
=
chatService
.
saveConversations
(
dialogId
,
chatMessage
,
selectedExpert
);
Messages
messagesQusetion
=
new
Messages
();
messagesQusetion
.
setRequestId
(
dialogId
);
messagesQusetion
.
setContent
(
chatMessage
);
messagesQusetion
.
setRole
(
"user"
);
messagesQusetion
.
setModelName
(
"qwen2.5-72b"
);
messagesQusetion
=
chatService
.
insertQuestionMessage
(
conversations
,
messagesQusetion
);
Messages
messagesContent
=
new
Messages
();
messagesContent
.
setRole
(
"assistant"
);
messagesContent
.
setModelName
(
"qwen2.5-72b"
);
messagesContent
.
setParentMsgId
(
messagesQusetion
.
getId
());
messagesContent
.
setSort
(
messagesQusetion
.
getSort
()+
1
);
chatService
.
insertMessage
(
conversations
,
messagesContent
);
String
params
=
JsonUtils
.
objectToJson
(
questionRequest
);
StringBuffer
content
=
new
StringBuffer
();
StringBuffer
lineCotent
=
new
StringBuffer
();
new
Thread
(()
->
{
HttpURLConnection
connection
=
null
;
try
{
URL
url
=
new
URL
(
urlAddr
);
// 建立链接
connection
=
(
HttpURLConnection
)
url
.
openConnection
();
connection
.
setRequestMethod
(
"GET"
);
connection
.
setRequestProperty
(
"AuthToken"
,
"4009fe23e6b648539792330c14f5ed8e"
);
// connection.setRequestProperty("AuthToken", "fc40db5b7abe47dabfe1899e61fde2d7");
// 允许输入和输出
connection
.
setDoInput
(
true
);
connection
.
setDoOutput
(
true
);
// 设置超时为0,表示无限制
connection
.
setConnectTimeout
(
0
);
connection
.
setReadTimeout
(
0
);
try
(
OutputStream
os
=
connection
.
getOutputStream
())
{
os
.
write
(
params
.
getBytes
(
StandardCharsets
.
UTF_8
));
os
.
flush
();
}
// 检查响应码
int
responseCode
=
connection
.
getResponseCode
();
if
(
responseCode
!=
HttpURLConnection
.
HTTP_OK
)
{
emitter
.
completeWithError
(
new
RuntimeException
(
"SSE 连接失败: "
+
responseCode
));
return
;
}
// 持续读取 SSE 数据流
try
(
BufferedReader
reader
=
new
BufferedReader
(
new
InputStreamReader
(
connection
.
getInputStream
())))
{
String
line
;
while
((
line
=
reader
.
readLine
())
!=
null
)
{
logger
.
info
(
line
);
if
(!
line
.
startsWith
(
"data:CALLBACK#"
))
{
if
(
line
.
startsWith
(
"data:"
))
{
String
data
=
line
.
substring
(
5
).
trim
();
if
(
"stop"
.
equals
(
data
))
{
emitter
.
send
(
SseEmitter
.
event
().
data
(
"stop"
),
MediaType
.
parseMediaType
(
"application/json; charset=UTF-8"
));
}
else
{
lineCotent
.
append
(
data
);
String
sendData
=
data
.
replace
(
"attachment#[]#attachment"
,
""
).
replace
(
"source#[]#source"
,
""
)
.
replace
(
"#"
,
""
).
replace
(
"*"
,
""
);
emitter
.
send
(
SseEmitter
.
event
().
data
(
sendData
),
MediaType
.
parseMediaType
(
"application/json; charset=UTF-8"
));
if
(!
sendData
.
startsWith
(
"SUGGEST["
))
content
.
append
(
sendData
);
}
}
}
}
}
emitter
.
complete
();
// 流结束
}
catch
(
Exception
e
)
{
emitter
.
completeWithError
(
e
);
logger
.
info
(
e
.
getMessage
());
}
finally
{
messagesContent
.
setContent
(
content
.
toString
());
chatService
.
updateMessage
(
messagesContent
);
if
(
connection
!=
null
)
{
connection
.
disconnect
();
}
logger
.
info
(
lineCotent
.
toString
());
}
}).
start
();
return
ResponseEntity
.
ok
()
.
header
(
HttpHeaders
.
CONTENT_TYPE
,
MediaType
.
TEXT_EVENT_STREAM_VALUE
)
.
body
(
emitter
);
}
}
}
src/main/resources/static/pages/fusion/fusion-experts-chat.html
0 → 100644
View file @
9a0d119d
<!DOCTYPE html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF-8"
>
<title>
智能推荐专家
</title>
<link
rel=
"stylesheet"
href=
"style/ai-chat.css"
>
<style>
[
v-cloak
]
{
display
:
none
;
}
</style>
</head>
<body>
<div
id=
"app"
class=
"main-container"
:class=
"{'dark-theme': isDarkTheme}"
v-cloak
>
<!-- 左侧历史对话列表 -->
<div
class=
"sidebar"
>
<div
class=
"sidebar-header"
>
<h1>
智能推荐专家
</h1>
<!-- 添加主题切换按钮 -->
<div
id=
"theme-toggle"
class=
"theme-toggle"
title=
"切换主题"
@
click=
"toggleTheme"
>
<svg
id=
"light-icon"
viewBox=
"0 0 24 24"
:style=
"{display: isDarkTheme ? 'block' : 'none'}"
>
<path
d=
"M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"
></path>
</svg>
<svg
id=
"dark-icon"
viewBox=
"0 0 24 24"
:style=
"{display: isDarkTheme ? 'none' : 'block'}"
>
<path
d=
"M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"
></path>
</svg>
</div>
<button
id=
"new-chat-btn"
class=
"new-chat-btn"
@
click=
"newChat"
>
<span
class=
"plus-icon"
>
+
</span>
新对话
</button>
</div>
<div
class=
"history-list"
>
<div
v-for=
"(section, sectionIndex) in historySections"
:key=
"sectionIndex"
class=
"history-section"
>
<div
class=
"time-label"
>
{{ section.label }}
</div>
<div
v-for=
"(item, itemIndex) in section.items"
:key=
"item.id"
class=
"history-item"
:class=
"{ active: item.active }"
:data-id=
"item.id"
@
click=
"loadChatHistory(item)"
>
<div
class=
"history-title"
>
{{ item.title }}
</div>
<button
class=
"delete-btn"
@
click
.
stop=
"deleteChat(item.id)"
>
<svg
viewBox=
"0 0 24 24"
width=
"16"
height=
"16"
stroke=
"currentColor"
stroke-width=
"2"
fill=
"none"
stroke-linecap=
"round"
stroke-linejoin=
"round"
>
<path
d=
"M3 6h18"
></path>
<path
d=
"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"
></path>
<path
d=
"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
></path>
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- 右侧聊天区域 -->
<div
class=
"chat-area"
>
<!-- 消息历史区域 -->
<div
id=
"scrollContainer"
class=
"scrollContainer"
>
<div
class=
"fist-loading"
v-if=
"fistLoading"
>
<p>
快速查找专家
</p>
<div
class=
"expert-guides-container"
>
<div
class=
"expert-guides-scroll"
:style=
"scrollStyle"
>
<div
v-for=
"(item, index) in extendedGuides"
:key=
"index"
class=
"expert-guide-item"
@
click=
"questionClick(item)"
>
<p>
{{ item }}
</p>
</div>
</div>
</div>
</div>
<div
id=
"chat-messages"
class=
"chat-messages"
v-if=
"!fistLoading"
>
<div
v-for=
"(message, index) in messages"
:key=
"index"
class=
"message"
:class=
"message.role + '-message'"
>
<div
class=
"avatar"
>
{{ message.role === 'user' ? '我' : 'AI' }}
</div>
<div
class=
"content"
>
<p
v-html=
"message.content"
></p>
<!--v-html可能会有xss攻击,但是数据来源于大模型,是否需要清洗数据然后再显示?-->
<div
v-if=
"message.typing"
class=
"typing-indicator"
></div>
</div>
</div>
<div
class=
"question"
>
<div
class=
"question_left"
></div>
<div
v-if=
"questions.length"
class=
"question_right"
>
<p>
您可以继续问我:
</p>
<div
class=
"questionContent"
v-for=
"(q, i) in questions"
:key=
"i"
@
click=
"questionClick(q)"
>
<p>
{{ q }}
</p>
</div>
</div>
</div>
</div>
</div>
<!-- 输入区域 -->
<div
class=
"input-area"
>
<textarea
id=
"user-input"
v-model=
"userInput"
placeholder=
"请输入您的问题 shift+enter换行"
rows=
"3"
></textarea>
<div
class=
"input-area-content"
>
<div
style=
"display: flex;align-items: center;justify-content: flex-start;"
>
<div
class=
"custom-select"
v-click-outside=
"closeExpertDropdown"
style=
"display: none"
>
<div
class=
"selected-option"
@
click
.
stop=
"selectExpert('内部专家')"
:class=
"{ 'active': selectedExpert === '内部专家' }"
>
<span>
内部专家
</span>
<!-- <svg class="dropdown-icon" :class="{ 'rotated': showExpertDropdown }" viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">-->
<!-- <polyline points="6 9 12 15 18 9"></polyline>-->
<!-- </svg>-->
</div>
<!-- <div class="dropdown-menu" v-show="showExpertDropdown">-->
<!-- <div class="dropdown-item" @click.stop="selectExpert('内部专家')" :class="{ 'active': selectedExpert === '内部专家' }">-->
<!-- <span>内部专家</span>-->
<!-- </div>-->
<!--<!– <div class="dropdown-item" @click.stop="selectExpert('外部专家')" :class="{ 'active': selectedExpert === '外部专家' }">–>-->
<!--<!– <span>外部专家</span>–>-->
<!--<!– </div>–>-->
<!-- </div>-->
</div>
<div
class=
"custom-select"
v-click-outside=
"closeExpertDropdown"
style=
"display: none"
>
<div
class=
"selected-option"
@
click
.
stop=
"selectExpert('外部专家')"
:class=
"{ 'active': selectedExpert === '外部专家' }"
>
<span>
外部专家
</span>
</div>
</div>
<div
class=
"custom-select"
v-click-outside=
"closeOrgDropdown"
v-show=
"showOrgSelection"
style=
"display: none"
>
<!-- <div class="selected-option" @click.stop="toggleOrgDropdown" style="background-color: rgba(230, 220, 250, 0.5);color: #6633ff;">-->
<!-- <span>{{ selectedOrg }}</span>-->
<!-- <svg class="dropdown-icon" :class="{ 'rotated': showOrgDropdown }" viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">-->
<!-- <polyline points="6 9 12 15 18 9"></polyline>-->
<!-- </svg>-->
<!-- </div>-->
<!-- <div class="dropdown-menu" v-show="showOrgDropdown">-->
<!-- <div class="dropdown-item" @click.stop="selectOrg('全部组织')" :class="{ 'active': selectedOrg === '全部组织' }">-->
<!-- <span>全部</span>-->
<!-- </div>-->
<!-- <div class="dropdown-item" @click.stop="selectOrg('组织内')" :class="{ 'active': selectedOrg === '组织内' }">-->
<!-- <span>组织内</span>-->
<!-- </div>-->
<!-- <div class="dropdown-item" @click.stop="selectOrg('组织外')" :class="{ 'active': selectedOrg === '组织外' }">-->
<!-- <span>组织外</span>-->
<!-- </div>-->
<!-- </div>-->
</div>
</div>
<div>
<button
id=
"send-btn"
class=
"send-btn"
@
click=
"sendMessage"
v-show=
"!isResponding"
>
<svg
class=
"send-icon"
viewBox=
"0 0 24 24"
width=
"18"
height=
"18"
stroke=
"currentColor"
stroke-width=
"2"
fill=
"none"
stroke-linecap=
"round"
stroke-linejoin=
"round"
>
<line
x1=
"12"
y1=
"19"
x2=
"12"
y2=
"5"
></line>
<polyline
points=
"5 12 12 5 19 12"
></polyline>
</svg>
</button>
<button
id=
"stop-btn"
class=
"stop-btn"
@
click=
"stopResponse"
v-show=
"isResponding"
>
<svg
viewBox=
"0 0 24 24"
width=
"18"
height=
"18"
stroke=
"currentColor"
stroke-width=
"2"
fill=
"none"
stroke-linecap=
"round"
stroke-linejoin=
"round"
class=
"stop-icon"
>
<rect
x=
"6"
y=
"6"
width=
"12"
height=
"12"
rx=
"2"
ry=
"2"
></rect>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
<script
type=
"text/javascript"
src=
"../../libs/require.js/require.min.js"
></script>
<script
type=
"text/javascript"
src=
"../../scripts/require-config.js"
></script>
<!-- 引入Vue相关JS -->
<script
type=
"text/javascript"
src=
"js/ai-chat-vue.js"
></script>
</body>
</html>
\ No newline at end of file
src/main/resources/static/pages/fusion/images/reload.png
0 → 100644
View file @
9a0d119d
1.94 KB
src/main/resources/static/pages/fusion/images/service_head.png
0 → 100644
View file @
9a0d119d
640 Bytes
src/main/resources/static/pages/fusion/images/service_img1.png
0 → 100644
View file @
9a0d119d
600 Bytes
src/main/resources/static/pages/fusion/images/service_img2.png
0 → 100644
View file @
9a0d119d
265 Bytes
src/main/resources/static/pages/fusion/images/service_img3.png
0 → 100644
View file @
9a0d119d
528 Bytes
src/main/resources/static/pages/fusion/images/service_img4.png
0 → 100644
View file @
9a0d119d
659 Bytes
src/main/resources/static/pages/fusion/images/service_img5.png
0 → 100644
View file @
9a0d119d
649 Bytes
src/main/resources/static/pages/fusion/images/service_img6.png
0 → 100644
View file @
9a0d119d
703 Bytes
src/main/resources/static/pages/fusion/images/user_head.png
0 → 100644
View file @
9a0d119d
10.8 KB
src/main/resources/static/pages/fusion/js/ai-chat-vue.js
0 → 100644
View file @
9a0d119d
/**
* AI聊天页面Vue应用
*/
require
([
'jquery'
,
'vue'
,
'utils'
,
'marked'
,
'markdown'
,
'global'
],
function
(
$
,
Vue
,
utils
,
marked
,
markdown
)
{
// 添加点击外部关闭指令
Vue
.
directive
(
'click-outside'
,
{
bind
:
function
(
el
,
binding
,
vnode
)
{
el
.
clickOutsideEvent
=
function
(
event
)
{
if
(
!
(
el
==
event
.
target
||
el
.
contains
(
event
.
target
)))
{
vnode
.
context
[
binding
.
expression
](
event
);
}
};
document
.
body
.
addEventListener
(
'click'
,
el
.
clickOutsideEvent
);
},
unbind
:
function
(
el
)
{
document
.
body
.
removeEventListener
(
'click'
,
el
.
clickOutsideEvent
);
}
});
const
app
=
new
Vue
({
el
:
'#app'
,
data
:
{
// 用户相关数据
currentLoginUser
:
{},
fistLoading
:
true
,
// 聊天相关数据
chatHistory
:
[],
sessionId
:
""
,
currentEventSource
:
null
,
// UI相关数据
theme
:
localStorage
.
getItem
(
'ai-chat-theme'
)
||
'light'
,
isResponding
:
false
,
userInput
:
""
,
// 专家选择相关数据
selectedExpert
:
'内部专家'
,
showExpertDropdown
:
false
,
// 组织内网选择相关数据
selectedOrg
:
'全部组织'
,
showOrgDropdown
:
false
,
questions
:[],
guides
:[
'请帮我推荐几位南京地区的网络规划方面的专家'
,
'推荐既懂运维规划又熟悉移动产品体系的专家'
,
'我需要私有云多中心容灾规划领域的专家'
,
'帮我找几位熟悉5G网络优化的专家'
,
'推荐几位通信网基础设施资源清查方面的专家'
,
'请帮我找工业互联网平台研发与部署的专家'
],
scrollOffset
:
0
,
scrollInterval
:
null
,
transitioning
:
true
,
// 历史对话分类
historySections
:
[
],
// 消息列表
messages
:
[
// { role: 'ai', content: '您好!我是您的专家推荐助手,有什么可以帮助您的吗?' }
],
showOrgSelection
:
true
},
computed
:
{
isDarkTheme
()
{
return
this
.
theme
===
'dark'
;
},
extendedGuides
()
{
// 克隆前3项到末尾
return
[...
this
.
guides
,
...
this
.
guides
.
slice
(
0
,
3
)];
},
scrollStyle
()
{
return
{
transform
:
`translateY(-
${
this
.
scrollOffset
}
px)`
,
transition
:
this
.
transitioning
?
'transform 0.5s ease'
:
'none'
,
};
}
},
created
:
function
()
{
this
.
currentUser
();
this
.
getSessionId
();
this
.
getConversationHistory
();
},
mounted
:
function
()
{
// 监听输入框的键盘事件
this
.
$nextTick
(()
=>
{
const
textarea
=
document
.
getElementById
(
'user-input'
);
if
(
textarea
)
{
textarea
.
addEventListener
(
'keypress'
,
this
.
handleKeyPress
);
}
// 启动自动滚动
this
.
startAutoScroll
();
// 添加鼠标悬停事件监听
this
.
$watch
(
'fistLoading'
,
(
newVal
)
=>
{
if
(
newVal
)
{
// 等待DOM更新
this
.
$nextTick
(()
=>
{
const
guideItems
=
document
.
querySelectorAll
(
'.expert-guide-item'
);
guideItems
.
forEach
(
item
=>
{
item
.
addEventListener
(
'mouseenter'
,
this
.
pauseAutoScroll
);
item
.
addEventListener
(
'mouseleave'
,
this
.
resumeAutoScroll
);
});
});
}
},
{
immediate
:
true
});
});
},
beforeDestroy
:
function
()
{
// 清除自动滚动定时器
this
.
stopAutoScroll
();
// 移除事件监听器
const
guideItems
=
document
.
querySelectorAll
(
'.expert-guide-item'
);
guideItems
.
forEach
(
item
=>
{
item
.
removeEventListener
(
'mouseenter'
,
this
.
pauseAutoScroll
);
item
.
removeEventListener
(
'mouseleave'
,
this
.
resumeAutoScroll
);
});
},
methods
:
{
// 自动滚动相关方法
startAutoScroll
:
function
()
{
const
itemHeight
=
80
;
const
visibleItems
=
3
;
const
totalItems
=
this
.
guides
.
length
;
this
.
scrollInterval
=
setInterval
(()
=>
{
this
.
transitioning
=
true
;
this
.
scrollOffset
+=
itemHeight
;
// 判断是否滚动到克隆项部分(即第 totalItems 行之后)
if
(
this
.
scrollOffset
>=
itemHeight
*
(
totalItems
))
{
// 短暂延迟后瞬间回到原始位置
setTimeout
(()
=>
{
this
.
transitioning
=
false
;
this
.
scrollOffset
=
0
;
},
500
);
// 等待动画结束
}
},
2000
);
},
stopAutoScroll
:
function
()
{
if
(
this
.
scrollInterval
)
{
clearInterval
(
this
.
scrollInterval
);
this
.
scrollInterval
=
null
;
}
},
pauseAutoScroll
:
function
()
{
this
.
stopAutoScroll
();
},
resumeAutoScroll
:
function
()
{
if
(
!
this
.
scrollInterval
&&
this
.
fistLoading
)
{
this
.
startAutoScroll
();
}
},
// 用户相关方法
currentUser
:
function
()
{
const
that
=
this
;
$
.
ajax
({
url
:
"../../auth/current-user"
,
type
:
"get"
,
dataType
:
"json"
,
contentType
:
"application/json;charset=UTF-8"
,
async
:
false
,
success
:
function
(
data
)
{
that
.
currentLoginUser
=
data
.
data
;
}
});
},
// 主题相关方法
toggleTheme
()
{
this
.
theme
=
this
.
theme
===
'light'
?
'dark'
:
'light'
;
localStorage
.
setItem
(
'ai-chat-theme'
,
this
.
theme
);
document
.
body
.
classList
.
toggle
(
'dark-theme'
,
this
.
theme
===
'dark'
);
},
// 会话相关方法
async
getSessionId
()
{
try
{
//const response = await fetch('../../api/langchain/getSessionId');
const
response
=
await
fetch
(
'../../api/langchain/getDialogId'
);
const
dataJson
=
await
response
.
json
();
this
.
sessionId
=
dataJson
.
data
;
}
catch
(
error
)
{
console
.
error
(
'会话ID获取失败'
,
error
);
}
},
async
getConversationHistory
()
{
try
{
//const response = await fetch('../../api/langchain/getSessionId');
const
response
=
await
fetch
(
'../../api/langchain/conversationHistory'
);
const
dataJson
=
await
response
.
json
();
this
.
historySections
=
dataJson
.
data
;
}
catch
(
error
)
{
console
.
error
(
'会话获取失败'
,
error
);
}
},
// 聊天相关方法
handleKeyPress
(
e
)
{
if
(
e
.
key
===
'Enter'
&&
!
e
.
shiftKey
)
{
e
.
preventDefault
();
this
.
sendMessage
();
}
},
questionClick
:
function
(
message
){
this
.
userInput
=
message
;
this
.
sendMessage
();
},
sendMessage
()
{
if
(
!
this
.
selectedExpert
)
{
this
.
showToast
(
'请先选择专家类型(内部/外部专家)'
);
return
;
}
const
message
=
this
.
userInput
.
trim
();
if
(
!
message
)
return
;
if
(
this
.
fistLoading
)
{
this
.
fistLoading
=
false
;
// 停止自动滚动
this
.
stopAutoScroll
();
}
this
.
questions
=
[];
this
.
stopResponse
();
// 添加用户消息
this
.
addMessage
(
'user'
,
message
);
this
.
userInput
=
''
;
// 显示终止按钮,隐藏发送按钮
this
.
isResponding
=
true
;
// 调用API获取响应
this
.
connectSSE
(
message
);
},
stopResponse
()
{
if
(
this
.
currentEventSource
)
{
this
.
currentEventSource
.
close
();
this
.
currentEventSource
=
null
;
// 切换回发送按钮
this
.
isResponding
=
false
;
// 在最后一条AI消息后添加"(已终止)"标记
const
aiMessages
=
this
.
messages
.
filter
(
msg
=>
msg
.
role
===
'ai'
);
if
(
aiMessages
.
length
>
0
)
{
const
lastAIMessage
=
aiMessages
[
aiMessages
.
length
-
1
];
lastAIMessage
.
typing
=
false
;
if
(
!
lastAIMessage
.
content
.
includes
(
'(已手动结束回答)'
))
{
lastAIMessage
.
content
+=
' (已手动结束回答)'
;
}
}
}
},
addMessage
(
role
,
content
)
{
this
.
messages
.
push
({
role
,
content
});
this
.
chatHistory
.
push
({
role
,
content
});
// 自动滚动到底部
this
.
$nextTick
(()
=>
{
const
messagesDiv
=
document
.
getElementById
(
'scrollContainer'
);
if
(
messagesDiv
)
{
messagesDiv
.
scrollTop
=
messagesDiv
.
scrollHeight
;
}
});
},
connectSSE
(
chatMessage
)
{
// 在消息列表中添加一个带"正在输入"指示器的AI消息
const
aiMessageIndex
=
this
.
messages
.
length
;
this
.
messages
.
push
({
role
:
'ai'
,
content
:
''
,
typing
:
true
});
// 自动滚动到底部
this
.
$nextTick
(()
=>
{
const
messagesDiv
=
document
.
getElementById
(
'scrollContainer'
);
if
(
messagesDiv
)
{
messagesDiv
.
scrollTop
=
messagesDiv
.
scrollHeight
;
}
});
// 连接SSE
this
.
currentEventSource
=
new
EventSource
(
'../../api/langchain/sseFusionIntelligent?chatMessage='
+
chatMessage
+
"&dialogId="
+
this
.
sessionId
+
"&selectedExpert="
+
this
.
selectedExpert
+
"&selectedOrg="
+
this
.
selectedOrg
);
let
responseText
=
''
;
var
md
=
new
markdown
({
html
:
true
,
// 允许解析 HTML 标签
linkify
:
true
,
// 自动识别链接
typographer
:
true
,
// 启用排版优化
breaks
:
true
,
// 将单个换行符视为换行
});
this
.
currentEventSource
.
onmessage
=
(
event
)
=>
{
const
data
=
JSON
.
parse
(
event
.
data
);
const
content
=
data
.
dataToSend
[
1
].
data
;
if
(
content
!==
"stop"
)
{
const
match
=
content
.
match
(
/SUGGEST
\[(
.*
?)\]
SUGGEST/
);
if
(
match
)
{
const
suggestionsJson
=
`[
${
match
[
1
]}
]`
;
const
suggestions
=
JSON
.
parse
(
suggestionsJson
);
if
(
Array
.
isArray
(
suggestions
))
{
this
.
questions
=
suggestions
;
}
}
else
{
//提示词不展示
responseText
+=
content
;
}
this
.
messages
[
aiMessageIndex
].
content
=
marked
.
parse
(
responseText
);
//md.render(responseText);
}
else
{
// 移除输入指示器
this
.
messages
[
aiMessageIndex
].
typing
=
false
;
this
.
chatHistory
.
push
({
role
:
'ai'
,
content
:
responseText
});
// 切换回发送按钮
this
.
isResponding
=
false
;
// 关闭事件源
this
.
currentEventSource
.
close
();
this
.
currentEventSource
=
null
;
}
// 自动滚动到底部
this
.
$nextTick
(()
=>
{
const
messagesDiv
=
document
.
getElementById
(
'scrollContainer'
);
if
(
messagesDiv
)
{
messagesDiv
.
scrollTop
=
messagesDiv
.
scrollHeight
;
}
});
};
this
.
currentEventSource
.
onerror
=
()
=>
{
console
.
error
(
'SSE连接异常'
);
// 移除输入指示器,显示错误信息
this
.
messages
[
aiMessageIndex
].
typing
=
false
;
this
.
messages
[
aiMessageIndex
].
content
=
responseText
||
'抱歉,连接出现问题,请稍后再试。'
;
// 切换回发送按钮
this
.
isResponding
=
false
;
// 关闭事件源
this
.
currentEventSource
.
close
();
this
.
currentEventSource
=
null
;
};
},
clearChat
()
{
// 清空消息,保留欢迎消息
this
.
messages
=
[
{
role
:
'ai'
,
content
:
'您好!我是您的AI助手,有什么可以帮助您的吗?'
}
];
// 清空历史记录
this
.
chatHistory
=
[];
},
newChat
()
{
// 终止当前响应(如果有)
if
(
this
.
currentEventSource
)
{
this
.
currentEventSource
.
close
();
this
.
currentEventSource
=
null
;
this
.
isResponding
=
false
;
}
this
.
questions
=
[];
// 获取新的会话ID并清空对话
this
.
getSessionId
().
then
(()
=>
{
this
.
clearChat
();
// 这里可以添加将新对话添加到历史列表的逻辑
// this.addChatToHistory('新对话', new Date());
});
this
.
fistLoading
=
true
;
this
.
selectedExpert
=
'内部专家'
;
this
.
showOrgSelection
=
this
.
selectedExpert
===
'内部专家'
;
this
.
selectedOrg
=
'全部组织'
;
// 重置滚动位置并重新启动自动滚动
this
.
scrollOffset
=
0
;
this
.
startAutoScroll
();
},
loadChatHistory
(
chat
)
{
var
that
=
this
var
chatId
=
chat
.
id
;
// 关闭所有菜单
this
.
historySections
.
forEach
(
section
=>
{
section
.
items
.
forEach
(
item
=>
{
item
.
showMenu
=
false
;
});
});
//this.addMessage('user', "123456");
// 设置活跃状态
this
.
historySections
.
forEach
(
section
=>
{
section
.
items
.
forEach
(
item
=>
{
item
.
active
=
(
item
.
id
===
chatId
);
});
});
//
this
.
sessionId
=
chatId
;
// this.clearChat();
this
.
questions
=
[];
this
.
messages
=
[];
// 如果正在显示初始推荐,停止自动滚动
if
(
this
.
fistLoading
)
{
this
.
stopAutoScroll
();
}
this
.
fistLoading
=
false
;
this
.
selectedExpert
=
'内部专家'
;
this
.
showOrgSelection
=
this
.
selectedExpert
===
'内部专家'
;
this
.
selectedOrg
=
'全部组织'
;
//这里添加加载对应对话历史的逻辑
//实际实现时可以调用API获取历史记录
$
.
ajax
({
url
:
"../../api/langchain/conversationMessages?sessionId="
+
chatId
,
type
:
"get"
,
dataType
:
"json"
,
contentType
:
"application/json;charset=UTF-8"
,
async
:
false
,
success
:
function
(
data
)
{
var
conversationMessages
=
data
.
data
;
if
(
conversationMessages
&&
conversationMessages
.
length
>
0
){
that
.
fistLoading
=
false
;
conversationMessages
.
forEach
(
section
=>
{
that
.
addMessage
(
section
.
role
,
section
.
content
);
});
}
}
});
},
// 删除对话
deleteChat
(
chatId
)
{
// 关闭菜单
this
.
historySections
.
forEach
(
section
=>
{
const
item
=
section
.
items
.
find
(
item
=>
item
.
id
===
chatId
);
if
(
item
)
{
item
.
showMenu
=
false
;
}
});
// 从历史记录中删除
this
.
historySections
.
forEach
(
section
=>
{
section
.
items
=
section
.
items
.
filter
(
item
=>
item
.
id
!==
chatId
);
});
// 如果删除的是当前活跃的对话,则清空聊天记录
const
isActiveChat
=
this
.
historySections
.
some
(
section
=>
section
.
items
.
some
(
item
=>
item
.
id
===
chatId
&&
item
.
active
)
);
if
(
isActiveChat
)
{
this
.
clearChat
();
}
$
.
ajax
({
url
:
"../../api/langchain/deleteChat"
,
type
:
"post"
,
dataType
:
"json"
,
contentType
:
"application/json;charset=UTF-8"
,
data
:
chatId
,
async
:
false
,
success
:
function
(
data
)
{
}
});
},
// 专家选择相关方法
toggleExpertDropdown
()
{
this
.
showExpertDropdown
=
!
this
.
showExpertDropdown
;
},
closeExpertDropdown
()
{
this
.
showExpertDropdown
=
false
;
},
selectExpert
(
expert
)
{
// this.selectedExpert = expert;
if
(
this
.
selectedExpert
===
expert
)
{
this
.
selectedExpert
=
''
;
}
else
{
this
.
selectedExpert
=
expert
;
}
this
.
showOrgSelection
=
expert
===
'内部专家'
;
this
.
selectedOrg
=
'全部组织'
;
// 立即关闭下拉框
this
.
showExpertDropdown
=
false
;
},
//组织内外选择相关方法
toggleOrgDropdown
()
{
this
.
showOrgDropdown
=
!
this
.
showOrgDropdown
;
},
closeOrgDropdown
()
{
this
.
showOrgDropdown
=
false
;
},
selectOrg
(
org
)
{
this
.
selectedOrg
=
org
;
// 立即关闭下拉框
this
.
showOrgDropdown
=
false
;
},
showToast
(
message
)
{
// 使用你项目中已有的提示组件,或创建一个简单的提示
const
toast
=
document
.
createElement
(
'div'
);
toast
.
className
=
'expert-toast'
;
toast
.
textContent
=
message
;
document
.
body
.
appendChild
(
toast
);
setTimeout
(()
=>
{
toast
.
remove
();
},
3000
);
}
}
});
});
\ No newline at end of file
src/main/resources/static/pages/fusion/style/ai-chat.css
0 → 100644
View file @
9a0d119d
/* 全局样式重置 */
*
{
margin
:
0
;
padding
:
0
;
box-sizing
:
border-box
;
font-family
:
'PingFang SC'
,
'Microsoft YaHei'
,
sans-serif
;
}
:root
{
/* 浅色主题变量 */
--bg-color
:
#f5f7fa
;
--sidebar-bg
:
#fff
;
--sidebar-border
:
#e8e8e8
;
--primary-color
:
#1890ff
;
--primary-hover
:
#40a9ff
;
--text-color
:
#333
;
--text-secondary
:
#8c8c8c
;
--message-bg
:
#fff
;
--message-user-bg
:
#f5f7fa
;
--message-border
:
#e8e8e8
;
--input-area-bg
:
#fff
;
--hover-bg
:
#f0f2f5
;
--active-bg
:
#e6f7ff
;
--avatar-bg
:
#1890ff
;
--avatar-user-bg
:
#722ed1
;
--scrollbar-track
:
#f1f1f1
;
--scrollbar-thumb
:
#c1c1c1
;
--scrollbar-hover
:
#a8a8a8
;
--box-shadow
:
rgba
(
0
,
0
,
0
,
0.08
);
--question-text
:
#676c90
;
}
.dark-theme
{
/* 深色主题变量 */
--bg-color
:
#1f1f1f
;
--sidebar-bg
:
#2d2d2d
;
--sidebar-border
:
#3a3a3a
;
--primary-color
:
#177ddc
;
--primary-hover
:
#3c9ae8
;
--text-color
:
#e0e0e0
;
--text-secondary
:
#aaaaaa
;
--message-bg
:
#2d2d2d
;
--message-user-bg
:
#363636
;
--message-border
:
#404040
;
--input-area-bg
:
#363636
;
--hover-bg
:
#404040
;
--active-bg
:
#1a365d
;
--avatar-bg
:
#177ddc
;
--avatar-user-bg
:
#722ed1
;
--scrollbar-track
:
#2d2d2d
;
--scrollbar-thumb
:
#555555
;
--scrollbar-hover
:
#777777
;
--box-shadow
:
rgba
(
0
,
0
,
0
,
0.2
);
--question-text
:
#dbdef8
;
}
body
{
background-color
:
var
(
--bg-color
);
color
:
var
(
--text-color
);
line-height
:
1.6
;
height
:
100vh
;
overflow
:
hidden
;
}
/* 主容器样式 */
.main-container
{
display
:
flex
;
height
:
100vh
;
width
:
100%
;
overflow
:
hidden
;
}
/* 左侧边栏样式 */
.sidebar
{
width
:
280px
;
background-color
:
var
(
--sidebar-bg
);
border-right
:
1px
solid
var
(
--sidebar-border
);
display
:
flex
;
flex-direction
:
column
;
overflow
:
hidden
;
}
.sidebar-header
{
padding
:
16px
;
border-bottom
:
1px
solid
var
(
--sidebar-border
);
position
:
relative
;
}
.sidebar-header
h1
{
font-size
:
1.2rem
;
font-weight
:
600
;
color
:
var
(
--primary-color
);
margin-bottom
:
16px
;
}
.new-chat-btn
{
width
:
100%
;
padding
:
10px
;
background-color
:
var
(
--primary-color
);
color
:
white
;
border
:
none
;
border-radius
:
6px
;
cursor
:
pointer
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
font-weight
:
500
;
transition
:
background-color
0.3s
;
}
.new-chat-btn
:hover
{
background-color
:
var
(
--primary-hover
);
}
.plus-icon
{
margin-right
:
8px
;
font-size
:
16px
;
font-weight
:
bold
;
}
/* 主题切换按钮 */
.theme-toggle
{
position
:
absolute
;
right
:
16px
;
top
:
16px
;
cursor
:
pointer
;
width
:
30px
;
height
:
30px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
border-radius
:
50%
;
transition
:
background-color
0.2s
;
}
.theme-toggle
:hover
{
background-color
:
var
(
--hover-bg
);
}
.theme-toggle
svg
{
width
:
18px
;
height
:
18px
;
fill
:
var
(
--text-secondary
);
}
/* 历史对话列表样式 */
.history-list
{
flex
:
1
;
overflow-y
:
auto
;
padding
:
10px
;
scrollbar-width
:
thin
;
scrollbar-color
:
var
(
--scrollbar-thumb
)
var
(
--scrollbar-track
);
max-height
:
calc
(
100vh
-
90px
);
}
.history-section
{
margin-bottom
:
16px
;
}
.time-label
{
font-size
:
0.8rem
;
color
:
var
(
--text-secondary
);
padding
:
5px
10px
;
margin-bottom
:
5px
;
}
.history-item
{
padding
:
10px
;
border-radius
:
6px
;
cursor
:
pointer
;
transition
:
background-color
0.2s
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
}
.history-item
:hover
{
background-color
:
var
(
--hover-bg
);
}
.history-item.active
{
background-color
:
var
(
--active-bg
);
border-left
:
3px
solid
var
(
--primary-color
);
}
.history-title
{
font-size
:
0.9rem
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
max-width
:
180px
;
color
:
var
(
--text-color
);
}
.delete-btn
{
background
:
none
;
border
:
none
;
cursor
:
pointer
;
padding
:
5px
;
opacity
:
0.5
;
transition
:
opacity
0.2s
;
color
:
red
;
}
.delete-btn
:hover
{
opacity
:
1
;
}
.delete-btn
svg
{
display
:
block
;
}
.history-time
{
font-size
:
0.75rem
;
color
:
var
(
--text-secondary
);
}
/* 右侧聊天区域 */
.fist-loading
{
width
:
900px
;
margin
:
8%
auto
10px
;
}
.fist-loading
>
p
{
color
:
var
(
--primary-color
);
font-weight
:
bold
;
font-size
:
20px
;
width
:
500px
;
margin
:
0
auto
;
}
.expert-guides-container
{
width
:
500px
;
height
:
240px
;
/* 高度为每条推荐项高度的3倍 */
margin
:
0
auto
;
overflow
:
hidden
;
position
:
relative
;
}
.expert-guides-scroll
{
width
:
100%
;
position
:
absolute
;
transition
:
transform
0.5s
ease
;
}
.expert-guide-item
{
width
:
500px
;
height
:
70px
;
line-height
:
70px
;
border-radius
:
16px
;
padding
:
0
16px
;
background-color
:
var
(
--sidebar-bg
);
border
:
1px
solid
var
(
--box-shadow
);
/*box-shadow: 0 1px 8px var(--box-shadow);*/
/*box-sizing: border-box;*/
align-items
:
center
;
margin
:
10px
auto
0
;
font-size
:
16px
;
cursor
:
pointer
;
color
:
var
(
--question-text
);
}
.expert-guide-item
:hover
{
background-color
:
#e5eaf6
;
color
:
#475ada
;
}
.chat-area
{
flex
:
1
;
display
:
flex
;
flex-direction
:
column
;
position
:
relative
;
height
:
100vh
;
overflow-y
:
auto
;
/* 使整个聊天区域可滚动,滚动条显示在最右边 */
overflow-x
:
hidden
;
background-color
:
var
(
--bg-color
);
}
.scrollContainer
{
width
:
100%
;
overflow-y
:
auto
;
/* 让内容继续流动 */
height
:
calc
(
100vh
-
150px
);
/* 确保占据足够高度 */
overflow-x
:
hidden
;
padding-top
:
20px
;
}
/* 聊天消息区域 */
.chat-messages
{
width
:
900px
;
margin
:
0
auto
10px
;
}
/* 输入区域 */
.input-area
{
width
:
900px
;
max-width
:
95%
;
margin
:
0
auto
30px
auto
;
padding
:
8px
10px
;
display
:
flex
;
background-color
:
var
(
--input-area-bg
);
border-radius
:
15px
;
box-shadow
:
0
1px
8px
var
(
--box-shadow
);
position
:
sticky
;
bottom
:
30px
;
flex-shrink
:
0
;
/* 防止被压缩 */
flex-direction
:
column
;
justify-content
:
flex-start
;
}
.input-area-content
{
display
:
flex
;
justify-content
:
space-between
;
padding-left
:
10px
;
}
/* 单条消息样式 */
.message
{
display
:
flex
;
margin-bottom
:
20px
;
align-items
:
flex-start
;
}
/* 用户消息 */
.question
{
display
:
flex
;
justify-content
:
flex-start
;
}
.question_left
{
width
:
40px
;
margin
:
0
10px
;
}
.question_right
{
padding
:
0
6px
;
}
.question_right
>
p
{
font-size
:
14px
;
color
:
var
(
--question-text
);
}
.questionContent
{
font-size
:
13px
;
color
:
var
(
--question-text
);
background-color
:
var
(
--input-area-bg
);
border-radius
:
6px
;
box-shadow
:
0
16px
20px
0
rgba
(
174
,
167
,
223
,
.06
);
line-height
:
20px
;
max-width
:
800px
;
overflow
:
hidden
;
padding
:
8px
16px
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
width
:
-moz-fit-content
;
width
:
fit-content
;
margin
:
10px
0
;
cursor
:
pointer
;
}
.questionContent
:hover
{
background-color
:
#e5eaf6
;
color
:
#475ada
;
}
.user-message
{
flex-direction
:
row-reverse
;
}
.avatar
{
width
:
40px
;
height
:
40px
;
border-radius
:
50%
;
background-color
:
var
(
--avatar-bg
);
color
:
white
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
font-weight
:
bold
;
flex-shrink
:
0
;
margin
:
0
10px
;
}
.user-message
.avatar
{
background-color
:
var
(
--avatar-user-bg
);
}
.message
.content
{
max-width
:
87%
;
padding
:
12px
16px
;
border-radius
:
12px
;
box-shadow
:
0
1px
2px
var
(
--box-shadow
);
}
.ai-message
.content
{
background-color
:
var
(
--message-bg
);
border-top-left-radius
:
0
;
}
.user-message
.content
{
background-color
:
var
(
--message-user-bg
);
border
:
1px
solid
var
(
--message-border
);
color
:
var
(
--text-color
);
border-top-right-radius
:
0
;
}
.content
p
{
white-space
:
pre-wrap
;
word-break
:
break-word
;
color
:
var
(
--text-color
);
}
#user-input
{
flex
:
1
;
padding
:
8px
10px
;
border
:
none
;
border-radius
:
0
;
resize
:
none
;
outline
:
none
;
height
:
100px
;
max-height
:
150px
;
background-color
:
transparent
;
transition
:
all
0.3s
;
margin-right
:
8px
;
line-height
:
20px
;
box-shadow
:
none
;
overflow-y
:
auto
;
color
:
var
(
--text-color
);
}
#user-input
:focus
{
box-shadow
:
none
;
}
#user-input
::placeholder
{
color
:
var
(
--text-secondary
);
}
.send-btn
,
.stop-btn
{
width
:
36px
;
height
:
36px
;
min-height
:
36px
;
background-color
:
var
(
--primary-color
);
color
:
white
;
border
:
none
;
border-radius
:
50%
;
cursor
:
pointer
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
transition
:
all
0.3s
;
padding
:
0
;
}
.send-btn
:hover
,
.stop-btn
:hover
{
background-color
:
var
(
--primary-hover
);
}
.stop-btn
{
background-color
:
#ff4d4f
;
}
.stop-btn
:hover
{
background-color
:
#ff7875
;
}
.stop-icon
,
.send-icon
{
width
:
18px
;
height
:
18px
;
stroke
:
white
;
}
/* 打字指示器样式 */
.typing-indicator
{
display
:
inline-block
;
position
:
relative
;
min-height
:
20px
;
font-size
:
25px
;
font-weight
:
bold
;
}
.typing-indicator
::after
{
content
:
"···"
;
animation
:
typing
1.5s
infinite
;
}
@keyframes
typing
{
0
%,
100
%
{
content
:
"."
;
}
33
%
{
content
:
".."
;
}
66
%
{
content
:
"..."
;
}
}
@media
(
max-width
:
1190px
){
.chat-messages
{
width
:
800px
;
}
}
@media
(
max-width
:
1090px
){
.chat-messages
{
width
:
700px
;
}
}
@media
(
max-width
:
990px
){
.chat-messages
{
width
:
600px
;
}
}
/* 响应式设计 */
@media
(
max-width
:
768px
)
{
.sidebar
{
position
:
absolute
;
left
:
-280px
;
height
:
100vh
;
z-index
:
10
;
transition
:
left
0.3s
;
}
.sidebar.visible
{
left
:
0
;
}
.chat-messages
,
.input-area
{
width
:
100%
;
max-width
:
100%
;
}
.message
.content
{
max-width
:
85%
;
}
}
/* 滚动条样式 */
::-webkit-scrollbar
{
width
:
6px
;
}
::-webkit-scrollbar-track
{
background
:
var
(
--scrollbar-track
);
}
::-webkit-scrollbar-thumb
{
background
:
var
(
--scrollbar-thumb
);
border-radius
:
3px
;
}
::-webkit-scrollbar-thumb:hover
{
background
:
var
(
--scrollbar-hover
);
}
/* 动画效果 */
.message
{
animation
:
fadeIn
0.3s
ease-out
;
}
@keyframes
fadeIn
{
from
{
opacity
:
0
;
transform
:
translateY
(
10px
);
}
to
{
opacity
:
1
;
transform
:
translateY
(
0
);
}
}
/* 自定义选择器样式 */
.custom-select
{
position
:
relative
;
width
:
auto
;
min-width
:
70px
;
cursor
:
pointer
;
user-select
:
none
;
margin-right
:
10px
;
margin-left
:
0
;
}
.selected-option
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
padding
:
2px
5px
;
border-radius
:
8px
;
/*background-color: rgba(230, 220, 250, 0.5);*/
background-color
:
#f5f6fa
;
/*color: #6633ff;*/
color
:
#4c4c4c
;
font-size
:
14px
;
transition
:
all
0.2s
;
}
.selected-option
span
{
margin-right
:
2px
;
text-align
:
center
;
display
:
inline-block
;
width
:
100%
;
}
.dark-theme
.selected-option
{
background-color
:
rgba
(
130
,
100
,
200
,
0.2
);
color
:
#9980ff
;
}
.selected-option
:hover
{
background-color
:
rgba
(
210
,
200
,
250
,
0.7
);
}
.dark-theme
.selected-option
:hover
{
background-color
:
rgba
(
150
,
120
,
220
,
0.3
);
}
.dropdown-icon
{
transition
:
transform
0.2s
;
margin-left
:
2px
;
flex-shrink
:
0
;
stroke
:
#6633ff
;
}
.dark-theme
.dropdown-icon
{
stroke
:
#9980ff
;
}
.dropdown-icon.rotated
{
transform
:
rotate
(
180deg
);
}
.dropdown-menu
{
position
:
absolute
;
bottom
:
100%
;
left
:
0
;
width
:
100%
;
margin-bottom
:
5px
;
background-color
:
var
(
--message-bg
);
border-radius
:
8px
;
box-shadow
:
0
2px
10px
var
(
--box-shadow
);
z-index
:
100
;
overflow
:
hidden
;
}
.dropdown-item
{
padding
:
6px
8px
;
transition
:
background-color
0.2s
;
font-size
:
13px
;
color
:
var
(
--text-color
);
text-align
:
center
;
}
.dropdown-item
:hover
{
background-color
:
var
(
--hover-bg
);
}
.dropdown-item.active
{
background-color
:
var
(
--active-bg
);
font-weight
:
500
;
}
/* 输入区域内容样式 */
.input-area-content
{
display
:
flex
;
align-items
:
center
;
}
.selected-option.active
{
/* 选中样式 */
/*background-color: #f5f6fa;*/
background-color
:
rgba
(
230
,
220
,
250
,
0.5
);
/*color: #272933;*/
color
:
#6633ff
;
}
.expert-toast
{
position
:
fixed
;
left
:
50%
;
top
:
65%
;
/* 垂直居中 */
background-color
:
rgba
(
230
,
220
,
250
,
0.5
);
color
:
#6633ff
;
padding
:
10px
20px
;
border-radius
:
4px
;
z-index
:
1000
;
}
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment