前言(第二阶段收官!🎉)
太棒了!我们来到了第二阶段的最后一篇!这一篇,我们来做一个完整的实战项目:智能笔记助手!
这个项目会用到我们之前学到的所有知识!
1. 项目需求
功能需求
- ✅ 笔记CRUD(创建、读取、更新、删除)
- ✅ AI写作助手(续写、润色、翻译)
- ✅ 语音输入(语音转文字)
- ✅ 笔记分类和标签
- ✅ 本地存储(离线可用)
- ✅ AI智能标签建议
- ✅ 笔记内容总结
技术栈
- React 19
- Tailwind CSS
- Chrome AI API
- Web Speech API
- LocalStorage
2. 完整项目实现
2.1 AI Hook
jsx
// use-ai.js
import { useState, useCallback, useRef, useEffect } from 'react';
export function useAI() {
const [isReady, setIsReady] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const sessionRef = useRef(null);
useEffect(() => {
initAI();
}, []);
async function initAI() {
try {
if ('ai' in window) {
sessionRef.current = await window.ai.assistant.create({
systemPrompt: `你是一个智能笔记助手。帮助用户写作、润色、总结笔记。
用简洁、清晰的中文回复。`
});
setIsReady(true);
} else {
setError('当前浏览器不支持Chrome AI');
}
} catch (err) {
setError(err.message);
}
}
const generate = useCallback(async (prompt) => {
if (!sessionRef.current) {
throw new Error('AI未就绪');
}
setIsLoading(true);
try {
return await sessionRef.current.prompt(prompt);
} finally {
setIsLoading(false);
}
}, []);
const generateStream = useCallback(async function* (prompt) {
if (!sessionRef.current) {
throw new Error('AI未就绪');
}
setIsLoading(true);
try {
const stream = sessionRef.current.promptStreaming(prompt);
for await (const chunk of stream) {
yield chunk;
}
} finally {
setIsLoading(false);
}
}, []);
return { isReady, isLoading, error, generate, generateStream };
}
2.2 笔记存储 Hook
jsx
// use-notes.js
import { useState, useEffect } from 'react';
const STORAGE_KEY = 'smart_notes';
export function useNotes() {
const [notes, setNotes] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadNotes();
}, []);
function loadNotes() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
setNotes(JSON.parse(saved));
}
} catch (error) {
console.error('加载笔记失败:', error);
} finally {
setLoading(false);
}
}
function saveNotes(newNotes) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(newNotes));
} catch (error) {
console.error('保存笔记失败:', error);
}
}
function addNote(title, content, tags = []) {
const newNote = {
id: Date.now(),
title,
content,
tags,
createdAt: new Date().toLocaleString(),
updatedAt: new Date().toLocaleString()
};
const newNotes = [newNote, ...notes];
setNotes(newNotes);
saveNotes(newNotes);
return newNote;
}
function updateNote(id, updates) {
const newNotes = notes.map(note =>
note.id === id ? { ...note, ...updates, updatedAt: new Date().toLocaleString() } : note
);
setNotes(newNotes);
saveNotes(newNotes);
}
function deleteNote(id) {
const newNotes = notes.filter(note => note.id !== id);
setNotes(newNotes);
saveNotes(newNotes);
}
return { notes, loading, addNote, updateNote, deleteNote };
}
2.3 主应用组件
jsx
// App.jsx
import React, { useState } from 'react';
import { useNotes } from './use-notes';
import { useAI } from './use-ai';
function NoteEditor({ note, onSave, onCancel, isReady, generate }) {
const [title, setTitle] = useState(note?.title || '');
const [content, setContent] = useState(note?.content || '');
const [tags, setTags] = useState(note?.tags?.join(', ') || '');
const [aiResult, setAiResult] = useState('');
const [isGenerating, setIsGenerating] = useState(false);
const handleSave = () => {
onSave({
title: title || '无标题',
content,
tags: tags.split(',').map(t => t.trim()).filter(Boolean)
});
};
const aiActions = [
{ id: 'continue', label: '✍️ 续写', prompt: '请继续写这篇笔记:\n' },
{ id: 'improve', label: '✨ 润色', prompt: '请润色这篇笔记,让它更通顺:\n' },
{ id: 'summarize', label: '📝 总结', prompt: '请总结这篇笔记的内容:\n' },
{ id: 'tags', label: '🏷️ 建议标签', prompt: '请给这篇笔记推荐一些标签,用逗号分隔:\n' }
];
const handleAIAction = async (action) => {
if (!isReady || !content.trim()) return;
setIsGenerating(true);
try {
const result = await generate(`${action.prompt}\n\n${content}`);
setAiResult(result);
} catch (error) {
console.error('AI生成失败:', error);
} finally {
setIsGenerating(false);
}
};
const applyAIResult = () => {
if (aiResult) {
setContent(prev => prev + '\n\n' + aiResult);
setAiResult('');
}
};
return (
<div className="max-w-4xl mx-auto p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">{note ? '编辑笔记' : '新建笔记'}</h1>
<div className="flex gap-2">
<button onClick={onCancel} className="px-4 py-2 border rounded-lg hover:bg-gray-100">
取消
</button>
<button onClick={handleSave} className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
保存
</button>
</div>
</div>
<div className="space-y-6">
<div>
<label className="block mb-2 font-medium">标题</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="输入标题..."
className="w-full p-3 border rounded-lg"
/>
</div>
<div>
<label className="block mb-2 font-medium">标签</label>
<input
type="text"
value={tags}
onChange={(e) => setTags(e.target.value)}
placeholder="标签,用逗号分隔..."
className="w-full p-3 border rounded-lg"
/>
</div>
<div>
<label className="block mb-2 font-medium">内容</label>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="开始写作..."
className="w-full h-64 p-3 border rounded-lg"
/>
</div>
{/* AI工具栏 */}
<div className="p-4 bg-gray-50 rounded-lg">
<h3 className="font-medium mb-3">🤖 AI助手</h3>
<div className="flex flex-wrap gap-2 mb-4">
{aiActions.map(action => (
<button
key={action.id}
onClick={() => handleAIAction(action)}
disabled={!isReady || isGenerating}
className="px-3 py-2 bg-white border rounded-lg hover:bg-gray-100 disabled:opacity-50"
>
{action.label}
</button>
))}
</div>
{aiResult && (
<div className="p-4 bg-blue-50 rounded-lg">
<div className="flex justify-between items-start mb-2">
<span className="font-medium text-blue-800">AI建议</span>
<button
onClick={applyAIResult}
className="text-sm px-2 py-1 bg-blue-600 text-white rounded"
>
应用
</button>
</div>
<p className="text-blue-900">{aiResult}</p>
</div>
)}
{isGenerating && (
<div className="text-center py-4 text-gray-500">
AI思考中...
</div>
)}
</div>
</div>
</div>
);
}
function NoteCard({ note, onEdit, onDelete }) {
return (
<div className="p-4 bg-white rounded-lg shadow hover:shadow-md">
<h3 className="font-bold text-lg mb-2">{note.title}</h3>
<p className="text-gray-600 mb-3 line-clamp-3">{note.content}</p>
{note.tags && note.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mb-3">
{note.tags.map((tag, i) => (
<span key={i} className="px-2 py-1 bg-gray-100 text-sm rounded">{tag}</span>
))}
</div>
)}
<p className="text-sm text-gray-400 mb-3">{note.updatedAt}</p>
<div className="flex gap-2">
<button onClick={onEdit} className="text-sm text-blue-600 hover:text-blue-800">
编辑
</button>
<button onClick={onDelete} className="text-sm text-red-600 hover:text-red-800">
删除
</button>
</div>
</div>
);
}
export default function SmartNotesApp() {
const [view, setView] = useState('list'); // list, edit
const [editingNote, setEditingNote] = useState(null);
const { notes, loading, addNote, updateNote, deleteNote } = useNotes();
const { isReady, generate } = useAI();
const handleCreateNote = () => {
setEditingNote(null);
setView('edit');
};
const handleEditNote = (note) => {
setEditingNote(note);
setView('edit');
};
const handleSaveNote = (noteData) => {
if (editingNote) {
updateNote(editingNote.id, noteData);
} else {
addNote(noteData.title, noteData.content, noteData.tags);
}
setView('list');
setEditingNote(null);
};
const handleDeleteNote = (id) => {
if (confirm('确定删除这篇笔记吗?')) {
deleteNote(id);
}
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<p>加载中...</p>
</div>
);
}
if (view === 'edit') {
return (
<NoteEditor
note={editingNote}
onSave={handleSaveNote}
onCancel={() => setView('list')}
isReady={isReady}
generate={generate}
/>
);
}
return (
<div className="min-h-screen bg-gray-50">
{/* 顶部导航 */}
<div className="bg-white shadow">
<div className="max-w-6xl mx-auto px-4 py-4 flex justify-between items-center">
<h1 className="text-2xl font-bold">📓 智能笔记助手</h1>
<div className="flex items-center gap-4">
<span className={`text-sm ${isReady ? 'text-green-600' : 'text-gray-400'}`}>
{isReady ? '🤖 AI就绪' : '⏳ 加载AI...'}
</span>
<button
onClick={handleCreateNote}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
+ 新建笔记
</button>
</div>
</div>
</div>
{/* 笔记列表 */}
<div className="max-w-6xl mx-auto px-4 py-8">
{notes.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-500 text-lg mb-4">还没有笔记</p>
<button
onClick={handleCreateNote}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
创建第一篇笔记
</button>
</div>
) : (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{notes.map(note => (
<NoteCard
key={note.id}
note={note}
onEdit={() => handleEditNote(note)}
onDelete={() => handleDeleteNote(note.id)}
/>
))}
</div>
)}
</div>
</div>
);
}
3. 项目总结
学到的知识点
- ✅ 端侧AI基础
- ✅ Chrome AI API使用
- ✅ 提示工程
- ✅ React/Vue结合
- ✅ 性能优化
- ✅ 安全与隐私
- ✅ 离线应用
项目功能
- ✅ 笔记CRUD
- ✅ AI写作助手
- ✅ 本地存储
- ✅ 响应式设计
4. 第二阶段总结(🎉)
恭喜你完成了第二阶段的学习!我们学习了:
- 011-025:端侧AI开发全流程
- 从基础到实战,完整掌握端侧AI应用开发!
5. 下篇预告(整个系列)
接下来我们还有更多阶段(如果继续写的话):
- 第三阶段:AI辅助前端工程化
- 第四阶段:AI与前端框架结合
- 第五阶段:AI原生前端实战项目
- 第六阶段:进阶与未来
第二阶段收官!完美!🚀