first commit

This commit is contained in:
JianGuo 2025-04-16 13:12:10 +08:00
commit 0fe225c858
23 changed files with 4763 additions and 0 deletions

30
.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

29
README.md Normal file
View File

@ -0,0 +1,29 @@
# wxproject
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

530
api/resp.txt Normal file

File diff suppressed because one or more lines are too long

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

2804
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "wxproject",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"echarts": "^5.6.0",
"marked": "^15.0.8",
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"vite": "^6.2.4",
"vite-plugin-vue-devtools": "^7.7.2"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

12
public/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<title>智能对话系统</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

56
src/App.vue Normal file
View File

@ -0,0 +1,56 @@
<script setup>
import ChatInterface from './components/ChatInterface.vue'
import ChatModeSelector from './components/ChatModeSelector.vue'
import { ref } from 'vue'
const currentMode = ref({
id: 'training',
token: 'app-88ae2GN49aUyNO6qGg7tbTfX'
})
const handleModeChange = (mode) => {
currentMode.value = mode
}
</script>
<template>
<div class="app">
<div class="app-container">
<ChatModeSelector @mode-changed="handleModeChange" />
<div class="chat-wrapper">
<ChatInterface :chatMode="currentMode" />
</div>
</div>
</div>
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
}
.app {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.app-container {
height: 100%;
display: flex;
overflow: hidden;
}
.chat-wrapper {
flex: 1;
height: 100%;
overflow: hidden;
background-color: #f5f5f5;
position: relative;
}
</style>

86
src/assets/base.css Normal file
View File

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

35
src/assets/main.css Normal file
View File

@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 100vh;
margin: 0 auto;
/* padding: 2rem; */
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: flex;
/* grid-template-columns: 1fr 1fr; */
/* padding: 0 2rem; */
}
}

View File

