Files
agent-frontend-web/docs/superpowers/plans/2026-07-15-shuzhi-ai-chat-plan.md
T
2026-08-07 13:56:29 +08:00

510 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 数智 AI 聊天页面实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 创建独立的「数智 AI」聊天页面,实现流式对话功能
**Architecture:** 单文件 Vue 组件实现,组合式 API + ElementPlus 组件,左侧边栏 + 中间消息区 + 底部输入栏的三栏布局
**Tech Stack:** Vue3 Composition API, ElementPlus, SCSS
---
## Task 1: 创建 frontend 目录结构
**Files:**
- 创建: `src/views/frontend/` (目录)
- [ ] **Step 1: 创建目录**
命令: `mkdir -p src/views/frontend`
---
## Task 2: 创建主页面组件 index.vue
**Files:**
- 创建: `src/views/frontend/index.vue`
- [ ] **Step 1: 编写页面基础模板结构**
```vue
<template>
<div class="shuzhi-ai-container">
<!-- 左侧边栏 -->
<aside class="sidebar">
<!-- LOGO -->
<div class="logo-bar">
<span class="logo-text">数智 AI</span>
<el-button link @click="handleComingSoon">
<el-icon><Fold /></el-icon>
</el-button>
</div>
<!-- Tab 切换 -->
<div class="tab-switch">
<el-button size="small" @click="handleComingSoon">项目</el-button>
<el-button size="small" @click="handleComingSoon">通用</el-button>
</div>
<!-- 树形列表 -->
<div class="tree-section">
<div class="tree-node tree-node-level1">
<span class="node-label">数智AI Demo</span>
<el-button link size="small" @click="handleComingSoon">×</el-button>
<el-button link size="small" @click="handleComingSoon">+</el-button>
</div>
<div class="tree-node tree-node-level2" @click="handleComingSoon">
<span class="node-label">欢迎使用 数智AI</span>
<span class="node-hint">57 分钟前</span>
</div>
</div>
</aside>
<!-- 中间主内容区 -->
<main class="main-content">
<!-- 面包屑和功能导航 -->
<div class="content-header">
<el-breadcrumb separator="/">
<el-breadcrumb-item>general</el-breadcrumb-item>
<el-breadcrumb-item>智能体</el-breadcrumb-item>
<el-breadcrumb-item>欢迎使用 数智AI</el-breadcrumb-item>
</el-breadcrumb>
<div class="nav-buttons">
<el-button v-for="btn in navButtons" :key="btn" @click="handleComingSoon">{{ btn }}</el-button>
</div>
</div>
<!-- 消息区域 -->
<el-scrollbar ref="scrollRef" class="messages-container">
<div class="messages-wrapper">
<template v-for="(msg, index) in messages" :key="index">
<div class="message-divider" v-if="index > 0"></div>
<!-- 用户消息 -->
<div v-if="msg.role === 'user'" class="message-item user">
<div class="message-bubble">
<span class="message-tag">已处理 1m</span>
<div class="message-content">{{ msg.content }}</div>
</div>
</div>
<!-- AI 消息 -->
<div v-else class="message-item assistant">
<div class="message-bubble">
<div class="message-content" v-html="formatMessage(msg.content)"></div>
</div>
</div>
</template>
</div>
</el-scrollbar>
<!-- 底部输入栏 -->
<div class="input-bar">
<div class="input-controls">
<el-button size="small" @click="handleComingSoon">智能体</el-button>
<el-select size="small" placeholder="选择" @click="handleComingSoon" style="width: 100px">
<el-option label="选项1" value="1" />
</el-select>
<el-button size="small" @click="handleComingSoon">@</el-button>
<el-button size="small" @click="handleComingSoon">权限</el-button>
</div>
<div class="input-wrapper">
<el-input
v-model="inputText"
type="textarea"
:rows="2"
placeholder="告诉 数智AI 你想完成什么..."
resize="none"
@keydown.enter.exact.prevent="handleSend"
/>
<el-button
type="primary"
class="send-btn"
:disabled="!inputText.trim() || loading"
@click="handleSend"
:loading="loading"
>
发送
</el-button>
</div>
</div>
</main>
</div>
</template>
```
- [ ] **Step 2: 编写 script setup 部分**
```javascript
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Fold } from '@element-plus/icons-vue'
const inputText = ref('')
const messages = ref([])
const loading = ref(false)
const scrollRef = ref()
const navButtons = ['智能体', '文件', '技能', '路由', '记忆', '常驻']
function handleComingSoon() {
ElMessage.info('敬请期待')
}
function formatMessage(content) {
if (!content) return ''
// 高亮代码关键词
return content
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\n/g, '<br/>')
}
async function handleSend() {
if (!inputText.value.trim() || loading.value) return
const userContent = inputText.value.trim()
inputText.value = ''
// 添加用户消息
messages.value.push({
role: 'user',
content: userContent
})
scrollToBottom()
loading.value = true
try {
const response = await fetch('http://192.168.5.125:18000/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': 'Bearer 5c49dd2a721e0f408ea49298b6c13ddf17267eb0b64e3da7019ee581f597408e',
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'qwen3.6-27b-nvfp4',
messages: [
{
role: 'user',
content: `/no_think\n ${userContent}`
}
],
max_tokens: 200,
temperature: 0.7
})
})
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`)
}
// 处理流式响应
const reader = response.body.getReader()
const decoder = new TextDecoder()
let aiContent = ''
// 添加 AI 消息占位
messages.value.push({
role: 'assistant',
content: ''
})
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
aiContent += chunk
// 更新最后一条 AI 消息
const lastMsg = messages.value[messages.value.length - 1]
lastMsg.content = aiContent
scrollToBottom()
}
} catch (error) {
ElMessage.error('问答失败:' + error.message)
} finally {
loading.value = false
}
}
function scrollToBottom() {
setTimeout(() => {
if (scrollRef.value) {
scrollRef.value.setScrollTop(9999999)
}
}, 100)
}
</script>
```
- [ ] **Step 3: 编写样式部分**
```scss
<style scoped lang="scss">
.shuzhi-ai-container {
display: flex;
height: 100vh;
overflow: hidden;
}
.sidebar {
width: 220px;
background: #F8F9FA;
display: flex;
flex-direction: column;
border-right: 1px solid #E5E7EB;
}
.logo-bar {
height: 56px;
background: #0F172A;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
flex-shrink: 0;
.logo-text {
color: #fff;
font-size: 16px;
font-weight: 600;
}
}
.tab-switch {
padding: 12px;
display: flex;
gap: 8px;
border-bottom: 1px solid #E5E7EB;
}
.tree-section {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.tree-node {
display: flex;
align-items: center;
padding: 8px;
border-radius: 8px;
cursor: pointer;
&:hover {
background: #E5E7EB;
}
}
.tree-node-level1 {
justify-content: space-between;
.node-label {
font-weight: 600;
color: #111;
}
}
.tree-node-level2 {
flex-direction: column;
align-items: flex-start;
margin-left: 16px;
padding: 6px 8px;
.node-label {
color: #222;
}
.node-hint {
font-size: 12px;
color: #888;
margin-top: 2px;
}
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
overflow: hidden;
}
.content-header {
padding: 12px 24px;
border-bottom: 1px solid #E5E7EB;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.nav-buttons {
display: flex;
gap: 8px;
}
.messages-container {
flex: 1;
overflow: hidden;
padding: 20px;
}
.messages-wrapper {
max-width: 800px;
margin: 0 auto;
}
.message-divider {
height: 1px;
background: #E5E7EB;
margin: 20px 0;
}
.message-item {
display: flex;
&.user {
justify-content: flex-end;
.message-bubble {
background: #F1F3F5;
border-radius: 8px;
padding: 12px 16px;
max-width: 70%;
}
.message-tag {
display: block;
font-size: 12px;
color: #888;
margin-bottom: 4px;
}
.message-content {
color: #222;
line-height: 1.6;
}
}
&.assistant {
justify-content: flex-start;
.message-bubble {
padding: 0;
max-width: 70%;
}
.message-content {
color: #222;
line-height: 1.8;
:deep(code) {
background: #EEEEEE;
padding: 2px 4px;
border-radius: 4px;
font-family: monospace;
}
}
}
}
.input-bar {
padding: 16px 24px;
border-top: 1px solid #E5E7EB;
flex-shrink: 0;
}
.input-controls {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.input-wrapper {
display: flex;
gap: 12px;
align-items: flex-end;
.el-textarea {
flex: 1;
}
}
.send-btn {
height: 60px;
width: 80px;
background: #F1F3F5;
border: none;
color: #444;
border-radius: 8px;
&:not(:disabled):hover {
background: #E5E7EB;
}
&:disabled {
background: #F8F9FA;
color: #bbb;
}
}
</style>
```
---
## Task 3: 添加路由配置
**Files:**
- 修改: `src/router/index.js` (在 dynamicRoutes 数组末尾添加)
- [ ] **Step 1: 添加路由配置**
`dynamicRoutes` 数组末尾添加:
```javascript
{
path: '/frontend',
component: Layout,
children: [
{
path: 'index',
component: () => import('@/views/frontend/index'),
name: 'Frontend',
meta: { title: '数智 AI' }
}
]
}
```
---
## Task 4: 验证实现
- [ ] **Step 1: 检查文件是否创建成功**
命令: `ls -la src/views/frontend/`
- [ ] **Step 2: 验证路由配置语法**
检查 `src/router/index.js` 中是否有新增的 `/frontend` 路由
---
## 自检清单
- [ ] Spec 覆盖:所有需求点都有对应实现
- [ ] 占位符扫描:无 TBD、TODO 等占位符
- [ ] 类型一致性:所有方法名、变量名一致
---
**Plan complete and saved to `docs/superpowers/plans/2026-07-15-shuzhi-ai-chat-plan.md`**
Two execution options:
**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration
**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
**Which approach?**