AI原生前端开发(025):端侧AI实战项目:智能笔记助手

前言(第二阶段收官!🎉)

太棒了!我们来到了第二阶段的最后一篇!这一篇,我们来做一个完整的实战项目:智能笔记助手!

这个项目会用到我们之前学到的所有知识!


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. 项目总结

学到的知识点

  1. ✅ 端侧AI基础
  2. ✅ Chrome AI API使用
  3. ✅ 提示工程
  4. ✅ React/Vue结合
  5. ✅ 性能优化
  6. ✅ 安全与隐私
  7. ✅ 离线应用

项目功能

  1. ✅ 笔记CRUD
  2. ✅ AI写作助手
  3. ✅ 本地存储
  4. ✅ 响应式设计

4. 第二阶段总结(🎉)

恭喜你完成了第二阶段的学习!我们学习了:

  • 011-025:端侧AI开发全流程
  • 从基础到实战,完整掌握端侧AI应用开发!

5. 下篇预告(整个系列)

接下来我们还有更多阶段(如果继续写的话):

  • 第三阶段:AI辅助前端工程化
  • 第四阶段:AI与前端框架结合
  • 第五阶段:AI原生前端实战项目
  • 第六阶段:进阶与未来

第二阶段收官!完美!🚀

相关推荐
成旭先生7 小时前
车辆、能源与地理数据 API 实战:把定价、油价、行政区划串成一条链路
人工智能·windows·能源·api集成·油价查询·车辆定价·地址清洗
北方的银狐-Zero7 小时前
OntoL 7.0|面向工业多智能体的语义本体套件,让异构系统真正 “看懂彼此”——OntoL的本体产品矩阵
人工智能·mysql·sqlite·1024程序员节·本体论·本体产品
stormzhangV7 小时前
史诗级封号收入
人工智能·aigc
龍德明宇7 小时前
AI 和办事系统,是同一台机器的两张照片-龍德明宇
大数据·人工智能·负主体性
fl1768317 小时前
工业玻璃不同形状类别玻璃检测数据集VOC+YOLO格式1012张4类别
人工智能·yolo·机器学习
Rocky Ding*7 小时前
【三年面试五年模拟】2026-10-25 华为大模型算法岗一面面经:13道题解析与算法实现
论文阅读·人工智能·深度学习·机器学习·大模型·aigc·ai-native
马剑威(威哥爱编程)7 小时前
【HarmonyOS 7 小艺智能体实战】07:手写一个 Skill(下)——ArkTS、意图框架与工程结构
人工智能·华为·harmonyos
海鸥-w7 小时前
数据库冗余字段设计笔记
java·数据库·笔记·oracle
Old Uncle Tom7 小时前
LLM 替代 SAST?Claude Code 安全审查工具的设计取舍与工程启示
人工智能·安全·软件工程
叁两7 小时前
别再手动刷行业信息了,你可能需要一个"行业研究员"
人工智能