@ -0,0 +1,933 @@
<template>
<div class="chat-container">
<div class="chat-messages" ref="chatContainer">
<div v-for="(message, index) in messages" :key="index"
:class="['message', message.type === 'ai' ? 'ai-message' : 'user-message']">
<div class="message-wrapper">
<div class="avatar">
<span class="emoji">{{ message.type === 'ai' ? '🤖' : '👤' }}</span>
</div>
<div class="message-content">
<div class="message-text" v-html="formatMessage(message.content)"></div>
<div v-if="message.type === 'user' && message.evaluation"
class="evaluation-section">
<div class="evaluation-title" @click="toggleEvaluation(index)">
评价解析 {{ message.showEvaluation ? '▼' : '▶' }}
</div>
<div v-show="message.showEvaluation" class="evaluation-content">
<div v-show="message.pingfen" class="pingfen" v-html="formatMessage(message.pingfen)"></div>
<div v-show="message.zongjie" class="zongjie" v-html="formatMessage(message.zongjie)"></div>
</div>
</div>
</div>
</div>
</div>
<div v-if="isTyping" class="message ai-message">
<div class="message-wrapper">
<div class="avatar">
<span class="emoji">🤖</span>
</div>
<div class="message-content">
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
</div>
<div v-if="showScore" class="score-overlay">
<div class="score-container">
<h2>成绩展示</h2>
<div class="score-chart" ref="scoreChart"></div>
<div class="score-value">{{ score }}</div>
</div>
</div>
<div class="input-area">
<button class="new-chat-btn" @click="startNewChat" :disabled="loading">
<span class="new-chat-icon">+</span>
新会话
</button>
<textarea
v-model="newMessage"
@keyup.enter.exact="sendMessage"
@keydown.enter.exact.prevent
placeholder="请输入消息..."
:disabled="loading"
rows="1"
ref="messageInput"
@input="adjustTextareaHeight"
></textarea>
<button @click="sendMessage" :disabled="loading">发送</button>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts';
import { marked } from 'marked';
export default {
name: 'ChatInterface',
props: {
chatMode: {
type: Object,
default: () => ({
id: 'training',
token: 'app-88ae2GN49aUyNO6qGg7tbTfX'
})
}
},
data() {
return {
messages: [],
newMessage: '',
loading: false,
evaluation: null,
showEvaluation: false,
score: '',
summary: '',
chart: null,
apiEndpoint: 'http://160.202.224.52:31002/v1/chat-messages',
conversationId: '',
isTyping: false,
maxRows: 6
}
},
created() {
// marked
marked.setOptions({
breaks: true, // GitHub
gfm: true, // GitHubMarkdown
sanitize: false // HTML
});
// localStorage
this.loadChatHistory();
},
watch: {
'chatMode.id': {
handler(newModeId, oldModeId) {
if (oldModeId) {
//
this.saveChatHistory(oldModeId);
}
//
this.loadChatHistory(newModeId);
},
immediate: true
},
messages: {
handler(newMessages) {
// localStorage
if (this.chatMode.id) {
this.saveChatHistory(this.chatMode.id);
}
},
deep: true
}
},
methods: {
saveChatHistory(modeId) {
const history = {
messages: this.messages,
conversationId: this.conversationId
};
localStorage.setItem(`chat_history_${modeId}`, JSON.stringify(history));
},
loadChatHistory(modeId = this.chatMode.id) {
const savedHistory = localStorage.getItem(`chat_history_${modeId}`);
if (savedHistory) {
const history = JSON.parse(savedHistory);
this.messages = history.messages;
this.conversationId = history.conversationId;
} else {
//
this.messages = [];
this.conversationId = '';
}
},
async sendMessage() {
if (!this.newMessage.trim()) return;
const userMessage = {
type: 'user',
content: this.newMessage
};
this.messages.push(userMessage);
this.loading = true;
const currentMessage = this.newMessage;
this.newMessage = '';
this.$refs.messageInput.style.height = 'auto';
try {
this.isTyping = true;
const response = await fetch(this.apiEndpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.chatMode.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
inputs: {},
query: currentMessage,
response_mode: 'streaming',
conversation_id: this.conversationId,
user: 'sys123'
})
});
if (!response.ok) {
throw new Error('网络请求失败');
}
const reader = response.body.getReader();
let aiMessage = null;
let aiContent = '';
let evaluationContent = {
pingfen: '',
zongjie: '',
dafen: ''
};
let currentTag = null;
let isFirstChunk = true;
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = new TextDecoder().decode(value);
buffer += text;
//
const lines = buffer.split('\n');
buffer = lines.pop() || ''; //
for (const line of lines) {
if (!line.trim() || !line.startsWith('data: ')) continue;
try {
const jsonStr = line.substring(6);
const jsonData = JSON.parse(jsonStr);
// conversation_id
if (jsonData.conversation_id && !this.conversationId) {
this.conversationId = jsonData.conversation_id;
}
if (jsonData.event === 'message' && jsonData.answer) {
const answer = jsonData.answer;
//
if (answer.includes('<kehu>')) {
currentTag = 'kehu';
} else if (answer.includes('<pingfen>')) {
currentTag = 'pingfen';
} else if (answer.includes('<zongjie>')) {
currentTag = 'zongjie';
} else if (answer.includes('<dafen>')) {
currentTag = 'dafen';
}
//
if (answer.includes('</kehu>')) {
currentTag = null;
} else if (answer.includes('</pingfen>')) {
currentTag = null;
} else if (answer.includes('</zongjie>')) {
currentTag = null;
} else if (answer.includes('</dafen>')) {
currentTag = null;
}
//
if (currentTag === 'kehu' || !currentTag) {
// kehu
const cleanedAnswer = answer.replace(/<\/?kehu>/g, '');
if (cleanedAnswer.trim()) {
if (isFirstChunk) {
this.isTyping = false;
aiMessage = {
type: 'ai',
content: ''
};
this.messages.push(aiMessage);
isFirstChunk = false;
}
aiContent += cleanedAnswer;
if (aiMessage) {
aiMessage.content = aiContent;
// 使requestAnimationFrame
requestAnimationFrame(() => {
this.scrollToBottom();
});
}
}
} else if (currentTag === 'pingfen') {
evaluationContent.pingfen += answer.replace(/<\/?pingfen>/g, '');
} else if (currentTag === 'zongjie') {
evaluationContent.zongjie += answer.replace(/<\/?zongjie>/g, '');
} else if (currentTag === 'dafen') {
evaluationContent.dafen += answer.replace(/<\/?dafen>/g, '');
}
}
//
if (jsonData.event === 'message_end' || jsonData.event === 'workflow_finished') {
//
if (evaluationContent.pingfen || evaluationContent.zongjie) {
const lastUserMessageIndex = this.messages.length - 2;
if (lastUserMessageIndex >= 0) {
this.messages[lastUserMessageIndex].evaluation = true;
this.messages[lastUserMessageIndex].showEvaluation = false;
this.messages[lastUserMessageIndex].pingfen = evaluationContent.pingfen;
this.messages[lastUserMessageIndex].zongjie = evaluationContent.zongjie;
}
}
//
if (evaluationContent.dafen) {
this.score = evaluationContent.dafen;
if (this.$refs.scoreChart) {
this.initChart();
}
}
}
} catch (e) {
console.error('Error parsing SSE message:', e);
}
}
}
} catch (error) {
console.error('发送消息失败:', error);
this.messages.push({
type: 'ai',
content: '抱歉,发生了一些错误。请稍后重试。'
});
} finally {
this.loading = false;
this.isTyping = false;
this.$nextTick(() => {
this.scrollToBottom();
});
}
},
toggleEvaluation(index) {
this.messages[index].showEvaluation = !this.messages[index].showEvaluation;
},
scrollToBottom() {
this.$nextTick(() => {
const container = this.$refs.chatContainer;
container.scrollTop = container.scrollHeight;
});
},
initChart() {
if (this.chart) {
this.chart.dispose();
}
this.chart = echarts.init(this.$refs.scoreChart);
// 1:1,2:2,...
const scoreData = this.score.split(',').map(item => {
const [name, value] = item.split(':');
return { name, value: parseFloat(value) };
});
const option = {
title: {
text: '成绩维度分析',
left: 'center'
},
tooltip: {
trigger: 'item'
},
radar: {
indicator: scoreData.map(item => ({
name: item.name,
max: 100
}))
},
series: [{
type: 'radar',
data: [{
value: scoreData.map(item => item.value),
name: '成绩',
areaStyle: {
color: 'rgba(76, 175, 80, 0.3)'
},
lineStyle: {
color: '#4CAF50'
},
itemStyle: {
color: '#4CAF50'
}
}]
}]
};
this.chart.setOption(option);
//
window.addEventListener('resize', this.handleResize);
},
handleResize() {
if (this.chart) {
this.chart.resize();
}
},
formatMessage(text) {
if (!text) return '';
//
text = text.replace(/<\/?(kehu|pingfen|zongjie|chengji|dafen)[^>]*>/g, '');
// 使markedMarkdown
try {
return marked(text);
} catch (e) {
console.error('Markdown parsing error:', e);
return text;
}
},
adjustTextareaHeight() {
const textarea = this.$refs.messageInput;
textarea.style.height = 'auto';
const newHeight = Math.min(textarea.scrollHeight, this.maxRows * 24); // 24px
textarea.style.height = newHeight + 'px';
},
startNewChat() {
//
this.messages = [];
// ID
this.conversationId = '';
//
this.evaluation = null;
this.showEvaluation = false;
this.score = '';
this.summary = '';
//
this.saveChatHistory(this.chatMode.id);
}
},
beforeUnmount() {
//
window.removeEventListener('resize', this.handleResize);
if (this.chart) {
this.chart.dispose();
}
}
}
</script>
<style scoped>
.chat-container {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
background-color: #f5f5f5;
position: relative;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
padding-bottom: 100px;
}
.message {
margin-bottom: 20px;
width: 100%;
position: relative;
}
.ai-message {
margin-right: auto;
}
.user-message {
margin-left: auto;
}
.message-wrapper {
display: flex;
align-items: flex-start;
gap: 12px;
max-width: 66.666%;
position: relative;
}
.avatar {
width: 40px;
height: 40px;
flex-shrink: 0;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.emoji {
font-size: 24px;
line-height: 1;
}
.user-message .avatar {
background: #e8f5e8;
}
.ai-message .avatar {
background: #f0f2f5;
}
.message-content {
flex: 1;
min-width: 0;
word-wrap: break-word;
position: relative;
}
.ai-message .message-wrapper {
margin-right: auto;
}
.user-message .message-wrapper {
margin-left: auto;
flex-direction: row-reverse;
}
.message-text {
line-height: 1.6;
font-size: 15px;
padding: 12px 16px;
border-radius: 3px;
position: relative;
margin: 0 12px;
}
.ai-message .message-text {
background: #fff;
color: #333;
}
.ai-message .message-text::before {
content: '';
position: absolute;
left: -8px;
top: 14px;
width: 0;
height: 0;
border-style: solid;
border-width: 6px 8px 6px 0;
border-color: transparent #fff transparent transparent;
}
.user-message .message-text {
background: #95ec69;
color: #333;
}
.user-message .message-text::before {
content: '';
position: absolute;
right: -8px;
top: 14px;
width: 0;
height: 0;
border-style: solid;
border-width: 6px 0 6px 8px;
border-color: transparent transparent transparent #95ec69;
}
.evaluation-section {
margin-top: 8px;
font-size: 14px;
width: 100%;
background: #fff;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.evaluation-title {
cursor: pointer;
color: #576b95;
font-weight: normal;
padding: 10px 16px;
background: #f7f7f7;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
justify-content: space-between;
}
.evaluation-title:hover {
background: #e8e8e8;
}
.evaluation-content {
margin-top: 0;
padding: 16px;
background: #fff;
color: #333;
line-height: 1.6;
}
.evaluation-content :deep(ul),
.evaluation-content :deep(ol) {
margin: 8px 0;
padding-left: 24px;
list-style-position: outside;
}
.evaluation-content :deep(li) {
margin: 4px 0;
padding-left: 4px;
}
.evaluation-content :deep(blockquote) {
margin: 12px 0;
padding: 0 0 0 16px;
border-left: 4px solid #ddd;
color: #666;
}
.evaluation-content :deep(pre) {
margin: 12px 0;
padding: 16px;
background-color: #f8f9fa;
border-radius: 4px;
overflow-x: auto;
}
.evaluation-content :deep(code) {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
font-size: 0.9em;
padding: 2px 4px;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 3px;
}
.evaluation-content :deep(pre code) {
padding: 0;
background-color: transparent;
}
.evaluation-content :deep(p) {
margin: 12px 0;
line-height: 1.6;
}
.evaluation-content :deep(h1),
.evaluation-content :deep(h2),
.evaluation-content :deep(h3),
.evaluation-content :deep(h4),
.evaluation-content :deep(h5),
.evaluation-content :deep(h6) {
margin: 16px 0 8px;
line-height: 1.4;
}
.evaluation-content :deep(h1) { font-size: 1.5em; margin-top: 24px; }
.evaluation-content :deep(h2) { font-size: 1.3em; margin-top: 20px; }
.evaluation-content :deep(h3) { font-size: 1.2em; margin-top: 16px; }
.evaluation-content :deep(h4) { font-size: 1.1em; }
.evaluation-content :deep(h5),
.evaluation-content :deep(h6) { font-size: 1em; }
.evaluation-content .pingfen,
.evaluation-content .zongjie {
color: #333;
margin-bottom: 16px;
}
.evaluation-content .pingfen:last-child,
.evaluation-content .zongjie:last-child {
margin-bottom: 0;
}
.input-area {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
gap: 12px;
padding: 12px 16px;
background: #f5f5f5;
border-top: 1px solid #e5e5e5;
align-items: flex-end;
}
textarea {
flex: 1;
padding: 10px 16px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 15px;
outline: none;
transition: all 0.3s;
background: #fff;
resize: none;
min-height: 42px;
max-height: calc(24px * 6);
line-height: 1.5;
}
textarea:focus {
border-color: #07c160;
box-shadow: 0 0 0 2px rgba(7, 193, 96, 0.1);
}
button {
padding: 0 24px;
background: #07c160;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
height: 42px;
line-height: 42px;
white-space: nowrap;
}
button:hover {
background: #06ae56;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
/* 优化滚动条 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.1);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(0,0,0,0.2);
}
.score-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.score-container {
background: white;
padding: 24px;
border-radius: 8px;
text-align: center;
width: 90%;
max-width: 600px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.score-container h2 {
margin: 0 0 20px;
color: #333;
font-size: 18px;
font-weight: 500;
}
.score-chart {
width: 100%;
height: 400px;
margin: 20px auto;
}
.score-value {
font-size: 16px;
color: #07c160;
margin-top: 16px;
padding: 8px 16px;
background: #f6ffed;
border-radius: 4px;
display: inline-block;
}
.typing-indicator {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 8px 16px;
background: #fff;
border-radius: 3px;
margin: 0 12px;
}
.typing-indicator span {
width: 6px;
height: 6px;
background: #95ec69;
border-radius: 50%;
animation: typing 1s infinite ease-in-out;
opacity: 0.6;
}
@keyframes typing {
0%, 100% {
transform: scale(1);
opacity: 0.5;
}
50% {
transform: scale(1.2);
opacity: 1;
}
}
.new-chat-btn {
padding: 0 16px;
background: #f5f5f5;
border: 1px solid #ddd;
color: #333;
font-size: 14px;
display: flex;
align-items: center;
gap: 6px;
height: 42px;
transition: all 0.3s;
}
.new-chat-btn:hover {
background: #e8e8e8;
border-color: #ccc;
}
.new-chat-btn:disabled {
background: #f5f5f5;
border-color: #ddd;
color: #999;
cursor: not-allowed;
}
.new-chat-icon {
font-size: 18px;
font-weight: bold;
line-height: 1;
}
/* 修改输入区域样式以适应新按钮 */
.input-area {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
gap: 12px;
padding: 12px 16px;
background: #f5f5f5;
border-top: 1px solid #e5e5e5;
align-items: flex-end;
}
/* 发送按钮样式保持绿色 */
.input-area button:last-child {
background: #07c160;
color: white;
}
.input-area button:last-child:hover {
background: #06ae56;
}
.input-area button:last-child:disabled {
background: #ccc;
}
/* 添加全局样式以支持Markdown渲染 */
.message-text :deep(h1),
.message-text :deep(h2),
.message-text :deep(h3),
.message-text :deep(h4),
.message-text :deep(h5),
.message-text :deep(h6) {
margin: 16px 0 8px;
font-weight: 600;
line-height: 1.25;
}
.message-text :deep(h1) { font-size: 1.5em; }
.message-text :deep(h2) { font-size: 1.3em; }
.message-text :deep(h3) { font-size: 1.2em; }
.message-text :deep(h4) { font-size: 1.1em; }
.message-text :deep(h5),
.message-text :deep(h6) { font-size: 1em; }
.message-text :deep(p) {
margin: 8px 0;
}
.message-text :deep(ul),
.message-text :deep(ol) {
margin: 8px 0;
padding-left: 24px;
list-style-position: outside;
}
.message-text :deep(li) {
margin: 4px 0;
padding-left: 4px;
}
.message-text :deep(blockquote) {
margin: 12px 0;
padding: 0 0 0 16px;
border-left: 4px solid #ddd;
color: #666;
}
.message-text :deep(pre) {
margin: 12px 0;
padding: 16px;
background-color: #f8f9fa;
border-radius: 4px;
overflow-x: auto;
}
.message-text :deep(code) {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
font-size: 0.9em;
padding: 2px 4px;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 3px;
}
.message-text :deep(pre code) {
padding: 0;
background-color: transparent;
}
.message-text :deep(p) {
margin: 12px 0;
line-height: 1.6;
}
</style>

View File

@ -0,0 +1,131 @@
<template>
<div class="mode-selector">
<h3 class="mode-title">对话场景</h3>
<div class="mode-list">
<div
v-for="mode in chatModes"
:key="mode.id"
:class="['mode-item', { active: currentMode === mode.id }]"
@click="selectMode(mode.id)"
>
<i :class="mode.icon"></i>
<span>{{ mode.name }}</span>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ChatModeSelector',
data() {
return {
currentMode: 'training',
chatModes: [
{
id: 'training',
name: '寿险代理人AI陪练',
icon: 'fas fa-user-tie',
token: 'app-88ae2GN49aUyNO6qGg7tbTfX'
},
{
id: 'quote_objection',
name: '报价中异议',
icon: 'fas fa-comments',
token: 'app-88ae2GN49aUyNO6qGg7tbTfX'
},
{
id: 'post_quote_objection',
name: '报价后异议',
icon: 'fas fa-comment-dollar',
token: 'app-88ae2GN49aUyNO6qGg7tbTfX'
}
]
}
},
methods: {
selectMode(modeId) {
this.currentMode = modeId;
const selectedMode = this.chatModes.find(mode => mode.id === modeId);
this.$emit('mode-changed', {
id: modeId,
token: selectedMode.token
});
}
}
}
</script>
<style scoped>
.mode-selector {
width: 240px;
min-width: 240px;
background: #ffffff;
border-right: 1px solid #e0e0e0;
height: 100%;
padding: 20px 0;
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
}
.mode-title {
padding: 0 20px;
margin-bottom: 20px;
color: #333;
font-size: 16px;
font-weight: bold;
}
.mode-list {
display: flex;
flex-direction: column;
gap: 5px;
}
.mode-item {
padding: 15px 20px;
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
transition: all 0.3s ease;
color: #333;
}
.mode-item:hover {
background: #f5f5f5;
}
.mode-item.active {
background: #4CAF50;
color: white;
}
.mode-item i {
font-size: 18px;
width: 24px;
text-align: center;
}
.mode-item span {
font-size: 14px;
}
@media (prefers-color-scheme: dark) {
.mode-selector {
background: #1e1e1e;
border-right-color: #333;
}
.mode-title {
color: #fff;
}
.mode-item {
color: #fff;
}
.mode-item:hover {
background: #2d2d2d;
}
}
</style>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

6
src/main.js Normal file
View File

@ -0,0 +1,6 @@
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

18
vite.config.js Normal file
View File

@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})