项目地址
- 在线预览:frontend-ai-assistant-two.vercel.app/
- GitHub 源码:github.com/hewq/dify-c…
说明:当前在线版本部署在 Vercel,国内网络访问可能不稳定。如果打不开,可以直接查看 GitHub 源码并在本地启动。
前言
上一篇我们已经在 Dify 里搭好了一个最小 RAG 应用:
用户输入 → 知识检索 → LLM → 直接回复
并且解决了一个很容易踩的坑:知识检索节点命中了文档,但 LLM 没有真正读到上下文。
这一篇开始写前端代码。
目标很简单:
用 Vite + React 做一个最小 AI 聊天页面,能够输入问题、发送请求、展示 Dify 返回的回答。
不过这里先说明一下:这一篇会先做一个最小可用版本,重点是跑通前端调用链路。API Key 安全问题会在下一篇用 Express BFF 代理来解决。
最终我们会把调用方式从:
浏览器前端 → Dify API
升级成:
浏览器前端 → Express BFF → Dify API
但学习时最好一步一步来,先看清楚 Dify API 怎么调用,再抽到服务端。
本篇目标
完成后页面应该具备这些能力:
1. 有一个输入框
2. 有一个发送按钮
3. 能把用户问题发送给 Dify
4. 能展示 AI 返回内容
5. 有基础 loading 状态
6. 支持多轮 conversationId
这一篇暂时不做:
流式输出
Markdown 渲染
引用来源
多会话管理
Express 代理
数据库
登录
这些后面都会一步步加。
第一步:创建 Vite React 项目
在本地创建项目:
npm create vite@latest dify-chat-demo -- --template react-ts
cd dify-chat-demo
npm install
npm run dev
启动成功后,浏览器打开:
http://localhost:5173
能看到 Vite 默认页面,说明项目创建成功。
第二步:准备 Dify 应用 API Key
进入 Dify 应用,找到:
访问 API
复制应用 API Key。
一般长这样:
app-xxxxxxxxxxxxxxxx
注意,这个 Key 不要提交到 GitHub。
Dify 的 Chat Messages API 一般是:
https://api.dify.ai/v1/chat-messages
如果你是私有部署 Dify,则换成自己的 API 地址。
第三步:配置环境变量
在项目根目录创建:
.env.local
写入:
VITE_DIFY_API_KEY=你的 Dify 应用 API Key
VITE_DIFY_API_URL=https://api.dify.ai/v1/chat-messages
注意:
这里用了 VITE_ 前缀,是因为 Vite 只有以 VITE_ 开头的环境变量才会暴露给浏览器。
但这也意味着:
这个 Key 会出现在前端构建产物里。
所以这一篇的写法只适合本地学习和快速验证。下一篇我们会把 Key 移到 Express 服务端。
确保 .gitignore 里包含:
.env
.env.local
第四步:封装 Dify API 调用
创建文件:
src/api/dify.ts
写入:
export type DifyChatResponse = {
answer: string
conversation_id: string
[key: string]: unknown
}
export type ChatResponse = {
answer: string
conversationId: string
}
const DIFY_API_KEY = import.meta.env.VITE_DIFY_API_KEY
const DIFY_API_URL =
import.meta.env.VITE_DIFY_API_URL || 'https://api.dify.ai/v1/chat-messages'
export async function sendMessageToDify(
message: string,
conversationId?: string
): Promise<ChatResponse> {
if (!DIFY_API_KEY) {
throw new Error('Missing VITE_DIFY_API_KEY')
}
const response = await fetch(DIFY_API_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${DIFY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
inputs: {},
query: message,
response_mode: 'blocking',
conversation_id: conversationId || '',
user: 'vite-demo-user',
}),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(errorText || 'Dify request failed')
}
const data = (await response.json()) as DifyChatResponse
return {
answer: data.answer,
conversationId: data.conversation_id,
}
}
这里有几个关键参数:
inputs
inputs: {}
如果 Dify 应用里定义了额外输入变量,就需要在这里传。我们当前没有,所以传空对象。
query
query: message
用户输入的问题。
response_mode
response_mode: 'blocking'
blocking 表示等 Dify 完整生成回答后一次性返回。
后面做流式输出时,会改成:
response_mode: 'streaming'
conversation_id
conversation_id: conversationId || ''
Dify 用这个字段维持多轮对话。
第一次请求传空字符串,Dify 返回一个新的 conversation_id。后续请求带上它,就能继续同一个会话。
user
user: 'vite-demo-user'
这是 Dify 用来标识终端用户的字段。学习阶段先写固定值即可。
第五步:定义消息类型
创建文件:
src/types/chat.ts
写入:
export type Role = 'user' | 'assistant'
export type Message = {
role: Role
content: string
}
消息列表里会有两类消息:
user:用户输入
assistant:AI 回答
第六步:实现最小 App 页面
修改:
src/App.tsx
写入:
import { useState } from 'react'
import { sendMessageToDify } from './api/dify'
import type { Message } from './types/chat'
import './App.css'
function App() {
const [input, setInput] = useState('')
const [messages, setMessages] = useState<Message[]>([])
const [conversationId, setConversationId] = useState<string>()
const [loading, setLoading] = useState(false)
async function handleSend() {
const text = input.trim()
if (!text || loading) return
setInput('')
setLoading(true)
setMessages(prev => [...prev, { role: 'user', content: text }])
try {
const result = await sendMessageToDify(text, conversationId)
setConversationId(result.conversationId)
setMessages(prev => [
...prev,
{
role: 'assistant',
content: result.answer,
},
])
} catch (error) {
console.error(error)
setMessages(prev => [
...prev,
{
role: 'assistant',
content:
error instanceof Error
? `请求失败:${error.message}`
: '请求失败,请稍后重试。',
},
])
} finally {
setLoading(false)
}
}
function handleClear() {
setMessages([])
setConversationId(undefined)
}
return (
<div className="app">
<h1>Frontend AI Assistant</h1>
<div className="messages">
{messages.map((message, index) => (
<div
key={index}
className={`message ${message.role === 'user' ? 'user' : 'ai'}`}
>
<strong>{message.role === 'user' ? '你' : 'AI'}:</strong>
<div>{message.content}</div>
</div>
))}
{loading && <div className="loading">AI 正在思考...</div>}
</div>
<textarea
value={input}
onChange={event => setInput(event.target.value)}
onKeyDown={event => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
handleSend()
}
}}
placeholder="请输入你的问题,按 Enter 发送,Shift + Enter 换行"
rows={4}
/>
<div className="actions">
<button onClick={handleSend} disabled={loading || !input.trim()}>
{loading ? '发送中...' : '发送'}
</button>
<button onClick={handleClear} disabled={loading}>
清空会话
</button>
</div>
</div>
)
}
export default App
第七步:加一点基础样式
修改:
src/App.css
写入:
.app {
max-width: 720px;
margin: 40px auto;
padding: 16px;
font-family:
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
}
.messages {
margin: 24px 0;
}
.message {
margin-bottom: 12px;
padding: 12px;
border-radius: 8px;
line-height: 1.6;
}
.message.user {
background: #e8f3ff;
}
.message.ai {
background: #f5f5f5;
}
.loading {
color: #666;
margin-bottom: 12px;
}
textarea {
width: 100%;
box-sizing: border-box;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
resize: vertical;
font: inherit;
}
.actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
button {
padding: 8px 14px;
border: 0;
border-radius: 6px;
cursor: pointer;
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
第八步:测试知识库问答
启动项目:
npm run dev
在页面里输入:
前端架构主要包括哪些内容?
如果 Dify 应用配置正确,应该能返回类似:
前端架构主要包括项目分层、组件设计、状态管理、权限控制、性能优化、工程化和可观测性。
再问:
大型前端项目可以分为哪几层?
理想回答:
大型前端项目可以分为 UI 层、业务逻辑层、数据请求层、基础设施层。
如果这两个问题都能回答,说明:
React 页面 → Dify Chat API → Dify 知识库 → LLM 回答
这条链路已经跑通。
第九步:验证多轮对话
Dify 每次返回时会带上:
conversation_id
我们在代码里保存到了:
const [conversationId, setConversationId] = useState<string>()
后续请求会继续带上它:
sendMessageToDify(text, conversationId)
可以这样测试:
第一句问:
前端架构主要包括哪些内容?
第二句追问:
那大型项目怎么分层?
如果 Dify 应用支持上下文,多轮对话就能继续沿用前面的会话。
现在这个版本有什么问题?
这一版虽然能跑,但问题也很明显。
1. API Key 暴露在浏览器里
我们把 Key 写在了:
VITE_DIFY_API_KEY=xxx
只要是 VITE_ 开头的变量,最终都会出现在前端构建结果里。
用户打开浏览器 DevTools,就可能看到请求头里的:
Authorization: Bearer app-xxx
这是不能用于正式项目的。
2. 回答是一次性返回的
当前使用的是:
response_mode: 'blocking'
所以用户要等模型生成完,才能看到完整回答。
体验不如 ChatGPT 那种逐字输出。
3. 不支持 Markdown 渲染
现在回答只是普通文本:
<div>{message.content}</div>
如果 AI 返回代码块、列表、表格,展示效果会比较差。
4. 没有引用来源
RAG 应用非常重要的一点是:
答案来自哪里?
当前页面还没有展示 retriever_resources。
5. 刷新页面会丢失会话
messages 和 conversationId 都保存在 React state 里,刷新页面就没了。
这些问题会在后续文章逐个解决。
为什么不要一步到位?
可能有人会觉得:既然 API Key 暴露有问题,为什么还要先写这个版本?
我的理解是:学习一个系统时,最好先让链路可见。
这个最小版本可以帮我们明确几件事:
Dify API 怎么调用
请求参数怎么传
conversation_id 怎么保存
blocking 模式是什么效果
React 页面怎么展示回答
等这些都清楚后,再抽出 Express 代理层,就会非常自然。
如果一开始就把前端、后端、流式、Markdown、多会话全部堆上去,反而不容易理解每一层的职责。
当前代码结构
这一篇结束后,项目结构大概是:
dify-chat-demo/
src/
api/
dify.ts
types/
chat.ts
App.tsx
App.css
main.tsx
.env.local
package.json
vite.config.ts
功能链路是:
App.tsx
↓
sendMessageToDify
↓
Dify Chat Messages API
↓
Dify Chatflow
↓
知识检索 + LLM
本篇总结
这一篇我们完成了前端最小版本:
Vite + React + TypeScript
调用 Dify Chat API
展示 AI 回答
保存 conversationId
支持基础 loading
支持清空会话
现在项目已经从 Dify 控制台里的 Demo,变成了我们自己的 Web 页面。
但是这个版本还不能算正式项目,因为 Dify API Key 暴露在浏览器里。
下一篇我们就来解决这个问题:
用 Express 做一个 BFF 代理层,把 Dify API Key 放到服务端。