|
@@ -0,0 +1,643 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { defineMarkdownPlugin, MarkdownPlugin } from '/@/components/markdown/types'
|
|
|
+import echartsPlugin, { EchartsPluginOptions } from '/@/components/markdown/plugins/markdown-it-echarts'
|
|
|
+import Markdown from '/@/components/markdown/index.vue'
|
|
|
+import { ref, nextTick, onMounted, computed } from 'vue'
|
|
|
+import { Local } from '/@/utils/storage'
|
|
|
+import { User, ChatDotRound } from '@element-plus/icons-vue'
|
|
|
+
|
|
|
+const plugins: Array<MarkdownPlugin<any>> = [
|
|
|
+ defineMarkdownPlugin<EchartsPluginOptions>(echartsPlugin, () => {
|
|
|
+ return {}
|
|
|
+ })
|
|
|
+]
|
|
|
+
|
|
|
+const getUserInfos = ref<{
|
|
|
+ avatar: string
|
|
|
+ userName: string
|
|
|
+}>(Local.get('userInfo') || {})
|
|
|
+
|
|
|
+// 消息类型定义
|
|
|
+interface Message {
|
|
|
+ id: number
|
|
|
+ type: 'user' | 'ai'
|
|
|
+ content: string
|
|
|
+ timestamp: Date
|
|
|
+}
|
|
|
+
|
|
|
+// 会话列表
|
|
|
+const conversations = ref([
|
|
|
+ { id: 1, title: 'Summary 1', active: true },
|
|
|
+ { id: 2, title: 'Summary 2', active: false }
|
|
|
+])
|
|
|
+
|
|
|
+// 消息列表
|
|
|
+const messages = ref<Message[]>([
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ type: 'user',
|
|
|
+ content: '请帮我分析这个数据',
|
|
|
+ timestamp: new Date()
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ type: 'ai',
|
|
|
+ content: `好的,我来为您查询相关的人员信息。首先请让我从数据库中获取所有相关百科:
|
|
|
+
|
|
|
+## Tool Calls 📊
|
|
|
+
|
|
|
+\`\`\`json
|
|
|
+{
|
|
|
+ "id": "XXX",
|
|
|
+ "params": "yyy",
|
|
|
+ "returns": "zzz"
|
|
|
+}
|
|
|
+\`\`\`
|
|
|
+
|
|
|
+...ListItem
|
|
|
+
|
|
|
+根据我查询到的信息,我找到您想要的相关人员信息:
|
|
|
+
|
|
|
+1. xxxxx
|
|
|
+2. yyyyy
|
|
|
+3. zzzzz
|
|
|
+
|
|
|
+\`\`\`echarts
|
|
|
+{
|
|
|
+ "title": {
|
|
|
+ "text": "charts 1"
|
|
|
+ },
|
|
|
+ "xAxis": {
|
|
|
+ "type": "category",
|
|
|
+ "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
|
|
+ },
|
|
|
+ "yAxis": {
|
|
|
+ "type": "value"
|
|
|
+ },
|
|
|
+ "series": [{
|
|
|
+ "data": [120, 200, 150, 80, 70, 110, 130],
|
|
|
+ "type": "bar"
|
|
|
+ }]
|
|
|
+}
|
|
|
+\`\`\``,
|
|
|
+ timestamp: new Date()
|
|
|
+ }
|
|
|
+])
|
|
|
+
|
|
|
+// 输入框内容
|
|
|
+const inputMessage = ref('')
|
|
|
+const messagesContainer = ref<HTMLElement>()
|
|
|
+
|
|
|
+// 工具选择数据
|
|
|
+const toolOptions = ref([
|
|
|
+ {
|
|
|
+ value: 'code',
|
|
|
+ label: '代码工具',
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ value: 'codebase-retrieval',
|
|
|
+ label: '代码库检索'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: 'str-replace-editor',
|
|
|
+ label: '代码编辑器'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: 'save-file',
|
|
|
+ label: '文件保存'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: 'web',
|
|
|
+ label: '网络工具',
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ value: 'web-search',
|
|
|
+ label: '网络搜索'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: 'web-fetch',
|
|
|
+ label: '网页获取'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: 'system',
|
|
|
+ label: '系统工具',
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ value: 'execute-command',
|
|
|
+ label: '命令执行'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: 'launch-process',
|
|
|
+ label: '进程启动'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+])
|
|
|
+
|
|
|
+// 模型选择数据
|
|
|
+const modelOptions = ref([
|
|
|
+ { label: 'Claude Sonnet 4', value: 'claude-sonnet-4' },
|
|
|
+ { label: 'GPT-4', value: 'gpt-4' },
|
|
|
+ { label: 'GPT-3.5 Turbo', value: 'gpt-3.5-turbo' },
|
|
|
+ { label: 'Gemini Pro', value: 'gemini-pro' }
|
|
|
+])
|
|
|
+
|
|
|
+// 选中的工具和模型
|
|
|
+const selectedTool = ref([])
|
|
|
+const selectedModel = ref('claude-sonnet-4')
|
|
|
+
|
|
|
+// 对话状态
|
|
|
+const isConversationActive = ref(false)
|
|
|
+
|
|
|
+// 发送消息
|
|
|
+const sendMessage = () => {
|
|
|
+ if (!inputMessage.value.trim()) return
|
|
|
+
|
|
|
+ // 设置对话状态为活跃
|
|
|
+ isConversationActive.value = true
|
|
|
+
|
|
|
+ // 添加用户消息
|
|
|
+ messages.value.push({
|
|
|
+ id: Date.now(),
|
|
|
+ type: 'user',
|
|
|
+ content: inputMessage.value,
|
|
|
+ timestamp: new Date()
|
|
|
+ })
|
|
|
+
|
|
|
+ const userMessage = inputMessage.value
|
|
|
+ inputMessage.value = ''
|
|
|
+
|
|
|
+ // 滚动到底部
|
|
|
+ scrollToBottom()
|
|
|
+
|
|
|
+ // 模拟AI回复
|
|
|
+ setTimeout(() => {
|
|
|
+ messages.value.push({
|
|
|
+ id: Date.now() + 1,
|
|
|
+ type: 'ai',
|
|
|
+ content: `我收到了您的消息:"${userMessage}"。这是一个示例回复,包含markdown格式的内容。
|
|
|
+
|
|
|
+## 回复内容
|
|
|
+
|
|
|
+- 列表项 1
|
|
|
+- 列表项 2
|
|
|
+- 列表项 3
|
|
|
+
|
|
|
+\`\`\`javascript
|
|
|
+console.log('这是代码示例');
|
|
|
+\`\`\``,
|
|
|
+ timestamp: new Date()
|
|
|
+ })
|
|
|
+ isConversationActive.value = false
|
|
|
+ scrollToBottom()
|
|
|
+ }, 1000)
|
|
|
+}
|
|
|
+
|
|
|
+// 终止对话
|
|
|
+const stopConversation = () => {
|
|
|
+ isConversationActive.value = false
|
|
|
+ // 这里可以添加实际的终止逻辑
|
|
|
+}
|
|
|
+
|
|
|
+// 滚动到底部
|
|
|
+const scrollToBottom = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ if (messagesContainer.value) {
|
|
|
+ messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+// 选择会话
|
|
|
+const selectConversation = (id: number) => {
|
|
|
+ conversations.value.forEach(conv => {
|
|
|
+ conv.active = conv.id === id
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ scrollToBottom()
|
|
|
+})
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <el-container class="chat-container">
|
|
|
+ <!-- 左侧会话列表 -->
|
|
|
+ <el-aside width="300px" class="chat-sidebar">
|
|
|
+ <div class="sidebar-header">
|
|
|
+ <h3>对话历史</h3>
|
|
|
+ </div>
|
|
|
+ <div class="conversation-list">
|
|
|
+ <div
|
|
|
+ v-for="conv in conversations"
|
|
|
+ :key="conv.id"
|
|
|
+ :class="['conversation-item', { active: conv.active }]"
|
|
|
+ @click="selectConversation(conv.id)"
|
|
|
+ >
|
|
|
+ {{ conv.title }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-aside>
|
|
|
+
|
|
|
+ <!-- 右侧聊天区域 -->
|
|
|
+ <el-main class="chat-main">
|
|
|
+ <!-- 消息展示区域 -->
|
|
|
+ <div class="messages-container" ref="messagesContainer">
|
|
|
+ <div
|
|
|
+ v-for="message in messages"
|
|
|
+ :key="message.id"
|
|
|
+ :class="['message-wrapper', message.type]"
|
|
|
+ >
|
|
|
+ <!-- AI消息 -->
|
|
|
+ <div v-if="message.type === 'ai'" class="ai-message-container">
|
|
|
+ <el-avatar class="message-avatar" :icon="ChatDotRound" />
|
|
|
+ <div class="message-bubble ai-bubble">
|
|
|
+ <Markdown
|
|
|
+ :content="message.content"
|
|
|
+ :plugins="plugins"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 用户消息 -->
|
|
|
+ <div v-else class="user-message-container">
|
|
|
+ <div class="message-bubble user-bubble">
|
|
|
+ {{ message.content }}
|
|
|
+ </div>
|
|
|
+ <el-avatar
|
|
|
+ class="message-avatar"
|
|
|
+ :src="getUserInfos.avatar"
|
|
|
+ :icon="User"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="messages-spacer"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="input-container">
|
|
|
+ <!-- 工具和模型选择行 -->
|
|
|
+ <div class="selection-row">
|
|
|
+ <!-- 工具选择 -->
|
|
|
+ <div class="tool-selector">
|
|
|
+ <el-cascader
|
|
|
+ v-model="selectedTool"
|
|
|
+ :options="toolOptions"
|
|
|
+ :show-all-levels="false"
|
|
|
+ :props="{ multiple: true }"
|
|
|
+ collapse-tags
|
|
|
+ collapse-tags-tooltip
|
|
|
+ clearable
|
|
|
+ size="small"
|
|
|
+ style="width: 200px"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 模型选择 -->
|
|
|
+ <div class="model-selector">
|
|
|
+ <el-select
|
|
|
+ v-model="selectedModel"
|
|
|
+ placeholder="选择模型"
|
|
|
+ size="small"
|
|
|
+ style="width: 200px"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in modelOptions"
|
|
|
+ :key="item.value"
|
|
|
+ :label="item.label"
|
|
|
+ :value="item.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 输入框和按钮行 -->
|
|
|
+ <div class="input-row">
|
|
|
+ <!-- 输入框 -->
|
|
|
+ <div class="message-input-wrapper">
|
|
|
+ <el-input
|
|
|
+ v-model="inputMessage"
|
|
|
+ type="textarea"
|
|
|
+ placeholder="请输入您的问题..."
|
|
|
+ :rows="2"
|
|
|
+ resize="none"
|
|
|
+ @keydown.enter.ctrl="sendMessage"
|
|
|
+ @keydown.enter.meta="sendMessage"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 按钮组 -->
|
|
|
+ <div class="button-group">
|
|
|
+ <el-button
|
|
|
+ v-if="!isConversationActive"
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ @click="sendMessage"
|
|
|
+ :disabled="!inputMessage.trim()"
|
|
|
+ >
|
|
|
+ 发送
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ v-else
|
|
|
+ type="danger"
|
|
|
+ size="small"
|
|
|
+ @click="stopConversation"
|
|
|
+ >
|
|
|
+ 终止
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-main>
|
|
|
+ </el-container>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.chat-container {
|
|
|
+ height: 100%;
|
|
|
+ background: var(--el-bg-color-page);
|
|
|
+}
|
|
|
+/* 左侧边栏样式 */
|
|
|
+.chat-sidebar {
|
|
|
+ background: var(--el-bg-color);
|
|
|
+ border-right: 1px solid var(--el-border-color-light);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.sidebar-header {
|
|
|
+ padding: 20px;
|
|
|
+ border-bottom: 1px solid var(--el-border-color-light);
|
|
|
+
|
|
|
+ h3 {
|
|
|
+ margin: 0;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.conversation-list {
|
|
|
+ flex: 1;
|
|
|
+ padding: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.conversation-item {
|
|
|
+ padding: 12px 16px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ border-radius: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+ border: 1px solid transparent;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: var(--el-fill-color-light);
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ background: var(--el-color-primary-light-9);
|
|
|
+ color: var(--el-color-primary);
|
|
|
+ border-color: var(--el-color-primary-light-7);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 右侧聊天区域样式 */
|
|
|
+.chat-main {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ padding: 0;
|
|
|
+ background: var(--el-bg-color-page);
|
|
|
+
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.messages-container {
|
|
|
+ flex: 1;
|
|
|
+ padding: 20px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.messages-spacer {
|
|
|
+ height: 160px;
|
|
|
+}
|
|
|
+
|
|
|
+.message-wrapper {
|
|
|
+ margin-bottom: 20px;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* AI消息样式 */
|
|
|
+.ai-message-container {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.ai-bubble {
|
|
|
+ background: #f3f4f6;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ position: relative;
|
|
|
+ max-width: 70%;
|
|
|
+
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ left: -8px;
|
|
|
+ top: 12px;
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ border-right: 8px solid #f3f4f6;
|
|
|
+ border-top: 8px solid transparent;
|
|
|
+ border-bottom: 8px solid transparent;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 用户消息样式 */
|
|
|
+.user-message-container {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 12px;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+.user-bubble {
|
|
|
+ background: #4ade80;
|
|
|
+ color: white;
|
|
|
+ position: relative;
|
|
|
+ max-width: 70%;
|
|
|
+
|
|
|
+ &::after {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ right: -8px;
|
|
|
+ top: 12px;
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ border-left: 8px solid #4ade80;
|
|
|
+ border-top: 8px solid transparent;
|
|
|
+ border-bottom: 8px solid transparent;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 消息气泡通用样式 */
|
|
|
+.message-bubble {
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-radius: 12px;
|
|
|
+ line-height: 1.5;
|
|
|
+ word-wrap: break-word;
|
|
|
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.message-avatar {
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-top: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.tools-button {
|
|
|
+ margin-bottom: 10px;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+}
|
|
|
+
|
|
|
+.message-input {
|
|
|
+ margin-bottom: 12px;
|
|
|
+
|
|
|
+ :deep(.el-textarea__inner) {
|
|
|
+ border-radius: 8px;
|
|
|
+ resize: none;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.input-actions {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+/* Markdown 内容样式优化 */
|
|
|
+:deep(.markdown-content) {
|
|
|
+ h2 {
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ margin: 16px 0 12px 0;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ ul, ol {
|
|
|
+ margin: 12px 0;
|
|
|
+ padding-left: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ li {
|
|
|
+ margin: 4px 0;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+ }
|
|
|
+
|
|
|
+ pre {
|
|
|
+ background: rgba(0, 0, 0, 0.05);
|
|
|
+ border-radius: 6px;
|
|
|
+ margin: 12px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ code {
|
|
|
+ background: rgba(0, 0, 0, 0.05);
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ p {
|
|
|
+ margin: 8px 0;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.input-container {
|
|
|
+ position: absolute;
|
|
|
+ width: 85%;
|
|
|
+ left: 50%;
|
|
|
+ bottom: 25px;
|
|
|
+ transform: translate(-50%, 0);
|
|
|
+ background: var(--el-bg-color);
|
|
|
+ border: 1px solid var(--el-border-color-light);
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 16px;
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.selection-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.tool-selector,
|
|
|
+.model-selector {
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.input-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.message-input-wrapper {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.message-input-wrapper :deep(.el-textarea__inner) {
|
|
|
+ border-radius: 8px;
|
|
|
+ resize: none;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
+
|
|
|
+.button-group {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.button-group .el-button {
|
|
|
+ min-width: 60px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式调整 */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .input-container {
|
|
|
+ width: 95%;
|
|
|
+ padding: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .selection-row {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: stretch;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tool-selector,
|
|
|
+ .model-selector {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tool-selector :deep(.el-cascader),
|
|
|
+ .model-selector :deep(.el-select) {
|
|
|
+ width: 100% !important;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|