Commit a07a0486 authored by chenghong_tao's avatar chenghong_tao

init project

parent d379b45e
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
{
"recommendations": ["Vue.volar"]
}
# ai-chat-ui
# Dify-chat-UI
这是一个基于Dify的Chat UI(聊天界面),主要用于dify的应用接口,可以通过对`src/config.js`中的配置进行修改,来定制化自己的应用。
聊天UI界面
\ No newline at end of file
## 快速开始
1. 安装依赖
```bash
npm install
```
2. 运行项目
```bash
npm run dev
```
## 主题
主题文件在`src/theme`中,你可以根据需要修改主题文件,然后重启项目即可生效。
## 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
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
}
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AiBubble: typeof import('./src/components/chatSubassembly/aiBubble.vue')['default']
Chat: typeof import('./src/components/Chat.vue')['default']
ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElLink: typeof import('element-plus/es')['ElLink']
ElMain: typeof import('element-plus/es')['ElMain']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
Header: typeof import('./src/components/Header.vue')['default']
HistoryList: typeof import('./src/components/HistoryList.vue')['default']
InputMessage: typeof import('./src/components/chatSubassembly/inputMessage.vue')['default']
Layout: typeof import('./src/components/Layout.vue')['default']
MarkdownRender: typeof import('./src/components/markdown/markdownRender.vue')['default']
UserBubble: typeof import('./src/components/chatSubassembly/userBubble.vue')['default']
WelcomeBubble: typeof import('./src/components/chatSubassembly/welcomeBubble.vue')['default']
WorkflowItem: typeof import('./src/components/chatSubassembly/workflowItem.vue')['default']
}
}
import { fileURLToPath } from 'url';
import path, { dirname } from 'path';
import fs from 'fs';
import { minify } from 'terser';
// 获取当前模块的路径
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// 定义源文件路径和目标路径
const sourceFilePath = path.resolve(__dirname, 'iframePlugin/embed.js'); // 替换为实际的文件路径
const targetFilePath = path.resolve(__dirname, 'dist/embed.min.js'); // 替换为压缩后的目标文件路径
// 读取源文件内容
fs.readFile(sourceFilePath, 'utf8', (err, content) => {
if (err) {
console.error('读取文件失败:', err);
return;
}
// 使用 Terser 压缩 JavaScript
minify(content).then((result) => {
// 写入压缩后的文件到目标路径
fs.writeFile(targetFilePath, result.code, (err) => {
if (err) {
console.error('写入文件失败:', err);
} else {
console.log(`文件已成功压缩并保存到 ${targetFilePath}`);
}
});
}).catch((err) => {
console.error('压缩文件失败:', err);
});
});
\ No newline at end of file
(function() {
// 创建悬浮按钮
const button = document.createElement('button');
button.id = 'dify-chatbot-bubble-button';
button.style.position = 'fixed';
button.style.bottom = '20px';
button.style.right = '20px';
button.style.width = '50px';
button.style.height = '50px';
button.style.border = 'none';
button.style.borderRadius = '50%';
button.style.display = 'flex';
button.style.alignItems = 'center';
button.style.justifyContent = 'center';
button.style.fontSize = '18px';
button.style.cursor = 'pointer';
button.style.backgroundColor = 'transparent';
// 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.style.width = '60px';
// img.style.height = '100%';
img.style.objectFit = 'cover';
// Append the img element to the button
button.appendChild(img);
document.body.appendChild(button);
// 创建iframe容器
const iframeContainer = document.createElement('div');
iframeContainer.id = 'dify-chatbot-bubble-window';
iframeContainer.style.position = 'fixed';
iframeContainer.style.bottom = '80px';
iframeContainer.style.right = '20px';
iframeContainer.style.width = '650px'; // 24rem
iframeContainer.style.height = '85%'; // 40rem
iframeContainer.style.display = 'none';
iframeContainer.style.border = '1px solid #9a9a9a';
iframeContainer.style.borderRadius = '8px';
iframeContainer.style.backgroundColor = '#f9f9f9';
iframeContainer.style.zIndex = '9999';
// 创建关闭按钮并插入SVG
const closeButton = document.createElement('button');
closeButton.style.position = 'absolute';
closeButton.style.top = '15px';
closeButton.style.right = '5px';
closeButton.style.width = '20px';
closeButton.style.height = '20px';
closeButton.style.border = 'none';
closeButton.style.backgroundColor = 'transparent';
closeButton.style.cursor = 'pointer';
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("t", "1745391895314");
svg.setAttribute("class", "icon");
svg.setAttribute("viewBox", "0 0 1024 1024");
svg.setAttribute("version", "1.1");
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svg.setAttribute("width", "16");
svg.setAttribute("height", "16");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", "M512 0C229.273 0 0 229.273 0 512s229.273 512 512 512 512-229.273 512-512S794.727 0 512 0z m241.36 701.668a36.571 36.571 0 1 1-51.715 51.715L512 563.715 322.355 753.36a36.571 36.571 0 1 1-51.715-51.715L460.285 512 270.64 322.355a36.571 36.571 0 0 1 51.715-51.715L512 460.285 701.645 270.64a36.571 36.571 0 0 1 51.715 51.715L563.715 512z");
path.setAttribute("fill", "#D81E06");
svg.appendChild(path);
closeButton.appendChild(svg);
iframeContainer.appendChild(closeButton);
// 创建iframe
const iframe = document.createElement('iframe');
iframe.src = 'https://tk-test.infi-inside.com:2443/index.html'; // 替换为你的聊天窗口页面地址
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.border = 'none';
iframe.style.borderRadius = '8px';
iframe.sandbox = 'allow-scripts allow-top-navigation allow-same-origin allow-forms allow-popups allow-modals';
iframeContainer.appendChild(iframe);
document.body.appendChild(iframeContainer);
// 添加按钮点击事件
button.addEventListener('click', () => {
iframeContainer.style.display = iframeContainer.style.display === 'none' ? 'block' : 'none';
});
// 添加关闭按钮点击事件
closeButton.addEventListener('click', () => {
iframeContainer.style.display = 'none';
});
})();
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat iframe test</title>
<style>
body {
background-color: #9a9a9a;
}
</style>
</head>
<body>
<script src="http://10.82.13.89:8080/embed.min.js"></script>
</body>
</html>
\ No newline at end of file
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/avatarAI.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI助手</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "dify-chat",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build && npm run compress-js",
"preview": "vite preview",
"compress-js": "node compressAndCopy.js"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.8.4",
"dify-chat": "file:",
"echarts": "^5.6.0",
"element-plus": "^2.9.7",
"markdown-it": "^14.1.0",
"markdown-it-prism": "^3.0.0",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0",
"terser": "^5.39.0",
"vue": "^3.5.13",
"vue-element-plus-x": "^1.1.6"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.2",
"less": "^4.3.0",
"unplugin-auto-import": "^19.1.2",
"unplugin-vue-components": "^28.5.0",
"vite": "^6.3.1"
}
}
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745313584098" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3055" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M0 512a512 512 0 1 0 1024 0 512 512 0 1 0-1024 0Z" fill="#2C59D6" p-id="3056" data-spm-anchor-id="a313x.search_index.0.i10.2de73a81AhqBQM" class="selected"></path><path d="M716.8 256H307.2a51.2 51.2 0 0 0-51.2 51.2v460.8l102.4-76.8h358.4a51.2 51.2 0 0 0 51.2-51.2V307.2a51.2 51.2 0 0 0-51.2-51.2z m-110.1056 343.0912V358.4H665.6v240.6912H606.72z m-101.8368-55.1424h-72.3712l-14.1568 55.296H358.4l76.032-240.6912h70.4l76.0832 240.6912h-61.9264v-0.1536l-14.1568-55.1424zM467.712 401.408h1.3056c6.4512 24.576 12.8512 53.376 19.2512 77.312l5.2736 20.4032h-49.8432l5.2736-20.4032c6.272-23.936 12.8256-52.096 18.7392-77.312z" fill="#FFFFFF" p-id="3057"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="128px" height="128.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M512.000284 64.170631c-247.580307 0-447.829085 200.248778-447.829084 446.293085A443.733087 443.733087 0 0 0 180.338246 810.097328c64.22752-32.085316 41.30131-4.551109 125.326153-39.765312 87.096841-35.100425 106.951052-47.388418 106.951052-47.388418l1.592888-82.488843s-32.142204-24.462209-42.83731-102.399943c-19.854211 6.087108-27.477318-22.926209-27.477318-42.837309-1.535999-18.318212-12.231104-74.865736 13.767104-70.257739-4.607997-38.229312-9.215995-71.850627-7.679996-90.168839C356.124815 270.506516 417.223448 204.799886 513.536284 200.248778c111.50216 4.551109 155.875469 71.793738 162.019465 134.485258 1.47911 18.318212-1.535999 51.939527-7.679996 90.168839 24.462209-4.551109 13.767103 51.939527 12.231105 70.257739-1.535999 18.375101-7.623107 48.924417-27.477318 42.837309-10.751994 77.937734-42.83731 102.399943-42.83731 102.399943l1.535999 82.488844s19.9111 12.287993 107.007941 47.388418c84.024842 35.157314 61.155522 10.695105 125.326152 41.30131A446.748196 446.748196 0 0 0 959.829369 511.999716C958.29337 264.419409 758.044592 64.170631 512.000284 64.170631M512.000284 1023.999431a511.430827 511.430827 0 0 1-511.999715-511.999715c0-283.192732 228.806984-511.999716 511.999715-511.999716s511.999716 228.806984 511.999716 511.999716-228.806984 511.999716-511.999716 511.999715" fill="#707070" /></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file
<script setup>
import Layout from './components/Layout.vue';
</script>
<template>
<el-config-provider size="default">
<Layout />
</el-config-provider>
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
import request from './request.js';
import difyConfig from '../config.js';
import { useAppStore } from '../store/app.js';
const difyApi = {
// 发送对话消息
chatMessage: () => {
const appStore = useAppStore();
return {
url: `${difyConfig.DIFY_URL}/chat-messages`,
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${difyConfig.API_KEY}`
},
user: appStore.userId || difyConfig.USER_ID
}
},
// 上传文件
filesUpload: async (data) => await request.post('/files/upload', data),
// 停止响应
stop: async (task_id, data) => {
const appStore = useAppStore()
return await request.post(`/chat-messages/${task_id}/stop`, {user: appStore.userId || difyConfig.USER_ID, ...data})
},
// 消息反馈
feedback: async (message_id, data) => await request.post(`/messages/${message_id}/feedbacks`, data),
// 获取下一轮建议列表
getNextSuggestions: async (message_id, params) => await request.get(`/messages/${message_id}/suggested`, params),
// 获取对话历史记录
getChatHistory: async (params) => await request.get('/messages', params),
// 获取会话列表
getChatList: async (params) => await request.get('/conversations', params),
// 删除会话
deleteChat: async (conversation_id) => await request.delete(`/conversations/${conversation_id}`, null, {}),
// 会话重命名
renameChat: async (conversation_id, data) => await request.post(`/conversations/${conversation_id}/name`, data),
// 语音转文字
voiceToText: async (data) => await request.post('/audio-to-text', data),
// 文字转语音
textToVoice: async (data) => await request.post('/text-to-audio', data),
// 获取应用基本信息
getAppInfo: async () => await request.get('/info'),
// 获取应用参数
getAppParams: async () => await request.get('/parameters'),
// 获取应用Meta信息
getAppMeta: async () => await request.get('/meta')
}
export default difyApi
\ No newline at end of file
import axios from 'axios';
import difyConfig from '../config';
import { useAppStore } from '../store/app';
// 创建 axios 实例
const request = axios.create({
baseURL: `${difyConfig.DIFY_URL}`, // 替换为你的 API 基础地址
timeout: 10000, // 设置超时时间
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
request.interceptors.request.use(
(config) => {
const appStore = useAppStore()
// 在发送请求之前做些什么,例如添加 token
if (difyConfig.API_KEY) {
config.headers.Authorization = `Bearer ${difyConfig.API_KEY}`;
}
// 当存在params参数时,主动加入userID
if (config.params) {
config.params.user = appStore.userId || difyConfig.USER_ID
}
// 当存在data参数时,主动加入userID
if (config.data) {
config.data.user = appStore.userId || difyConfig.USER_ID
}
return config;
},
(error) => {
// 对请求错误做些什么
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
(response) => {
// 对响应数据做点什么,例如直接返回 data
return response.data || response;
},
(error) => {
// 统一处理错误
const { response } = error;
if (response) {
// 服务器返回了状态码
console.error(`Error ${response.status}: ${response.statusText}`);
switch (response.status) {
case 401:
console.error('未授权,请登录!');
break;
case 403:
console.error('权限不足!');
break;
case 404:
console.error('请求资源未找到!');
break;
case 500:
console.error('服务器内部错误!');
break;
default:
console.error(`未知错误:${response.statusText}`);
}
} else if (error.request) {
// 请求已发出,但没有收到响应
console.error('请求未收到响应:', error.request);
} else {
// 发送请求时出错
console.error('请求错误:', error.message);
}
return Promise.reject(error);
}
);
// 封装常用的请求方法
const get = (url, params) => request.get(url, { params });
const post = (url, data, config={}) => request.post(url, data, config);
const put = (url, data, config={}) => request.put(url, data, config);
const deleteRequest = (url, params, data) => request.delete(url, { params, data });
const stream = async (url, method, data) => {
try {
const appStore = useAppStore()
data.user = appStore.userId || difyConfig.USER_ID
url = difyConfig.DIFY_URL + url
const response = await fetch(url, {
method,
headers: {
"Content-Type": 'application/json',
"Authorization": `Bearer ${difyConfig.API_KEY}`
},
body: JSON.stringify(data)
});
return response
}catch (error) {
console.error('Error:', error);
}
}
// 导出封装后的请求方法
export default {
get,
post,
put,
delete: deleteRequest,
stream
};
\ No newline at end of file
/* Logo 字体 */
@font-face {
font-family: "iconfont logo";
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
}
.logo {
font-family: "iconfont logo";
font-size: 160px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
line-height: 42px;
color: #666;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
cursor: pointer;
width: 100px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
position: relative;
z-index: 1;
margin-bottom: -1px;
color: #666;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main .logo {
color: #333;
text-align: left;
margin-bottom: 30px;
line-height: 1;
height: 110px;
margin-top: -50px;
overflow: hidden;
*zoom: 1;
}
.main .logo a {
font-size: 160px;
color: #333;
}
.helps {
margin-top: 40px;
}
.helps pre {
padding: 20px;
margin: 10px 0;
border: solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists {
width: 100% !important;
overflow: hidden;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-bottom: 10px;
margin-right: 20px;
text-align: center;
list-style: none !important;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
line-height: 100px;
font-size: 42px;
margin: 10px auto;
color: #333;
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
-moz-transition: font-size 0.25s linear, width 0.25s linear;
transition: font-size 0.25s linear, width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p {
margin: 1em 0;
}
.markdown>p,
.markdown>blockquote,
.markdown>.highlight,
.markdown>ol,
.markdown>ul {
width: 80%;
}
.markdown ul>li {
list-style: circle;
}
.markdown>ul li,
.markdown blockquote ul>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown>ul li p,
.markdown>ol li p {
margin: 0.6em 0;
}
.markdown ol>li {
list-style: decimal;
}
.markdown>ol li,
.markdown blockquote ol>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown>table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown>table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown>table th,
.markdown>table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown>table th {
background: #F7F7F7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown>br,
.markdown>p>br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre)>code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre)>code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>iconfont Demo</title>
<link rel="shortcut icon" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg" type="image/x-icon"/>
<link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg"/>
<link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
<link rel="stylesheet" href="demo.css">
<link rel="stylesheet" href="iconfont.css">
<script src="iconfont.js"></script>
<!-- jQuery -->
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
<!-- 代码高亮 -->
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
<style>
.main .logo {
margin-top: 0;
height: auto;
}
.main .logo a {
display: flex;
align-items: center;
}
.main .logo .sub-title {
margin-left: 0.5em;
font-size: 22px;
color: #fff;
background: linear-gradient(-45deg, #3967FF, #B500FE);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
</head>
<body>
<div class="main">
<h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">
<img width="200" src="https://img.alicdn.com/imgextra/i3/O1CN01Mn65HV1FfSEzR6DKv_!!6000000000514-55-tps-228-59.svg">
</a></h1>
<div class="nav-tabs">
<ul id="tabs" class="dib-box">
<li class="dib active"><span>Unicode</span></li>
<li class="dib"><span>Font class</span></li>
<li class="dib"><span>Symbol</span></li>
</ul>
<a href="https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=4900927" target="_blank" class="nav-more">查看项目</a>
</div>
<div class="tab-container">
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe677;</span>
<div class="name"></div>
<div class="code-name">&amp;#xe677;</div>
</li>
</ul>
<div class="article markdown">
<h2 id="unicode-">Unicode 引用</h2>
<hr>
<p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
<ul>
<li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
<li>默认情况下不支持多色,直接添加多色图标会自动去色。</li>
</ul>
<blockquote>
<p>注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)</p>
</blockquote>
<p>Unicode 使用步骤如下:</p>
<h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1745237017010') format('woff2'),
url('iconfont.woff?t=1745237017010') format('woff'),
url('iconfont.ttf?t=1745237017010') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
<pre><code class="language-css"
>.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
<pre>
<code class="language-html"
>&lt;span class="iconfont"&gt;&amp;#x33;&lt;/span&gt;
</code></pre>
<blockquote>
<p>"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
</blockquote>
</div>
</div>
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icon-zan"></span>
<div class="name">
</div>
<div class="code-name">.icon-zan
</div>
</li>
</ul>
<div class="article markdown">
<h2 id="font-class-">font-class 引用</h2>
<hr>
<p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
<p>与 Unicode 使用方式相比,具有如下特点:</p>
<ul>
<li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
<li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
<pre><code class="language-html">&lt;link rel="stylesheet" href="./iconfont.css"&gt;
</code></pre>
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;span class="iconfont icon-xxx"&gt;&lt;/span&gt;
</code></pre>
<blockquote>
<p>"
iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
</blockquote>
</div>
</div>
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-zan"></use>
</svg>
<div class="name"></div>
<div class="code-name">#icon-zan</div>
</li>
</ul>
<div class="article markdown">
<h2 id="symbol-">Symbol 引用</h2>
<hr>
<p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
<ul>
<li>支持多色图标了,不再受单色限制。</li>
<li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
<li>兼容性较差,支持 IE9+,及现代浏览器。</li>
<li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
<pre><code class="language-html">&lt;script src="./iconfont.js"&gt;&lt;/script&gt;
</code></pre>
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
<pre><code class="language-html">&lt;style&gt;
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
&lt;/style&gt;
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;svg class="icon" aria-hidden="true"&gt;
&lt;use xlink:href="#icon-xxx"&gt;&lt;/use&gt;
&lt;/svg&gt;
</code></pre>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
$('.tab-container .content:first').show()
$('#tabs li').click(function (e) {
var tabContent = $('.tab-container .content')
var index = $(this).index()
if ($(this).hasClass('active')) {
return
} else {
$('#tabs li').removeClass('active')
$(this).addClass('active')
tabContent.hide().eq(index).fadeIn()
}
})
})
</script>
</body>
</html>
@font-face {
font-family: "iconfont"; /* Project id 4900927 */
src: url('iconfont.woff2?t=1745237017010') format('woff2'),
url('iconfont.woff?t=1745237017010') format('woff'),
url('iconfont.ttf?t=1745237017010') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-zan:before {
content: "\e677";
}
window._iconfont_svg_string_4900927='<svg><symbol id="icon-zan" viewBox="0 0 1024 1024"><path d="M841.900615 408.186609 664.015206 408.186609c-4.087085 0-7.605212-1.823532-9.635451-4.979408-2.044566-3.188622-2.387373-6.788613-0.932232-10.313903 10.105149-19.105127 57.494459-115.41052 44.899606-217.797421-3.51915-28.749788-18.948561-67.455321-72.482825-88.576361-13.886265-5.465478-39.682791-8.026814-59.192124-8.601912-46.860261-1.38044-56.329936 7.693216-60.373019 11.568477-0.164752 0.155543-0.324388 0.316202-0.484024 0.476861C490.730581 105.371886 491.958548 127.455857 493.514997 155.415652c0.795109 14.287401 1.694595 30.466895 0.857531 48.953945-4.579295 49.570999-11.171434 76.334549-24.283056 98.636484-40.689724 69.270666-109.423155 104.141872-122.656551 105.213273l-96.509031 0L86.587054 408.219355c-11.761881 0-21.296025 9.534144-21.296025 21.296025L65.291029 844.261383c0 11.760858 9.534144 21.296025 21.296025 21.296025l164.337859 0c0.858554 0 1.700735-0.065492 2.533706-0.164752l104.601336 0c1.191128 0.969071 2.534729 2.081405 3.739161 3.082198 22.952758 19.025309 83.921341 69.563332 184.571692 69.563332l209.634508 0c53.634548 0 96.863095-29.224602 112.819508-76.270081 0.13917-0.409322 0.26606-0.823761 0.38067-1.24127 2.246157-8.247848 8.025791-25.420973 15.344476-47.162136 27.735691-82.408895 74.159-220.339314 74.159-294.15653C958.708971 446.576965 918.317029 408.186609 841.900615 408.186609zM107.883079 450.812429l121.744785 0 0 372.152928L107.883079 822.965357 107.883079 450.812429zM844.182588 799.778262c-7.649214 22.723537-13.225209 39.289841-15.882736 48.862871-10.205433 29.319769-37.183877 46.803979-72.293513 46.803979l-209.634508 0c-85.292572 0-135.816269-41.879829-157.38961-59.763129-9.691733-8.03193-15.541975-12.881378-24.845874-12.881378l-91.914386 0L272.221962 450.812429l75.364455 0c20.070104 0 51.52142-17.555841 74.262354-33.985022 24.394596-17.623379 58.936297-47.932686 84.96409-92.242867 19.039635-32.386618 25.602098-68.703755 30.019711-116.802216 0.029676-0.317225 0.051165-0.63445 0.066515-0.952698 0.975211-20.862143-0.039909-39.114856-0.855484-53.780881-0.63445-11.403724-1.402953-25.209148-0.179079-31.395034 15.941064-3.081175 61.369719-0.632403 74.387197 4.490268 27.712155 10.934027 42.701544 28.639271 45.822628 54.138014 11.341303 92.193749-35.07689 182.903703-40.407292 192.944384-0.493234 0.932232-0.74906 1.486864-0.74906 1.486864-7.703449 16.70138-6.353708 35.947723 3.624551 51.509141 9.889231 15.376199 26.888394 24.556279 45.473681 24.556279l177.88541 0c52.712549 0 74.215282 19.825534 74.215282 68.429509C916.115897 586.05053 869.322152 725.084073 844.182588 799.778262z" ></path></symbol></svg>',(n=>{var t=(e=(e=document.getElementsByTagName("script"))[e.length-1]).getAttribute("data-injectcss"),e=e.getAttribute("data-disable-injectsvg");if(!e){var o,i,c,d,l,s=function(t,e){e.parentNode.insertBefore(t,e)};if(t&&!n.__iconfont__svg__cssinject__){n.__iconfont__svg__cssinject__=!0;try{document.write("<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>")}catch(t){console&&console.log(t)}}o=function(){var t,e=document.createElement("div");e.innerHTML=n._iconfont_svg_string_4900927,(e=e.getElementsByTagName("svg")[0])&&(e.setAttribute("aria-hidden","true"),e.style.position="absolute",e.style.width=0,e.style.height=0,e.style.overflow="hidden",e=e,(t=document.body).firstChild?s(e,t.firstChild):t.appendChild(e))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(o,0):(i=function(){document.removeEventListener("DOMContentLoaded",i,!1),o()},document.addEventListener("DOMContentLoaded",i,!1)):document.attachEvent&&(c=o,d=n.document,l=!1,r(),d.onreadystatechange=function(){"complete"==d.readyState&&(d.onreadystatechange=null,a())})}function a(){l||(l=!0,c())}function r(){try{d.documentElement.doScroll("left")}catch(t){return void setTimeout(r,50)}a()}})(window);
\ No newline at end of file
{
"id": "4900927",
"name": "dify-chat-ui2",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "658060",
"name": "赞",
"font_class": "zan",
"unicode": "e677",
"unicode_decimal": 58999
}
]
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
\ No newline at end of file
<template>
<el-scrollbar class="scrollbar" :style="scrollbarStyle" ref="scrollbarRef">
<div ref="innerRef" class="chat-message-com">
<!-- 开场对白或建议问题 -->
<div class="chat-message-item">
<welcomeBubble v-if="hasWelcomeMessage" :appParams="appStore.appParams" @userQuery="userQuery($event, true)"/>
</div>
<!-- 对话记录 -->
<template v-for="(item, index) in chatMessageList" :key="'chatMessage - ' + index">
<div class="chat-message-item">
<aiBubble
:query="item.content"
v-if="item.type === 'ai' || item.type === 'ai-history'"
:messageType="item.type"
:content="item.content"
@messageFinished="messageFinished"
@workflow-is-error="workflowIsError"
/>
<userBubble v-else :content="item.content"/>
</div>
</template>
</div>
</el-scrollbar>
<div class="input-box">
<InputMessage ref="inputMessageRef" @userQuery="userQuery" @getInputHeight="getInputHeight" :loadingFinished="loadingFinished" :workflowError="workflowError"/>
</div>
</template>
<script setup>
import { ref, computed, onMounted, nextTick, watch } from "vue";
import InputMessage from "./chatSubassembly/inputMessage.vue";
import aiBubble from "./chatSubassembly/aiBubble.vue";
import userBubble from "./chatSubassembly/userBubble.vue";
import welcomeBubble from "./chatSubassembly/welcomeBubble.vue";
import { useAppStore } from "../store/app";
import { useTaskStore } from '../store/task'
const props = defineProps({
historyMsgList: {
type: Array,
default: () => [],
},
})
const appStore = useAppStore();
const taskStore = useTaskStore()
const chatMessageList = ref([]);
const inputMessageRef = ref(null);
const scrollbarRef = ref(null);
const innerRef = ref(null);
const scrollTimer = ref(null)
const emits = defineEmits(['updateHistory'])
const userQuery = (query, isLoading=false) => {
if (chatMessageList.value.length === 0) {
emits('updateHistory')
}
chatMessageList.value.push({
type: 'user',
content: query,
})
chatMessageList.value.push({
type: 'ai',
content: query,
})
if (isLoading) {
inputMessageRef.value.changeLoading(true)
// 每隔500ms滚动到底部
nextTick(() => {
scrollTimer.value = setInterval(() => {
scrollToBottom()
}, 300)
})
}
}
const defaultHeight = ref(62);
const scrollbarStyle = computed(() => {
return `height: calc(100% - ${defaultHeight.value}px);`
})
const getInputHeight = (height) => {
const marginTop = 6 + 10 + 5 // 6 + 10像素padding(上下)+ 5像素margin-top
defaultHeight.value = height + marginTop
}
const loadingFinished = ref(false)
const messageFinished = (status) => {
loadingFinished.value = status
if (status) {
nextTick(() => {
scrollToBottom()
})
clearInterval(scrollTimer.value);
}
}
const hasWelcomeMessage = computed(() => {
return appStore.appParams.opening_statement != '';
});
const workflowError = ref(false)
const workflowIsError = (isError) => {
workflowError.value = isError
}
// 滚动到底部
const scrollToBottom = async () => {
if (scrollbarRef.value) {
scrollbarRef.value.setScrollTop(innerRef.value.scrollHeight);
}
}
// 会话重置
const resetChat = async () => {
await inputMessageRef.value.handleCancel()
taskStore.clear()
}
watch(() => props.historyMsgList, (newValue) => {
newValue.forEach((item) => {
chatMessageList.value.push({
type: 'user',
content: item?.query,
})
chatMessageList.value.push({
type: 'ai-history',
content: item?.answer,
})
console.log('zhisx');
}, {immediate: true})
})
defineExpose({
resetChat
})
</script>
<style scoped>
.chat-message-com{
padding: 10px 20px 10px 20px;
}
.chat-message-item{
padding: 5px 15px;
}
.scrollbar{
background-color: var(--scrollbar-bg, #fff);
border-radius: 8px;
}
.input-box{
background-color: var(--input-box-bg, #fff);
padding: 5px;
margin-top: 5px;
border-radius: 8px;
}
</style>
<template>
<div class="header bg-color">
<div class="left-tool">
<!-- 图标按钮 -->
<el-button :icon="Expand" v-if="!sideBarStatus" link @click="expandSidebar"/>
<el-button :icon="Fold" v-if="sideBarStatus" link @click="foldSidebar"/>
</div>
<div class="center-info">{{appInfo?.app?.name}}</div>
<div class="right-tool">
<el-button :icon="RefreshRight" @click="refreshConversation" link/>
</div>
</div>
</template>
<script setup>
import {ref, computed} from 'vue'
import { Expand, Fold, RefreshRight } from '@element-plus/icons-vue'
import { useAppStore } from '../store/app'
const appStore = useAppStore()
const sideBarStatus = ref(false)
const emit = defineEmits(['getSideBarStatus', 'refreshConversation'])
const expandSidebar = () => {
sideBarStatus.value = !sideBarStatus.value
emit('getSideBarStatus', sideBarStatus.value)
}
const foldSidebar = () => {
sideBarStatus.value = !sideBarStatus.value
emit('getSideBarStatus', sideBarStatus.value)
}
const appInfo = computed(() => {
return appStore
})
const refreshConversation = () => {
emit('refreshConversation')
}
</script>
<style lang="less" scoped>
.header {
width: 100%;
height: 100%;
display: flex;
align-items: center;
.left-tool {
width: 30px;
font-size: 20px;
padding-left: 20px;
.el-button{
font-size: 16px;
}
}
.center-info {
flex: 1;
text-align: center;
}
.right-tool {
width: 30px;
padding-right: 15px;
.el-button{
font-size: 16px;
}
}
}
.bg-color{
background-color: var(--header-bg-color);
}
</style>
\ No newline at end of file
<template>
<div class="history-list">
<el-input
v-model="keyword"
placeholder="输入关键字搜索"
class="input-with-select"
>
<template #append>
<el-button type="primary" :icon="Search" />
</template>
</el-input>
<el-divider style="margin: 5px 0 0px 0" />
<!-- li 左右布局,左边的文字 右边的图标 ,左边文字要做溢出处理 -->
<ul class="history-list-ul">
<el-scrollbar>
<li
:class="[
'history-list-li',
{ active: currentClickItemId === item.id },
]"
v-for="item in filteredList"
:key="item.id"
>
<div
class="history-list-li-left"
@click="getConversationMsg(item.id)"
>
{{ item.name }}
</div>
<div class="history-list-li-right">
<!-- 删除按钮 -->
<el-button
type="primary"
link
:icon="Edit"
@click="openEdit(item.id)"
></el-button>
<el-button
type="danger"
link
:icon="Delete"
@click="openDeleteConfirm(item.id)"
></el-button>
</div>
</li>
</el-scrollbar>
</ul>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from "vue";
import { Search, Delete, Edit } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
import difyApi from "../apis/difyApi";
const props = defineProps({
isRefreshHistoryList: {
type: Boolean,
default: false,
},
isResetHistoryItem: {
type: Boolean,
default: false,
},
});
const list = ref([]);
const keyword = ref("");
const filteredList = ref([]);
const currentClickItemId = ref(null);
const getHistoryList = async () => {
const historyResult = await difyApi.getChatList({ limit: 100 });
list.value = historyResult.data;
filteredList.value = list.value;
};
const openDeleteConfirm = (id) => {
ElMessageBox.confirm("你确定要删除该对话记录吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
deleteChat(id);
})
.catch(() => {
console.log("取消删除");
});
};
const deleteChat = async (id) => {
await difyApi.deleteChat(id);
getHistoryList();
};
// 防抖函数
const debounce = (func, wait) => {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
};
const filterList = () => {
if (!keyword.value) {
filteredList.value = list.value;
} else {
filteredList.value = list.value.filter((item) =>
item.name.toLowerCase().includes(keyword.value.toLowerCase())
);
}
};
const debouncedFilterList = debounce(filterList, 300); // 300 毫秒的防抖时间
watch(keyword, debouncedFilterList);
const emits = defineEmits(["getHistoryMsg"]);
const getConversationMsg = async (id) => {
currentClickItemId.value = id;
const result = await difyApi.getChatHistory({
conversation_id: id,
limit: 20,
});
console.log("result:", result);
emits("getHistoryMsg", result.data);
};
const openEdit = (id) => {
ElMessageBox.prompt("", "修改会话名称", {
confirmButtonText: "保存",
cancelButtonText: "取消",
// inputPattern:
// /[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?/,
// inputErrorMessage: "Invalid Email",
inputPlaceholder: "请输入新的会话名称",
})
.then(async ({ value }) => {
await difyApi.renameChat(id, { name: value });
getHistoryList();
})
.catch(() => {
console.log("取消修改");
});
};
watch(
() => props.isRefreshHistoryList,
(newValue) => {
if (newValue) {
getHistoryList();
}
}
);
watch(() => props.isResetHistoryItem, (newValue) => {
if(newValue) {
currentClickItemId.value = null;
}
})
onMounted(() => {
getHistoryList();
});
</script>
<style lang="less" scoped>
.history-list {
height: calc(100% - 60px);
.input-with-select {
margin-top: 2px;
}
}
.history-list-ul {
height: 100%;
padding: 0;
margin: 0;
padding: 5px 0;
.history-list-li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2px;
margin: 0;
height: 28px;
width: 94%;
border-radius: 6px;
margin-bottom: 2px;
&:hover {
background-color: #fff;
}
.history-list-li-left {
flex: 1;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
text-align: left;
color: var(--el-text-color-regular);
white-space: nowrap;
word-break: keep-all;
word-wrap: normal;
padding-left: 5px;
height: 28px;
line-height: 28px;
&:hover {
color: var(--el-text-color-primary);
// text-decoration: underline;
}
}
.history-list-li-right {
padding-right: 0;
justify-content: center;
align-content: center;
.el-button + .el-button {
margin-left: 0px;
}
}
}
.active {
background-color: #fff;
}
}
</style>
<template>
<div class="common-layout">
<el-container>
<transition name="slide">
<el-aside width="280px" v-show="sideBarHidden">
<HistoryList @getHistoryMsg="getHistoryMsg" :isRefreshHistoryList="isRefreshHistoryList" :isResetHistoryItem="isResetHistoryItem"/>
</el-aside>
</transition>
<el-container id="main-container">
<el-header>
<Header @getSideBarStatus="getSideBarStatus" @refreshConversation="refreshConversation" />
</el-header>
<el-main>
<Chat ref="chatRef" v-if="!refreshChat" :historyMsgList="historyMsgList" @updateHistory="refreshHistoryList"/>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import Chat from './Chat.vue'
import HistoryList from './HistoryList.vue'
import difyApi from '../apis/difyApi'
import { useAppStore } from '../store/app'
const appStore = useAppStore()
const chatRef = ref(null)
const sideBarHidden = ref(false)
const getSideBarStatus = (status) => {
sideBarHidden.value = status
}
const initAppInfo = async () => {
const app = await difyApi.getAppInfo()
const appMeta = await difyApi.getAppMeta()
const appParams = await difyApi.getAppParams()
appStore.setApp(app)
appStore.setAppMeta(appMeta)
appStore.setAppParams(appParams)
}
const refreshChat = ref(false)
const refreshConversation = (reset=true) => {
nextTick(() => {
chatRef.value.resetChat()
console.log('refreshChat');
refreshChat.value = true
nextTick(() => {
refreshChat.value = false
})
})
if (reset) {
resetHistoryItem()
}
}
const historyMsgList = ref([])
const getHistoryMsg = (msgList) => {
refreshConversation(false)
setTimeout(() => {
historyMsgList.value = msgList
}, 50)
}
const isRefreshHistoryList = ref(false)
const refreshHistoryList = () => {
isRefreshHistoryList.value = true
nextTick(() => {
isRefreshHistoryList.value = false
})
}
const isResetHistoryItem = ref(false)
const resetHistoryItem = () => {
isResetHistoryItem.value = true
nextTick(() => {
isResetHistoryItem.value = false
})
}
const getUserId = () => {
const url = new URL(window.location.href);
const user_id = url.searchParams.get('userId')
if (user_id) {
appStore.setUserId(user_id)
refreshHistoryList()
}
}
onMounted(() => {
initAppInfo()
getUserId()
})
</script>
<style lang="less" scoped>
.common-layout,
.el-container {
height: 100vh;
}
.el-header {
margin: 0;
padding: 0;
height: 45px;
}
.el-container {
text-align: left;
}
.el-aside {
padding: 5px;
background-color: var(--sidebar-bg-color);
border-right: 1px solid var(--sidebar-right-border-color);
transition: transform 0.1s ease;
}
/* 过渡类 */
.slide-enter-active,
.slide-leave-active {
transition: transform 0.1s ease;
}
.slide-enter-from,
.slide-leave-to {
transform: translateX(-280px);
}
</style>
\ No newline at end of file
<template>
<Bubble is-markdown placement="start" maxWidth="90%" :noStyle="true">
<template #avatar>
<el-avatar :size="28" :src="avatarAi" />
</template>
<template #content>
<div class="custom-bubble-content" v-adjust-width>
<workflowItem
v-if="workflowContent.length > 0"
:workflowContent="workflowContent"
ref="workflowItemRef"
@workflow-is-error="workflowIsError"
/>
<!-- 使用 MarkdownRenderer 组件渲染 messageContent -->
<MarkdownRenderer :content="messageContent" />
</div>
</template>
<template #footer>
<div class="footer-container">
<el-button
type="info"
:icon="Refresh"
size="small"
circle
@click="reStartSSE"
/>
<el-button
type="success"
class="iconfont icon-zan"
size="small"
circle
@click="feedback('like')"
/>
<el-button
type="warning"
class="iconfont icon-zan icon-rotate"
size="small"
circle
@click="feedback('dislike')"
/>
<el-button
color="#626aef"
:icon="DocumentCopy"
size="small"
circle
@click="copyContent()"
/>
</div>
</template>
</Bubble>
</template>
<script setup>
import { computed, ref, watch } from "vue";
import { Refresh, Search, Star, DocumentCopy } from "@element-plus/icons-vue";
import workflowItem from "./workflowItem.vue";
import MarkdownRenderer from '../markdown/markdownRender.vue';
import { Bubble, useXStream } from "vue-element-plus-x";
import difyApi from "../../apis/difyApi";
import { useTaskStore } from "../../store/task";
import { ElMessage } from "element-plus";
const taskStore = useTaskStore();
const { startStream, cancel, data, error, isLoading } = useXStream();
const avatarAi = "/avatarAI.svg";
const props = defineProps({
query: {
type: Array,
default: () => [],
},
messageType: {
type: String,
default: "ai",
},
content: {
type: String,
default: "",
}
});
const emits = defineEmits(["messageFinished", "workflow-is-error"]);
watch(
() => props.query,
(newValue) => {
if(props.messageType === 'ai') {
startSSE(newValue);
}
console.log("# query:", newValue, props.messageType, props.content);
},
{ immediate: true }
);
// 默认支持 SSE 协议
async function startSSE(query) {
try {
const chatMessageOptions = difyApi.chatMessage()
const response = await fetch(chatMessageOptions.url, {
headers: chatMessageOptions.headers,
method: chatMessageOptions.method,
body: JSON.stringify({
inputs: {},
query,
response_mode: "streaming",
conversation_id: "",
user: chatMessageOptions.user,
files: [],
}),
});
const readableStream = response.body;
await startStream({ readableStream });
} catch (err) {
console.error("Fetch error:", err);
}
}
// 机器人的 content 计算属性
const workflowContent = computed(() => {
if (props.messageType === 'ai-history') {
return [];
}
// 以下是处理非历史消息记录
if (!data.value.length) return [];
let workflowList = [];
for (let index = 0; index < data.value.length; index++) {
const chunk = data.value[index].data;
if (chunk) {
try {
const parsedChunk = JSON.parse(chunk);
workflowList.push(parsedChunk);
if (parsedChunk.event === "workflow_started") {
taskStore.setTaskId(parsedChunk.task_id);
taskStore.setConversationId(parsedChunk.conversation_id);
taskStore.setMessageId(parsedChunk.message_id);
taskStore.setWorkflowRunId(parsedChunk.workflow_run_id);
}
if (parsedChunk.event === "message_end") {
emits("messageFinished", true);
}
} catch (error) {
// 这个 结束标识 是后端给的,所以这里这样判断
// 实际项目中,以项目需要为准
if (chunk === " [DONE]") {
// 处理数据结束的情况
// console.log('数据接收完毕')
} else {
console.error("解析数据时出错:", error);
}
}
}
}
console.log("workflowContent:", workflowList);
return workflowList;
});
const workflowItemRef = ref(null);
const reStartSSE = () => {
workflowItemRef.value.clearWorkflow();
startSSE(props.query);
};
// AI回答问题
const messageContent = computed(() => {
if (props.messageType === 'ai-history') {
return props.content;
}
let messageContent = "";
workflowContent.value.forEach((chunk) => {
if (chunk.event === "message") {
messageContent += chunk.answer;
}
});
return messageContent;
});
const feedback = async (type) => {
const messageId = workflowContent.value[0].message_id;
await difyApi.feedback(messageId, { rating: type, content: type });
ElMessage.success("反馈成功");
};
const copyContent = () => {
const context = messageContent.value;
navigator.clipboard.writeText(context);
ElMessage.success("复制成功");
};
const workflowIsError = (isError) => {
emits("workflow-is-error", isError);
};
</script>
<style lang="less" scoped>
.message-content {
margin-top: 10px;
}
.footer-container {
text-align: right;
}
:deep(.el-bubble-content-wrapper .el-bubble-footer) {
margin-top: 5px;
}
.icon-rotate {
transform: rotate(180deg); /* 旋转 180 度 */
transition: transform 0.3s ease; /* 添加过渡效果,让旋转更平滑 */
}
</style>
<template>
<div
ref="inputRef"
style="display: flex; flex-direction: column; margin-top: 5px"
>
<MentionSender
ref="senderRef"
v-model="senderValue"
:loading="senderLoading"
clearable
@submit="handleSubmit"
>
<template #action-list>
<div class="action-list-self-wrap">
<el-button
type="danger"
link
v-if="senderValue"
@click="senderRef.clear()"
>
<el-icon style="font-size: 20px"><Close /></el-icon>
</el-button>
<el-tooltip
class="box-item"
effect="dark"
content="点击后将停止回答"
placement="top-start"
v-if="senderLoading"
>
<el-button type="primary" circle @click="handleCancel">
<el-icon style="font-size: 20px" class="is-loaidng">
<Loading />
</el-icon>
</el-button>
</el-tooltip>
<el-button
v-else
type="primary"
circle
@click="handleSubmit"
:disabled="!senderValue"
>
<el-icon style="font-size: 20px"><Promotion /></el-icon>
</el-button>
</div>
</template>
</MentionSender>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, watch } from "vue";
import { MentionSender } from "vue-element-plus-x";
import { Close, Promotion, Loading } from "@element-plus/icons-vue";
import difyApi from "../../apis/difyApi";
import { useTaskStore } from "../../store/task";
const props = defineProps({
loadingFinished: {
type: Boolean,
default: false,
},
workflowError: {
type: Boolean,
default: false,
},
});
watch(
() => props.loadingFinished,
(newValue) => {
if (newValue) {
senderLoading.value = false;
}
}
);
watch(
() => props.workflowError,
(newValue) => {
if (newValue) {
senderLoading.value = false;
}
}
);
const taskStore = useTaskStore();
const senderRef = ref();
const senderValue = ref("");
const senderLoading = ref(false);
const inputRef = ref(null);
const emit = defineEmits(["userQuery", "getInputHeight"]);
function handleSubmit() {
senderLoading.value = true;
// 可以在控制台 查看打印结果
emit("userQuery", senderValue.value);
senderValue.value = "";
}
// 定义一个函数来处理高度变化
const handleResize = (entries) => {
const entry = entries[0];
const newHeight = entry.contentRect.height;
console.log(`元素的新高度是: ${newHeight}px`);
emit("getInputHeight", newHeight);
};
const handleCancel = async () => {
if (taskStore.taskId) {
await difyApi.stop(taskStore.taskId);
}
senderLoading.value = false;
};
const changeLoading = (loading) => {
senderLoading.value = loading;
};
onMounted(() => {
if (inputRef.value) {
const resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(inputRef.value);
// 保存 resizeObserver 到组件实例上,以便后续卸载时移除
inputRef.value.resizeObserver = resizeObserver;
}
});
// 在组件卸载前移除 ResizeObserver
onUnmounted(() => {
if (inputRef.value && inputRef.value.resizeObserver) {
inputRef.value.resizeObserver.disconnect();
}
});
defineExpose({
changeLoading,
handleCancel
});
</script>
<style lang="less" scoped>
.action-list-self-wrap {
display: flex;
align-items: center;
& > span {
width: 120px;
font-weight: 600;
color: var(--el-color-primary);
}
}
.is-loaidng {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<template>
<Bubble :content="content" placement="end" maxWidth="90%">
<template #avatar>
<el-avatar :size="28" :src="avatarUser" />
</template>
</Bubble>
</template>
<script setup>
import { Bubble } from "vue-element-plus-x";
const props = defineProps({
content: {
type: String,
default: "",
},
});
const avatarUser = "/avatarUser.svg";
</script>
<template>
<Bubble :content="content" placement="start" maxWidth="90%" :noStyle="true">
<template #avatar>
<el-avatar :size="28" :src="avatarAi" />
</template>
<template #content>
<div class="custom-bubble-content" v-adjust-width>
{{ content }}
<el-divider style="margin: 6px 0;"/>
<template v-for="(item, index) in suggestedQuestions" :key="'suggest' + index">
<el-link type="primary" @click="handleQuery(item)">{{ item }}</el-link><br>
</template>
</div>
</template>
</Bubble>
</template>
<script setup>
import { computed, ref, watch } from "vue";
import { Refresh, Search, Star, DocumentCopy } from "@element-plus/icons-vue";
import { Bubble } from "vue-element-plus-x";
import difyApi from "../../apis/difyApi";
const avatarAi = "/avatarAI.svg";
const props = defineProps({
appParams: {
type: Object,
default: () => {},
},
});
const content = ref("");
const suggestedQuestions = ref([])
watch(() => props.appParams, (val) => {
content.value = val.opening_statement;
suggestedQuestions.value = val.suggested_questions;
}, {immediate: true});
const emit = defineEmits(["userQuery"]);
const handleQuery = (query) => {
emit("userQuery", query)
}
</script>
<template>
<div class="workflow-container">
<div class="icon">
<el-icon v-if="workflowStatus == 'loading'" style="font-size: 16px" class="is-loaidng">
<Loading />
</el-icon>
<el-icon v-if="workflowStatus == 'success'" style="font-size: 16px" class="is-success">
<SuccessFilled />
</el-icon>
<el-icon v-if="workflowStatus == 'error'" style="font-size: 16px" class="is-error">
<WarningFilled />
</el-icon>
</div>
<el-collapse class="workflow-collapse">
<el-collapse-item title="工作流">
<ThoughtChain :thinking-items="thinkingItems" row-key="codeId" dot-size="small"
style="margin-left: 10px" />
</el-collapse-item>
</el-collapse>
</div>
</template>
<script setup>
import { computed, watch, ref, nextTick, onMounted } from "vue";
import { ThoughtChain } from "vue-element-plus-x";
import { ElMessage } from 'element-plus'
import { Loading, SuccessFilled, WarningFilled } from "@element-plus/icons-vue";
const props = defineProps({
workflowContent: {
type: Array,
default: () => [],
}
});
// workflowContent内容会逐条增加,因此需要根据获取的内容逐条添加到thinkingItems中,添加时需要考虑是否重复
const thinkingItems = ref([]);
const workflowStatus = ref('loading')
const emit = defineEmits(['workflow-is-error'])
watch(
() => props.workflowContent,
(val) => {
if (val && Array.isArray(val)) {
for (let i = 0; i < val.length; i++) {
const item = val[i];
if (item.event === "error") {
ElMessage.error(item.message)
workflowStatus.value = 'error';
emit('workflow-is-error', true)
break;
}
if (item.event === "workflow_finished") {
workflowStatus.value = 'success';
}
if (
item.event !== "workflow_started" &&
item.event !== "workflow_finished" &&
item.event !== "message_end" &&
item.event !== "message"
) {
const index = thinkingItems.value.findIndex(
(ti) => ti.codeId === item.data.id
);
const dataId = item?.data?.id;
// 判断是否为 code、tool 节点,如果是则排除
// const isExcludeNode = item?.data?.node_type === "code" || item?.data?.node_type === "tool";
// 非代码节点才添加到 thinkingItems 中
// if (!isExcludeNode) {
if (index === -1) {
thinkingItems.value.push({
codeId: dataId,
status: "loading",
isCanExpand: false,
isDefaultExpand: false,
title: item.data.title,
thinkTitle: "",
thinkContent: "",
});
} else {
if (item.event === "node_finished") {
thinkingItems.value[index].status = "success";
}
}
// }
}
}
console.log("thinkingItems.value", thinkingItems.value);
}
},
{ immediate: true }
);
const clearWorkflow = () => {
thinkingItems.value = [];
workflowStatus.value = 'loading'
}
defineExpose({
clearWorkflow
})
</script>
<style lang="less" scoped>
.workflow-container {
display: flex;
background-color: var(--workflow-bg-color);
border-radius: 4px;
padding: 0 5px;
min-width: calc(100% - 15px);
.icon {
width: 20px;
line-height: 48px;
}
.workflow-collapse {
flex: 1;
}
.el-collapse {
--el-collapse-border-color: transparent !important;
}
:deep(.el-collapse-item__content) {
padding-bottom: 0px;
}
}
.is-success {
color: var(--el-color-success);
}
.is-error {
color: var(--el-color-danger);
}
.is-loaidng {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<!-- src/components/MarkdownRenderer.vue -->
<template>
<div v-html="renderedContent"></div>
</template>
<script setup>
import { computed, nextTick } from "vue";
import MarkdownIt from "markdown-it";
import { init } from "echarts";
import markdownItPrism from "markdown-it-prism";
import "prismjs/themes/prism-solarizedlight.min.css"
import "prismjs/components/prism-sql.min.js"
const props = defineProps({
content: {
type: String,
default: "",
},
});
// 初始化 markdown-it 实例并使用 markdown-it-prism 插件
const md = new MarkdownIt({
html: true, // 允许 HTML 标签
linkify: true, // 自动将 URL 转换为链接
typographer: true, // 启用一些语言中立的替换 + 引号美化
});
md.use(markdownItPrism); // 使用 markdown-it-prism 插件
// 自定义规则处理 ```echarts ``` 语法
md.use((md) => {
const defaultFence = md.renderer.rules.fence;
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
const token = tokens[idx];
if (token.info.trim() === "echarts") {
const chartId = `chart-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
const chartContainer = `<div id="${chartId}" style="width: 100%; height: 300px; overflow-x:auto"></div>`;
// 渲染图表容器
nextTick(() => {
const chartDom = document.getElementById(chartId);
if (chartDom) {
const myChart = init(chartDom);
const option = JSON.parse(token.content.trim());
// 调整 ECharts 图表的标题文字大小
if (option.title && option.title.text) {
option.title.textStyle = {
fontSize: 12, // 设置标题文字大小为 16px
};
}
myChart.setOption(option);
}
});
return chartContainer;
}
return defaultFence ? defaultFence(tokens, idx, options, env, self) : "";
};
});
// 处理后的 Markdown 内容
const renderedContent = computed(() => {
return md.render(props.content);
});
</script>
<style lang="less" scoped>
:deep(pre code[class*="language-"]) {
font-size: 12px;
white-space: pre-wrap; /* 代码自动换行 */
word-wrap: break-word; /* 确保单词可以换行 */
}
:deep(table) {
width: auto;
border-collapse: collapse;
margin: 10px 0;
}
:deep(th),
:deep(td) {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
:deep(th) {
background-color: #f2f2f2;
}
:deep(ul) {
padding-left: 15px; /* 增加左边距,调整项目符号与文本之间的距离 */
}
:deep(ol) {
padding-left: 15px; /* 增加左边距,调整项目符号与文本之间的距离 */
}
</style>
export default {
DIFY_URL: '/v1',
API_KEY: 'app-7HghtszMdnxVOtMdZtZoOEUK',
USER_ID: 'asset_user'
}
\ No newline at end of file
// src/directives/v-adjust-width.js
export default {
mounted(el) {
const adjustWidth = () => {
const parentWidth = document.querySelector('#main-container').clientWidth;
// console.log('parentWidth:', parentWidth);
if (parentWidth <= 740) {
el.style.width = '100%';
} else {
el.style.width = '580px';
}
};
const handleResize = () => {
adjustWidth();
};
window.addEventListener('resize', handleResize);
handleResize(); // 初始化时调整宽度
el.__resizeHandler = handleResize;
},
unmounted(el) {
// console.log('v-adjust-width unmounted');
window.removeEventListener('resize', el.__resizeHandler);
}
};
\ No newline at end of file
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import App from './App.vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import vAdjustWidth from './directives/v-adjust-width.js'
import './theme/theme.less'
import 'element-plus/dist/index.css'
import './style.css'
import './assets/icons/iconfont.css'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// 注册指令
app.directive('adjust-width', vAdjustWidth);
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate);
app.use(pinia)
app.mount('#app')
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => {
return {
app: {},
appMeta: {},
appParams: {},
userId: '',
}
},
actions: {
setApp (appInfo) {
this.app = appInfo
},
setAppMeta (appMeta) {
this.appMeta = appMeta
},
setAppParams (appParams) {
this.appParams = appParams
},
setUserId (userId) {
this.userId = userId
}
},
persist: true
})
\ No newline at end of file
import { defineStore } from 'pinia'
export const useTaskStore = defineStore('task', {
state: () => {
return {
taskId: '', // 当前任务id
conversation_id: '', // 当前会话id
message_id: '', // 当前消息id
workflow_run_id: '' // 当前工作流运行id
}
},
actions: {
setTaskId (taskId) {
this.taskId = taskId
},
setConversationId (conversation_id) {
this.conversation_id = conversation_id
},
setMessageId (message_id) {
this.message_id = message_id
},
setWorkflowRunId (workflow_run_id) {
this.workflow_run_id = workflow_run_id
},
clear () {
this.taskId = ''
this.conversation_id = ''
this.message_id = ''
this.workflow_run_id = ''
}
},
persist: true
})
\ No newline at end of file
body {
margin: 0;
display: flex;
place-items: center;
width: 100%;
height: 100%;
}
#app {
width: 100%;
height: 100%;
padding: 0;
text-align: center;
}
ul,li{
margin: 0;
padding: 0;
}
\ No newline at end of file
:root {
// 主要颜色
--el-color-primary: #2C59D6 !important;
// --el-color-primary-light-3: #4d70d1 !important;
// --el-color-primary-light-5: #6c86cd !important;
// --el-color-primary-light-7: #869bd4 !important;
// --el-color-primary-light-8: #9fabce !important;
// --el-color-primary-light-9: #bbc0cb !important;
--el-color-success: #05c67a !important;
// --el-color-success-light-3: #28c787 !important;
// --el-color-success-light-5: #5bc49a !important;
// --el-color-success-light-7: #86c8ae !important;
// --el-color-success-light-8: #97d0b9 !important;
// --el-color-success-light-9: #a7c1b7 !important;
--el-color-warning: #ffa435!important;
// --el-color-warning-light-3: #fdb359!important;
// --el-color-warning-light-5: #fdc887!important;
// --el-color-warning-light-7: #ffddb4!important;
// --el-color-warning-light-8: #ffeed8!important;
// --el-color-warning-light-9: #fef5ea!important;
--el-color-danger: #f65d68!important;
// --el-color-danger-light-3: #f97b84!important;
// --el-color-danger-light-5: #fb9ea4!important;
// --el-color-danger-light-7: #ffc9cc!important;
// --el-color-danger-light-8: #ffdee0!important;
// --el-color-danger-light-9: #fef2f3!important;
--el-color-error: #f65d68!important;
// --el-color-error-light-3: #f97b84!important;
// --el-color-error-light-5: #fb9ea4!important;
// --el-color-error-light-7: #ffc9cc!important;
// --el-color-error-light-8: #ffdee0!important;
// --el-color-error-light-9: #fef2f3!important;
--el-color-info: #1b8fff!important;
// --el-color-info-light-3: #47a5fc!important;
// --el-color-info-light-5: #79bdfc!important;
// --el-color-info-light-7: #a4d3fe!important;
// --el-color-info-light-8: #bedfff!important;
// --el-color-info-light-9: #e0f0ff!important;
// 头部颜色
--header-bg-color: #C7D4F6;
--workflow-bg-color: #FFF;
--sidebar-bg-color: #f6f6f7;
--sidebar-right-border-color: #9f9f9f;
// el-main中的滚动部分的颜色
--scrollbar-bg: #fff;
// 输入框背景颜色
--input-box-bg: #fff;
}
.el-header {
--el-header-padding: 0;
--el-header-height: 45px;
}
// 思维连颜色
.el-collapse {
--el-collapse-header-bg-color: transparent;
--el-collapse-content-bg-color: transparent;
}
// 头像颜色
.el-avatar {
--el-avatar-bg-color: #bbdcfc !important;
}
// 气泡颜色
.custom-bubble-content {
background-color: #f2f2f2;
/* 设置背景颜色为浅灰色 */
padding: 12px 16px;
/* 可选:添加内边距 */
border-radius: 8px;
/* 可选:添加圆角 */
width: 580px;
}
.el-main{
padding: 4px !important;
background-color: rgb(179, 189, 214);
}
\ No newline at end of file
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
css: {
preprocessorOptions: {
less: {
// 全局引入less
// additionalData: `@import "./src/assets/scss/variables.scss";`
}
}
},
server: {
proxy: {
'/v1': {
target: 'http://10.82.14.177',
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/v1/, '')
}
}
},
// build: {
// chunkSizeWarningLimit: 500,
// rollupOptions: {
// output: {
// manualChunks(id) {
// if (id.includes('node_modules')) {
// const parts = id.split('node_modules/')[1].split('/');
// return parts[0]; // 按依赖包名分包
// }
// return 'default'; // 其他模块默认分包
// },
// },
// },
// }
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment