Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
A
ai-chat-ui
Project
Project
Details
Activity
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
chenghong_tao
ai-chat-ui
Commits
b856ece6
Commit
b856ece6
authored
Apr 24, 2025
by
chenghong_tao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
优化部分代码
parent
3213252c
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
104 additions
and
29 deletions
+104
-29
README.md
README.md
+8
-8
embed.js
iframePlugin/embed.js
+10
-4
Chat.vue
src/components/Chat.vue
+7
-3
aiBubble.vue
src/components/chatSubassembly/aiBubble.vue
+79
-14
No files found.
README.md
View file @
b856ece6
...
...
@@ -17,10 +17,11 @@ npm run dev
## iframe/script 模式
### 注意事项
1.
使用embed模式时,需要手动修改
`iframePlugin/embed.js`
中的域名或IP地址。
```
javascript
// 大概21行
img
.
src
=
'https://tk-test.infi-inside.com:2443/deepseek.png'
;
// https://tk-test.infi-inside.com:2443修改成自己的ip或域名
// 大概77行
iframe
.
src
=
'https://tk-test.infi-inside.com:2443/index.html'
;
// https://tk-test.infi-inside.com:2443修改成自己的ip或域名
```
\ No newline at end of file
1.
使用embed模式时,引入embed.js文件时需要带URL参数
```
html
<script
src=
"/embed.min.js?url=当前前端所在的IP或域名地址"
></script>
```
如:在本地运行前端时,引入embed.js文件时,参数则为url=http://127.0.0.1:3000
```
html
<script
src=
"/embed.min.js?url=http://127.0.0.1:3000"
></script>
```
iframePlugin/embed.js
View file @
b856ece6
(
function
()
{
// 获取URL
const
urlParams
=
new
URLSearchParams
(
window
.
location
.
search
);
const
baseUrl
=
urlParams
.
get
(
'url'
)
||
'https://tk-test.infi-inside.com:2443'
;
// 创建悬浮按钮
const
button
=
document
.
createElement
(
'button'
);
button
.
id
=
'
dify-
chatbot-bubble-button'
;
button
.
id
=
'chatbot-bubble-button'
;
button
.
style
.
position
=
'fixed'
;
button
.
style
.
bottom
=
'20px'
;
button
.
style
.
right
=
'20px'
;
...
...
@@ -18,7 +22,7 @@
// Create an img element
const
img
=
document
.
createElement
(
'img'
);
img
.
src
=
'https://tk-test.infi-inside.com:2443
/deepseek.png'
;
// Replace with the path to your image
img
.
src
=
baseUrl
+
'
/deepseek.png'
;
// Replace with the path to your image
img
.
style
.
width
=
'60px'
;
// img.style.height = '100%';
img
.
style
.
objectFit
=
'cover'
;
...
...
@@ -30,7 +34,7 @@
// 创建iframe容器
const
iframeContainer
=
document
.
createElement
(
'div'
);
iframeContainer
.
id
=
'
dify-
chatbot-bubble-window'
;
iframeContainer
.
id
=
'chatbot-bubble-window'
;
iframeContainer
.
style
.
position
=
'fixed'
;
iframeContainer
.
style
.
bottom
=
'80px'
;
iframeContainer
.
style
.
right
=
'20px'
;
...
...
@@ -74,7 +78,9 @@
// 创建iframe
const
iframe
=
document
.
createElement
(
'iframe'
);
iframe
.
src
=
'https://tk-test.infi-inside.com:2443/index.html'
;
// 替换为你的聊天窗口页面地址
// 获取URL
iframe
.
src
=
baseUrl
;
// 替换为你的聊天窗口页面地址
iframe
.
style
.
width
=
'100%'
;
iframe
.
style
.
height
=
'100%'
;
iframe
.
style
.
border
=
'none'
;
...
...
src/components/Chat.vue
View file @
b856ece6
...
...
@@ -9,7 +9,7 @@
<template
v-for=
"(item, index) in chatMessageList"
:key=
"'chatMessage - ' + index"
>
<div
class=
"chat-message-item"
>
<aiBubble
:query=
"item.
content
"
:query=
"item.
query
"
v-if=
"item.type === 'ai' || item.type === 'ai-history'"
:messageType=
"item.type"
:content=
"item.content"
...
...
@@ -59,11 +59,13 @@ const userQuery = (query, isLoading=false) => {
}
chatMessageList
.
value
.
push
({
type
:
'user'
,
query
:
''
,
content
:
query
,
})
chatMessageList
.
value
.
push
({
type
:
'ai'
,
content
:
query
,
query
,
content
:
null
,
})
if
(
isLoading
)
{
inputMessageRef
.
value
.
changeLoading
(
true
)
...
...
@@ -123,10 +125,12 @@ watch(() => props.historyMsgList, (newValue) => {
newValue
.
forEach
((
item
)
=>
{
chatMessageList
.
value
.
push
({
type
:
'user'
,
content
:
item
?.
query
,
query
:
''
,
content
:
item
.
query
})
chatMessageList
.
value
.
push
({
type
:
'ai-history'
,
query
:
item
.
query
,
content
:
item
?.
answer
,
})
console
.
log
(
'zhisx'
);
...
...
src/components/chatSubassembly/aiBubble.vue
View file @
b856ece6
...
...
@@ -5,13 +5,28 @@
</
template
>
<
template
#
content
>
<div
class=
"custom-bubble-content"
v-adjust-width
>
<workflowItem
<!-- 工作流 -->
<!--
<workflowItem
v-if=
"workflowContent.length > 0"
:workflowContent=
"workflowContent"
ref=
"workflowItemRef"
@
workflow-is-error=
"workflowIsError"
/>
/>
-->
<!-- 使用 MarkdownRenderer 组件渲染 messageContent -->
<div
class=
"icon"
v-if=
"workflowStatus && workflowStatus != 'success'"
>
<div
v-if=
"workflowStatus == 'loading'"
>
<el-icon
style=
"font-size: 18px"
class=
"is-loaidng"
>
<Loading
/>
</el-icon>
<span
class=
"is-loaidng"
>
AI正在思考,请您稍等...
</span>
</div>
<div
v-if=
"workflowStatus == 'error'"
>
<el-icon
style=
"font-size: 18px"
class=
"is-error"
>
<WarningFilled
/>
</el-icon>
<span
class=
"is-error"
>
AI处理失败,请您重试或重新完善问题描述
</span>
</div>
</div>
<MarkdownRenderer
:content=
"messageContent"
/>
</div>
...
...
@@ -52,7 +67,7 @@
</template>
<
script
setup
>
import
{
computed
,
ref
,
watch
}
from
"vue"
;
import
{
Refresh
,
Search
,
Star
,
DocumentCopy
}
from
"@element-plus/icons-vue"
;
import
{
Refresh
,
Search
,
Star
,
DocumentCopy
,
Loading
,
SuccessFilled
,
WarningFilled
}
from
"@element-plus/icons-vue"
;
import
workflowItem
from
"./workflowItem.vue"
;
import
MarkdownRenderer
from
'../markdown/markdownRender.vue'
;
import
{
Bubble
,
useXStream
}
from
"vue-element-plus-x"
;
...
...
@@ -93,6 +108,8 @@ watch(
{
immediate
:
true
}
);
const
isReStartWorkflow
=
ref
(
false
)
const
workflowStatus
=
ref
(
null
)
// 默认支持 SSE 协议
async
function
startSSE
(
query
)
{
try
{
...
...
@@ -118,7 +135,7 @@ async function startSSE(query) {
// 机器人的 content 计算属性
const
workflowContent
=
computed
(()
=>
{
if
(
props
.
messageType
===
'ai-history'
)
{
if
(
props
.
messageType
===
'ai-history'
&&
!
isReStartWorkflow
.
value
)
{
return
[];
}
// 以下是处理非历史消息记录
...
...
@@ -131,18 +148,25 @@ const workflowContent = computed(() => {
const
parsedChunk
=
JSON
.
parse
(
chunk
);
workflowList
.
push
(
parsedChunk
);
if
(
parsedChunk
.
event
===
"workflow_started"
)
{
workflowStatus
.
value
=
'loading'
;
taskStore
.
setTaskId
(
parsedChunk
.
task_id
);
taskStore
.
setConversationId
(
parsedChunk
.
conversation_id
);
taskStore
.
setMessageId
(
parsedChunk
.
message_id
);
taskStore
.
setWorkflowRunId
(
parsedChunk
.
workflow_run_id
);
}
if
(
parsedChunk
.
event
===
"error"
)
{
ElMessage
.
error
(
parsedChunk
.
message
)
workflowStatus
.
value
=
'error'
;
emit
(
'workflow-is-error'
,
true
)
break
;
}
if
(
parsedChunk
.
event
===
"message_end"
)
{
workflowStatus
.
value
=
'success'
;
emits
(
"messageFinished"
,
true
);
}
}
catch
(
error
)
{
// 这个 结束标识 是后端给的,所以这里这样判断
// 实际项目中,以项目需要为准
if
(
chunk
===
"
[DONE]"
)
{
if
(
chunk
===
"[DONE]"
)
{
// 处理数据结束的情况
// console.log('数据接收完毕')
}
else
{
...
...
@@ -155,15 +179,19 @@ const workflowContent = computed(() => {
return
workflowList
;
});
const
workflowItemRef
=
ref
(
null
);
//
const workflowItemRef = ref(null);
const
reStartSSE
=
()
=>
{
workflowItemRef
.
value
.
clearWorkflow
();
isReStartWorkflow
.
value
=
true
;
// if(workflowItemRef.value) {
// workflowItemRef.value.clearWorkflow();
// }
startSSE
(
props
.
query
);
console
.
log
(
'reStartSSE'
,
props
.
query
,
props
.
messageType
,
props
.
content
)
};
// AI回答问题
const
messageContent
=
computed
(()
=>
{
if
(
props
.
messageType
===
'ai-history'
)
{
if
(
props
.
messageType
===
'ai-history'
&&
!
isReStartWorkflow
.
value
)
{
return
props
.
content
;
}
let
messageContent
=
""
;
...
...
@@ -187,11 +215,9 @@ const copyContent = () => {
ElMessage
.
success
(
"复制成功"
);
};
const
workflowIsError
=
(
isError
)
=>
{
emits
(
"workflow-is-error"
,
isError
);
};
// const workflowIsError = (isError) => {
// emits("workflow-is-error", isError);
// };
</
script
>
<
style
lang=
"less"
scoped
>
.message-content {
...
...
@@ -207,4 +233,43 @@ const workflowIsError = (isError) => {
transform: rotate(180deg); /* 旋转 180 度 */
transition: transform 0.3s ease; /* 添加过渡效果,让旋转更平滑 */
}
.icon {
display: flex;
align-items: center;
width: 100%;
line-height: 28px;
background-color: var(--el-color-white);
padding: 5px 0px;
i {
width: 28px;
vertical-align: middle;
}
span {
flex: 1;
vertical-align: middle;
}
.is-success {
color: var(--el-color-success);
}
.is-error {
color: var(--el-color-danger);
}
.is-loaidng {
color: var(--el-color-primary);
animation: spin 1s linear infinite;
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</
style
>
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