当你还在为 “如何让前端页面看一张图片” 抓秃头发时,有人已经用几行 JS 代码让网页和 AI 聊了起来 —— 拍下一只猫,前端直接甩给大模型一句 “这图里的东西用 A1 级单词怎么说?”,3 秒后就拿到 “cat” 和配套例句。
这就是当下最火的 “智能前端”:它不是让你死磕算法,而是教你把大模型当 “超级实习生”,把复杂的图片识别、文本生成、逻辑判断交给 AI,前端只需要专注 “指挥 AI 干活” 和 “把结果变好看”。
这篇文章,咱们就从这个 “拍照记单词” 的实战项目出发,看看智能前端到底怎么玩:从用 pnpm 提速开发,到用 React 的单向数据流管理 AI 返回的数据,再到怎么写一个让 AI “听话” 的 prompt,每一步都带着 “智能味儿”,新手也能跟着敲出一个会 “思考” 的 APP。
核心技术栈与工具选型
pnpm 包管理的正确打开方式
使用pnpm提升安装效率(比npm快3倍)
它像个 “共享书架”,把 react 这类常用包存在一个公共文件夹里,不同项目要用时直接 “借”(创建链接),之前装过的包,第二次 “零安装”。
用 vite 初始化的项目,自带完整的工程化配置(热更新、打包优化等),咱们只需要关注业务逻辑。
# 安装 pnpm(全局只需要一次)
npm install -g pnpm
# 用 pnpm 创建项目(代替 npm init)
pnpm create vite@latest ai-word-app --template react
# 安装依赖(比 npm 快 3 倍+)
cd ai-word-app
pnpm install
这一步就能省出大量时间,尤其在多项目开发时,谁用谁知道香~
多模态AI服务选型
| 服务类型 | 推荐方案 | 特点 |
|---|---|---|
| 图像理解 | 月之暗面Kimi | 支持多图分析,长文本推理 |
| 语音合成 | 火山引擎TTS | 高保真发音,低延迟 |
项目骨架:用 React 思想搭个 “智能背词” 框架
核心需求:拍照→AI 识别单词→生成例句→发音
我们要做的 app 核心流程很简单:
- 用户拍一张照片(比如拍一只狗);
- 前端把照片传给 月之暗面(Moonshot)多模态大模型(能“看懂”图片的AI),精准识别核心单词(比如"dog");
- 月之暗面同步生成场景化例句与单词解析;
- 调用火山引擎TTS接口生成发音,实现「看+听」沉浸式学习。
我们首先配置环境变量,API 密钥(如 VITE_KIMI_API_KEY)是调用 AI 接口的 “钥匙”,绝对不能硬编码在代码里(会被别人偷走)。正确做法是用 .env.local 存。
月之暗面:platform.moonshot.cn/docs/introd…
选择豆包语音
有很多语音模型供选择
从中获取API 并填入:
# Kimi API 密钥
VITE_KIMI_API_KEY=你的kimi_api_key
# 字节跳动 TTS 配置
VITE_AUDIO_ACCESS_TOKEN=你的access_token
VITE_AUDIO_APP_ID=你的app_id
VITE_AUDIO_CLUSTER_ID=你的cluster_id
VITE_AUDIO_VOICE_NAME=你的voice_name
在代码中用 import.meta.env.VITE_XXX 读取,既安全又方便切换环境(开发 / 生产用不同密钥)。
核心组件设计:前端的 “智能分工” 哲学
智能前端的核心不是堆代码,而是让每个组件各司其职:父组件管 “思考”(数据和 AI 交互),子组件管 “展示”(UI 和用户操作),配合 React 的单向数据流,数据像 “指令” 一样从父到子有序流动。
核心文件夹:
├── src/
│ ├── components/ # 组件目录(前端的“功能积木”)
│ │ └── PictureCard/ # 图片上传组件(拍照核心)
│ ├── libs/ # 工具库(AI 调用、音频处理等)
│ │ └── audio.js # TTS 语音合成工具
│ ├── App.jsx # 根组件(统筹全局的“总指挥”)
│ └── App.css # 全局样式
└── .env.local # 环境变量(存 API 密钥,安全第一)
这种结构的好处:组件复用性高,工具函数集中管理,后期维护像找东西一样方便。
根组件 App.jsx:AI 交互的 “总指挥”
App 组件是整个应用的 “大脑”,负责三件事:
- 存数据(单词、例句、音频等状态);
- 调 AI 接口(让多模态模型识别图片、TTS 生成发音);
- 把数据传给子组件(PictureCard)。
import { useState } from 'react'
import './App.css'
import PictureCard from './components/PictureCard'; // 导入图片上传子组件
import { generateAudio } from './lib/audio'; // 导入TTS语音合成工具
function App() {
// 1. 用 userPrompt 告诉 AI 该做什么(智能交互的核心指令)
const userPrompt = `分析图片内容,找出最能描述图片的一个英文单词,尽量选择更简单的A1~A2的词汇。
返回JSON数据:
{
"image_discription": "图片描述",
"representative_word": "图片代表的英文单词",
"example_sentence": "结合英文单词和图片描述,给出一个简单的例句",
"explaination": "结合图片解释英文单词,段落以Look at...开头,将段落分句,每一句单独一行,解释的最后给一个日常生活有关的问句",
"explaination_replys": ["根据explaination给出的回复1", "根据explaination给出的回复2"]
}`;
// 2. 用 useState 管理响应式状态(数据变了,页面自动更)
const [word, setWord] = useState('请上传图片'); // 识别出的单词(如“cat”)
const [sentence, setSentence] = useState(''); // AI生成的例句(如“I see a cat”)
const [explainations, setExplainations] = useState([]); // 单词解释(拆分成数组方便渲染)
const [expReply, setExpReply] = useState([]); // 互动回复(如“你喜欢猫吗?”)
const [audio, setAudio] = useState(''); // 音频地址(用于播放发音)
const [detailExpand, setDetailExpand] = useState(false); // 控制详情展开/折叠的状态
const [imgpreview, setImgPreview] = useState('https://res.bearbobo.com/resource/upload/W44yyxvl/upload-ih56twxirei.png'); // 图片预览地址
// 3. 核心函数:上传图片并调用AI处理
const uploadImg = async (imageData) => {
setImgPreview(imageData); // 先更新预览图(提升用户体验)
const endpoint = 'https://api.moonshot.cn/v1/chat/completions'; // 多模态模型接口地址
const headers = {
'Content-Type': 'application/json',
// 从环境变量拿API密钥(安全存储,避免硬编码)
Authorization: `Bearer ${import.meta.env.VITE_KIMI_API_KEY}`
};
setWord('分析中...'); // 显示加载状态(让用户知道“正在处理”)
// 调用多模态AI接口(核心:让AI“看懂”图片并返回结果)
const response = await fetch(endpoint, {
method: 'POST',
headers: headers,
body: JSON.stringify({
model: 'moonshot-v1-8k-vision-preview', // 支持图片识别的大模型
messages: [
{
role: 'user',
content: [
{
type: "image_url",
image_url: { "url": imageData, } // 传入图片的base64地址
},
{
type: "text", text: userPrompt // 传入指令(告诉AI要返回什么)
}]
}],
stream: false // 不使用流式返回,一次性获取结果
})
});
// 处理AI返回的结果
const data = await response.json();
const replyData = JSON.parse(data.choices[0].message.content); // 解析AI返回的JSON
// 更新状态(数据驱动视图,React自动重新渲染页面)
setWord(replyData.representative_word); // 存单词
setSentence(replyData.example_sentence); // 存例句
setExplainations(replyData.explaination.split('\n')); // 存解释(按换行拆分)
setExpReply(replyData.explaination_replys); // 存互动回复
// 调用TTS生成音频(让单词“会说话”)
const audioUrl = await generateAudio(replyData.example_sentence);
setAudio(audioUrl); // 存音频地址
};
return (
<div className="container">
{/* 把数据和方法传给子组件(单向数据流:父→子) */}
<PictureCard
audio={audio} // 音频地址(子组件用来播放)
word={word} // 单词(子组件用来显示)
uploadImg={uploadImg} // 上传图片的方法(子组件触发)
/>
{/* 显示例句和详情区域 */}
<div className="output">
<div>{sentence}</div> {/* 显示例句 */}
{/* 详情展开/折叠功能 */}
<div className="details">
<button onClick={() => setDetailExpand(!detailExpand)}>Talk about it</button>
{
detailExpand ? (
<div className="expand">
<img src={imgpreview} alt="preview"/> {/* 显示图片预览 */}
{/* 渲染解释内容 */}
{
explainations.map((explain, index) => (
<div key={index} className="explaination">
{explain}
</div>
))
}
{/* 渲染互动回复 */}
{
expReply.map((reply, index) => (
<div key={index} className="explaination_reply">
{reply}
</div>
))
}
</div>
) : (
<div className="fold"/> {/* 折叠状态显示的占位区域 */}
)
}
</div>
</div>
</div>
);
}
export default App;
智能前端亮点:这里的 userPrompt 是 “指挥 AI 干活” 的关键 —— 我们明确告诉 AI 要返回什么格式(JSON)、什么难度(A1~A2 词汇),让 AI 成为 “听话的实习生”,而不是返回一堆无用信息。
子组件PictureCard:图片上传组件
PictureCard只负责两件事:
- 让用户上传图片(拍照功能);
- 显示单词和播放发音(把父组件传来的数据展示出来)。
import { useState } from 'react';
import './style.css';
const PictureCard = (props) => {
// 接收父组件传来的数据和方法(props 就像“快递包裹”)
const {
word,
audio,
uploadImg
} = props
// 子组件自己的状态(私有数据,存储图片预览地址)
const [imgPreview, setImgPreview] = useState('https://res.bearbobo.com/resource/upload/W44yyxvl/upload-ih56twxirei.png')
// 处理图片上传(用户选完图片后触发)
const uploadImgData=(e)=>{
const file = (e.target).files?.[0]; // 获取用户选择的图片(可选链操作符避免报错)
if (!file) { return; } // 没选图片就直接返回
// 用 Promise 封装 FileReader,方便后续处理
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file); // 把图片转成 base64 格式
reader.onload = () => {
const data = reader.result; // 得到 base64 字符串(如“data:image/png;base64,...”)
setImgPreview(data); // 更新预览图(用户能立即看到选中的图片)
uploadImg(data); // 调用父组件的方法,把图片传给 AI 处理
resolve(data); // 成功时返回 base64 数据
}
reader.onerror = (error) => { reject(error); }; // 失败时抛出错误
})
}
// 播放音频(点击喇叭图标时触发)
const playAudio = () => {
const audioEle = new Audio(audio); // 创建音频对象
audioEle.play(); // 播放发音(如例句的朗读)
}
return (
<div className='card'>
{/* 隐藏原生文件输入框,用 label 美化成图片样式 */}
<input
type="file"
id="selectImage"
accept='.jpg,.jpeg,.png,.gif' // 只接受图片格式
onChange={uploadImgData} // 选完图片后触发上传逻辑
/>
{/* 点击图片区域触发文件选择(用户友好的交互设计) */}
<label htmlFor='selectImage' className='upload'>
<img src={imgPreview} alt='preview'/> // 显示预览图(默认图或用户选中的图)
</label>
<div className='word'>{word}</div> // 显示父组件传来的单词(如“cat”)
{/* 有音频时才显示播放按钮(条件渲染,避免空状态报错) */}
{audio && (
<div className="playAudio" onClick={playAudio}>
<img width="20px" src="https://res.bearbobo.com/resource/upload/Omq2HFs8/playA-3iob5qyckpa.png" alt="播放发音" />
</div>
)}
</div>
)
}
export default PictureCard;
audio.js TTS 语音合成:让单词 “会说话”
用字节跳动的 TTS 接口把文字转语音,前端处理音频的过程用到了 HTML5 的 Blob 技术
// 利用字节的tts 文字转语音
const getAudioUrl = (base64Data) => {
// 创建一个数组来存储字节数据
var byteArrays = [];
// 使用atob()将Base64编码的字符串解码为原始二进制字符串
// atob: ASCII to Binary
var byteCharacters = atob(base64Data);
// 遍历解码后的二进制字符串的每个字符
for (var offset = 0; offset < byteCharacters.length; offset++) {
// 将每个字符转换为其ASCII码值(0-255之间的数字)
var byteArray = byteCharacters.charCodeAt(offset);
// 将ASCII码值添加到字节数组中
byteArrays.push(byteArray);
}
// 创建一个Blob对象
// new Uint8Array(byteArrays)将普通数组转换为8位无符号整数数组
// { type: 'audio/mp3' } 指定Blob的MIME类型为MP3音频
var blob = new Blob([new Uint8Array(byteArrays)], { type: 'audio/mp3' });
// 使用URL.createObjectURL创建一个临时的URL
// 这个URL可以用于<audio>标签的src属性
// 这个URL在当前页面/会话有效,页面关闭后会自动释放
// 创建一个临时 URL 供音频播放
return URL.createObjectURL(blob);
}
export const generateAudio = async (text) => {
// 从环境变量拿密钥(安全存储,绝不硬编码)
const token = import.meta.env.VITE_AUDIO_ACCESS_TOKEN;
const appId = import.meta.env.VITE_AUDIO_APP_ID;
const clusterId = import.meta.env.VITE_AUDIO_CLUSTER_ID;
const voiceName = import.meta.env.VITE_AUDIO_VOICE_NAME;
const endpoint = '/tts/api/v1/tts';
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer;${token}`,
};
// 请求体
const payload = {
app: {
appid: appId,
token,
cluster: clusterId,
},
user: {
uid: 'bearbobo',
},
audio: {
voice_type: voiceName,
encoding: 'ogg_opus',
compression_rate: 1,
rate: 24000,
speed_ratio: 1.0,
volume_ratio: 1.0,
pitch_ratio: 1.0,
emotion: 'happy',
// language: 'cn',
},
request: {
reqid: Math.random().toString(36).substring(7),
text,
text_type: 'plain',
operation: 'query',
silence_duration: '125',
with_frontend: '1',
frontend_type: 'unitTson',
pure_english_opt: '1',
},
};
const res = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify(payload),
})
const data = await res.json();
const url = getAudioUrl(data.data);
return url; // 返回音频URL,用于播放或下载
}
可以从官方文档中获取请求体
css reset
初始化,消除不同浏览器的默认样式差异
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
/* margin: 0 auto;
padding: 2rem; */
text-align: center;
color: white;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: start;
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
font-size: .85rem;
background: linear-gradient(180deg, rgb(127, 224, 148) 0%, rgba(8, 92, 161, 0.783) 100%);
}
#selectImage {
display: none;
}
.input {
width: 200px;
}
.output {
margin-top: 20px;
/* min-height: 300px; */
width: 80%;
text-align: center;
font-weight: bold;
}
.preview img {
max-width: 100%;
}
button {
padding: 0 10px;
margin-left: 6px;
}
.details {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
.details button {
background-color: black;
color: white;
width: 160px;
height: 32px;
border-radius: 8px 8px 0 0;
border: none;
font-size: 12px;
font-weight: bold;
cursor: pointer;
}
.details .fold {
width: 200px;
height: 30px;
background-color: white;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.details .expand {
width: 200px;
height: 88vh;
background-color: white;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.expand img {
width: 60%;
margin-top: 20px;
border-radius: 6px;
}
.expand .explaination {
color: black;
font-weight: normal;
}
.expand .explaination p {
margin: 0 10px 10px 10px;
}
.expand .reply {
color: black;
font-weight: normal;
margin-top: 20px;
}
.expand .reply p {
padding: 4px 10px;
margin: 0 10px 10px 10px;
border-radius: 6px;
border: solid 1px grey;
}
#selecteImage {
display: none;
}
.card {
border-radius: 8px;
padding: 20px;
margin-top: 40px;
height: 280px;
box-shadow: rgb(63, 38, 21) 0 3px 0px 0;
background-color: rgb(105, 78, 62);
box-sizing: border-box;
}
.upload {
width: 160px;
height: 160px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.upload img {
width: 100%;
height: 100%;
object-fit: contain;
}
.word {
margin-top: 20px;
font-size: 16px;
color: rgb(255, 255, 255);
}
.playAudio {
margin-top: 16px;
}
.playAudio img {
cursor: pointer;
}
以上就是该项目的所有代码,可直接复制运行,记得换成自己的 API 密钥哦~
这个项目看起来涉及 AI、React、组件化,但拆开后每一步都很简单:本质是 “前端调接口 + 数据展示”,只是接口对面站着 AI 而已。
现在的智能前端开发,早就不是 “写静态页面” 了,而是用代码让产品 “变聪明”。下次背单词时,掏出自己写的 app 拍照识词,这种成就感,比死记硬背爽多了~
下次想做 AI 小应用,直接拿这篇当模板!