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
2223a16f
Commit
2223a16f
authored
Apr 21, 2025
by
姜耀祖
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
前端代码修改
parent
89e233a0
Pipeline
#21044
passed with stages
in 3 minutes and 6 seconds
Changes
4
Pipelines
1
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
440 additions
and
142 deletions
+440
-142
LangChainController.java
...chassistant/controller/langchain/LangChainController.java
+188
-0
ai-chat.html
src/main/resources/static/pages/langchain/ai-chat.html
+46
-24
ai-chat-vue.js
src/main/resources/static/pages/langchain/js/ai-chat-vue.js
+69
-7
ai-chat.css
src/main/resources/static/pages/langchain/style/ai-chat.css
+137
-111
No files found.
src/main/java/com/infoepoch/pms/dispatchassistant/controller/langchain/LangChainController.java
View file @
2223a16f
This diff is collapsed.
Click to expand it.
src/main/resources/static/pages/langchain/ai-chat.html
View file @
2223a16f
...
...
@@ -50,14 +50,15 @@
</div>
</div>
</div>
<!-- 中间聊天区域 -->
<div
class=
"chat-wrapper"
>
<div
class=
"chat-container"
>
<!-- 消息历史区域 -->
<!-- 右侧聊天区域 -->
<div
class=
"chat-area"
>
<!-- 消息历史区域 -->
<div
id=
"scrollContainer"
class=
"scrollContainer"
>
<div
id=
"chat-messages"
class=
"chat-messages"
>
<div
v-for=
"(message, index) in messages"
:key=
"index"
class=
"message"
<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"
>
...
...
@@ -66,23 +67,44 @@
</div>
</div>
</div>
<!-- 输入区域 -->
<div
class=
"input-area"
>
<textarea
id=
"user-input"
v-model=
"userInput"
placeholder=
"请输入您的问题..."
rows=
"2"
></textarea>
<button
id=
"send-btn"
class=
"send-btn"
@
click=
"sendMessage"
v-show=
"!isResponding"
>
<svg
viewBox=
"0 0 24 24"
width=
"24"
height=
"24"
stroke=
"currentColor"
stroke-width=
"2"
fill=
"none"
stroke-linecap=
"round"
stroke-linejoin=
"round"
class=
"send-icon"
>
<line
x1=
"22"
y1=
"2"
x2=
"11"
y2=
"13"
></line>
<polygon
points=
"22 2 15 22 11 13 2 9 22 2"
></polygon>
</svg>
</button>
<button
id=
"stop-btn"
class=
"stop-btn"
@
click=
"stopResponse"
v-show=
"isResponding"
>
<svg
viewBox=
"0 0 24 24"
width=
"24"
height=
"24"
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
class=
"input-area"
>
<textarea
id=
"user-input"
v-model=
"userInput"
placeholder=
"请输入您的问题 shift+enter换行"
rows=
"3"
></textarea>
<div
class=
"input-area-content"
>
<div>
<div
class=
"custom-select"
v-click-outside=
"closeExpertDropdown"
>
<div
class=
"selected-option"
@
click
.
stop=
"toggleExpertDropdown"
>
<span>
{{ selectedExpert }}
</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>
<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>
...
...
src/main/resources/static/pages/langchain/js/ai-chat-vue.js
View file @
2223a16f
...
...
@@ -2,6 +2,21 @@
* AI聊天页面Vue应用
*/
require
([
'jquery'
,
'vue'
,
'utils'
,
'echarts'
,
'global'
],
function
(
$
,
Vue
,
utils
,
echarts
)
{
// 添加点击外部关闭指令
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
:
{
...
...
@@ -18,6 +33,10 @@ require(['jquery', 'vue', 'utils', 'echarts', 'global'], function ($, Vue, utils
isResponding
:
false
,
userInput
:
""
,
// 专家选择相关数据
selectedExpert
:
'内部专家'
,
showExpertDropdown
:
false
,
// 历史对话分类
historySections
:
[
{
...
...
@@ -68,6 +87,7 @@ require(['jquery', 'vue', 'utils', 'echarts', 'global'], function ($, Vue, utils
this
.
getSessionId
();
},
mounted
:
function
()
{
this
.
pageEvent
();
// 监听输入框的键盘事件
this
.
$nextTick
(()
=>
{
const
textarea
=
document
.
getElementById
(
'user-input'
);
...
...
@@ -82,9 +102,37 @@ require(['jquery', 'vue', 'utils', 'echarts', 'global'], function ($, Vue, utils
this
.
closeAllMenus
();
}
});
// // 获取滚动容器和消息容器
// const scrollContainer = document.getElementById('chat-messages').parentElement;
// const chatMessages = document.getElementById('chat-messages');
// // 创建MutationObserver监听消息变化
// const observer = new MutationObserver(function(mutations) {
// mutations.forEach(mutation => {
// if (mutation.addedNodes.length) {
// // 使用延时确保内容已渲染
// setTimeout(() => {
// scrollContainer.scrollTo({
// top: scrollContainer.scrollHeight,
// behavior: 'smooth' // 可选平滑滚动
// });
// }, 50);
// }
// });
// });
//
// // 开始观察子元素变化
// observer.observe(chatMessages, {
// childList: true
// });
});
},
methods
:
{
pageEvent
:
function
(){
// 确保在DOM加载完成后执行
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
});
},
// 用户相关方法
currentUser
:
function
()
{
const
that
=
this
;
...
...
@@ -130,7 +178,8 @@ require(['jquery', 'vue', 'utils', 'echarts', 'global'], function ($, Vue, utils
sendMessage
()
{
const
message
=
this
.
userInput
.
trim
();
if
(
!
message
)
return
;
this
.
stopResponse
();
// 添加用户消息
this
.
addMessage
(
'user'
,
message
);
this
.
userInput
=
''
;
...
...
@@ -146,7 +195,7 @@ require(['jquery', 'vue', 'utils', 'echarts', 'global'], function ($, Vue, utils
if
(
this
.
currentEventSource
)
{
this
.
currentEventSource
.
close
();
this
.
currentEventSource
=
null
;
// 切换回发送按钮
this
.
isResponding
=
false
;
...
...
@@ -154,8 +203,9 @@ require(['jquery', 'vue', 'utils', 'echarts', 'global'], function ($, Vue, utils
const
aiMessages
=
this
.
messages
.
filter
(
msg
=>
msg
.
role
===
'ai'
);
if
(
aiMessages
.
length
>
0
)
{
const
lastAIMessage
=
aiMessages
[
aiMessages
.
length
-
1
];
if
(
!
lastAIMessage
.
content
.
includes
(
'(已终止)'
))
{
lastAIMessage
.
content
+=
' (已终止)'
;
lastAIMessage
.
typing
=
false
;
if
(
!
lastAIMessage
.
content
.
includes
(
'(已手动结束回答)'
))
{
lastAIMessage
.
content
+=
' (已手动结束回答)'
;
}
}
}
...
...
@@ -167,7 +217,7 @@ require(['jquery', 'vue', 'utils', 'echarts', 'global'], function ($, Vue, utils
// 自动滚动到底部
this
.
$nextTick
(()
=>
{
const
messagesDiv
=
document
.
getElementById
(
'
chat-messages
'
);
const
messagesDiv
=
document
.
getElementById
(
'
scrollContainer
'
);
if
(
messagesDiv
)
{
messagesDiv
.
scrollTop
=
messagesDiv
.
scrollHeight
;
}
...
...
@@ -181,7 +231,7 @@ require(['jquery', 'vue', 'utils', 'echarts', 'global'], function ($, Vue, utils
// 自动滚动到底部
this
.
$nextTick
(()
=>
{
const
messagesDiv
=
document
.
getElementById
(
'
chat-messages
'
);
const
messagesDiv
=
document
.
getElementById
(
'
scrollContainer
'
);
if
(
messagesDiv
)
{
messagesDiv
.
scrollTop
=
messagesDiv
.
scrollHeight
;
}
...
...
@@ -212,7 +262,7 @@ require(['jquery', 'vue', 'utils', 'echarts', 'global'], function ($, Vue, utils
// 自动滚动到底部
this
.
$nextTick
(()
=>
{
const
messagesDiv
=
document
.
getElementById
(
'
chat-messages
'
);
const
messagesDiv
=
document
.
getElementById
(
'
scrollContainer
'
);
if
(
messagesDiv
)
{
messagesDiv
.
scrollTop
=
messagesDiv
.
scrollHeight
;
}
...
...
@@ -348,6 +398,18 @@ require(['jquery', 'vue', 'utils', 'echarts', 'global'], function ($, Vue, utils
if
(
isActiveChat
)
{
this
.
clearChat
();
}
},
// 专家选择相关方法
toggleExpertDropdown
()
{
this
.
showExpertDropdown
=
!
this
.
showExpertDropdown
;
},
closeExpertDropdown
()
{
this
.
showExpertDropdown
=
false
;
},
selectExpert
(
expert
)
{
this
.
selectedExpert
=
expert
;
// 立即关闭下拉框
this
.
showExpertDropdown
=
false
;
}
}
});
...
...
src/main/resources/static/pages/langchain/style/ai-chat.css
View file @
2223a16f
...
...
@@ -65,6 +65,7 @@ body {
display
:
flex
;
height
:
100vh
;
width
:
100%
;
overflow
:
hidden
;
}
/* 左侧边栏样式 */
...
...
@@ -212,101 +213,54 @@ body {
color
:
var
(
--text-secondary
);
}
/*
聊天包装器样式 - 新增
*/
.chat-
wrapper
{
/*
右侧聊天区域
*/
.chat-
area
{
flex
:
1
;
display
:
flex
;
flex-direction
:
column
;
justify-content
:
flex-start
;
align-items
:
center
;
background-color
:
var
(
--bg-color
);
padding
:
0
;
position
:
relative
;
height
:
100vh
;
overflow
:
hidden
;
}
/* 聊天容器样式 */
.chat-container
{
width
:
900px
;
max-width
:
95%
;
height
:
100%
;
display
:
flex
;
flex-direction
:
column
;
overflow-y
:
auto
;
/* 使整个聊天区域可滚动,滚动条显示在最右边 */
overflow-x
:
hidden
;
background-color
:
var
(
--bg-color
);
overflow
:
hidden
;
position
:
relative
;
padding-bottom
:
80px
;
/* 为底部输入框留出空间 */
}
/* 聊天头部样式 */
.chat-header
{
padding
:
15px
20px
;
background-color
:
#fff
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
border-bottom
:
1px
solid
#e8e8e8
;
box-shadow
:
0
1px
2px
rgba
(
0
,
0
,
0
,
0.05
);
}
.current-chat-info
{
display
:
flex
;
align-items
:
center
;
.scrollContainer
{
width
:
100%
;
overflow-y
:
auto
;
/* 让内容继续流动 */
height
:
calc
(
100vh
-
150px
);
/* 确保占据足够高度 */
}
.current-chat-title
{
font-size
:
1rem
;
font-weight
:
600
;
color
:
#333
;
/* 聊天消息区域 */
.chat-messages
{
width
:
900px
;
max-width
:
95%
;
margin
:
0
auto
;
padding
:
20px
;
margin-bottom
:
10px
;
}
.chat-info
{
/* 输入区域 */
.input-area
{
width
:
900px
;
max-width
:
95%
;
margin
:
0
auto
30px
auto
;
padding
:
8px
10px
;
display
:
flex
;
align-items
:
center
;
gap
:
15px
;
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
;
}
.
status
{
.
input-area-content
{
display
:
flex
;
align-items
:
center
;
font-size
:
0.9rem
;
}
.status
::before
{
content
:
''
;
display
:
inline-block
;
width
:
8px
;
height
:
8px
;
margin-right
:
5px
;
border-radius
:
50%
;
}
.status.online
::before
{
background-color
:
#52c41a
;
}
.clear-btn
{
padding
:
6px
12px
;
background-color
:
#f0f0f0
;
color
:
#595959
;
border
:
none
;
border-radius
:
4px
;
cursor
:
pointer
;
transition
:
background-color
0.3s
;
}
.clear-btn
:hover
{
background-color
:
#e0e0e0
;
}
/* 消息区域样式 */
.chat-messages
{
flex
:
1
;
padding
:
20px
;
overflow-y
:
auto
;
background-color
:
var
(
--bg-color
);
height
:
calc
(
100vh
-
80px
);
justify-content
:
space-between
;
padding-left
:
10px
;
}
/* 单条消息样式 */
...
...
@@ -363,23 +317,6 @@ body {
word-break
:
break-word
;
}
/* 输入区域样式 */
.input-area
{
display
:
flex
;
padding
:
8px
10px
;
background-color
:
var
(
--input-area-bg
);
align-items
:
center
;
position
:
absolute
;
bottom
:
30px
;
left
:
20px
;
right
:
20px
;
width
:
calc
(
100%
-
40px
);
z-index
:
10
;
box-shadow
:
0
1px
8px
var
(
--box-shadow
);
border-radius
:
15px
;
border-top
:
none
;
}
#user-input
{
flex
:
1
;
padding
:
8px
10px
;
...
...
@@ -387,7 +324,7 @@ body {
border-radius
:
0
;
resize
:
none
;
outline
:
none
;
height
:
7
0px
;
height
:
10
0px
;
max-height
:
150px
;
background-color
:
transparent
;
transition
:
all
0.3s
;
...
...
@@ -407,13 +344,13 @@ body {
}
.send-btn
,
.stop-btn
{
width
:
44
px
;
height
:
44
px
;
min-height
:
44
px
;
width
:
36
px
;
height
:
36
px
;
min-height
:
36
px
;
background-color
:
var
(
--primary-color
);
color
:
white
;
border
:
none
;
border-radius
:
12px
;
border-radius
:
50%
;
cursor
:
pointer
;
display
:
flex
;
align-items
:
center
;
...
...
@@ -434,9 +371,9 @@ body {
background-color
:
#ff7875
;
}
.stop-icon
{
width
:
20
px
;
height
:
20
px
;
.stop-icon
,
.send-icon
{
width
:
18
px
;
height
:
18
px
;
stroke
:
white
;
}
...
...
@@ -473,15 +410,10 @@ body {
left
:
0
;
}
.chat-wrapper
{
padding
:
10px
;
}
.chat-container
{
.chat-messages
,
.input-area
{
width
:
100%
;
max-width
:
100%
;
height
:
100vh
;
border-radius
:
0
;
}
.message
.content
{
...
...
@@ -521,4 +453,98 @@ body {
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
);
color
:
#6633ff
;
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
;
}
\ 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