第十二篇:【React + AI】深度实践:从 LLM 集成到智能 UI 构建

人工智能与前端的完美融合:为你的 React 应用注入 AI 能力

各位前端开发者,随着大语言模型(LLM)和 AI 技术的迅猛发展,将人工智能能力整合到前端应用已成为热门趋势。本文将带你深入探索如何在 React 应用中集成 AI 能力,从基础调用到复杂交互,打造真正智能化的用户体验。

1. AI 时代的前端开发:挑战与机遇

AI 技术给前端开发带来了全新的可能性:

  • 内容生成:自动生成文本、图像和代码
  • 交互增强:智能搜索、聊天机器人和个性化推荐
  • 用户体验优化:通过 AI 预测用户需求和行为
  • 开发效率提升:自动化编码、调试和测试

然而,这些机遇也伴随着挑战:

  • 性能与延迟:如何在前端高效运行 AI 模型
  • 用户隐私:敏感数据处理与模型推理
  • 技术整合:将 AI 服务与现有前端架构融合
  • 成本控制:平衡 AI 功能与运营成本

下面,我们将分享一系列实用技术和最佳实践,帮助你有效地在 React 应用中整合 AI 能力。

2. AI 集成架构设计:前端 AI 集成模式

markdown 复制代码
┌─────────────────────────────────────────────────┐
│                  前端应用                         │
│                                                 │
│  ┌─────────────┐    ┌────────────┐   ┌────────┐ │
│  │  React UI   │◄───┤  状态管理   │◄──┤ AI中间层│ │
│  └─────────────┘    └────────────┘   └────────┘ │
│         │                                  ▲     │
└─────────┼──────────────────────────────────┼─────┘
          │                                  │
          ▼                                  │
    ┌─────────────┐                   ┌───────────┐
    │  用户交互    │                   │ AI服务/API │
    └─────────────┘                   └───────────┘
                                           │
                                           ▼
                                     ┌───────────┐
                                     │ 模型服务器 │
                                     └───────────┘

三种主要集成模式

  1. 云端 API 调用模式

    • AI 模型运行在云端服务器
    • 前端通过 API 请求获取结果
    • 优势:无需前端加载模型,响应速度快
    • 劣势:依赖网络连接,可能有 API 调用成本
  2. 前端模型运行模式

    • 模型直接在浏览器中运行
    • 使用 WebGL、WebAssembly 等技术
    • 优势:保护隐私,无需网络请求
    • 劣势:初始加载较慢,受限于浏览器性能
  3. 混合模式

    • 轻量模型在前端运行
    • 复杂任务发送到云端 API
    • 优势:平衡性能与功能
    • 劣势:架构复杂度增加

3. 云端 LLM 服务集成:基于 OpenAI API 构建智能问答系统

以下是一个完整的 AI 聊天组件实现,采用 React 和 OpenAI API:

tsx 复制代码
// src/components/AIChatAssistant.tsx
import React, { useState, useRef, useEffect } from "react";
import { useAuth } from "../hooks/useAuth";
import { useLocalStorage } from "../hooks/useLocalStorage";

interface Message {
  id: string;
  role: "user" | "assistant" | "system";
  content: string;
  timestamp: number;
}

interface ChatHistoryProps {
  messages: Message[];
  isLoading: boolean;
}

// 消息历史组件
const ChatHistory: React.FC<ChatHistoryProps> = ({ messages, isLoading }) => {
  const messagesEndRef = useRef<HTMLDivElement>(null);

  // 自动滚动到最新消息
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages]);

  // 格式化时间
  const formatTime = (timestamp: number) => {
    return new Date(timestamp).toLocaleTimeString([], {
      hour: "2-digit",
      minute: "2-digit",
    });
  };

  return (
    <div className="chat-history">
      {messages.map(
        (message) =>
          message.role !== "system" && (
            <div
              key={message.id}
              className={`message ${
                message.role === "user" ? "user-message" : "assistant-message"
              }`}
            >
              <div className="message-content">
                <ReactMarkdown>{message.content}</ReactMarkdown>
              </div>
              <div className="message-time">
                {formatTime(message.timestamp)}
              </div>
            </div>
          )
      )}

      {isLoading && (
        <div className="message assistant-message">
          <div className="message-content">
            <div className="typing-indicator">
              <span></span>
              <span></span>
              <span></span>
            </div>
          </div>
        </div>
      )}

      <div ref={messagesEndRef} />
    </div>
  );
};

// 输入组件
const ChatInput: React.FC<{
  onSendMessage: (message: string) => void;
  isLoading: boolean;
}> = ({ onSendMessage, isLoading }) => {
  const [input, setInput] = useState("");
  const textareaRef = useRef<HTMLTextAreaElement>(null);

  // 自动调整文本区高度
  useEffect(() => {
    if (textareaRef.current) {
      textareaRef.current.style.height = "auto";
      textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
    }
  }, [input]);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (input.trim() && !isLoading) {
      onSendMessage(input);
      setInput("");
    }
  };

  // 处理快捷键
  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      handleSubmit(e);
    }
  };

  return (
    <form className="chat-input-form" onSubmit={handleSubmit}>
      <textarea
        ref={textareaRef}
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyDown={handleKeyDown}
        placeholder="输入问题..."
        rows={1}
        disabled={isLoading}
      />
      <button
        type="submit"
        className="send-button"
        disabled={isLoading || !input.trim()}
      >
        <svg
          viewBox="0 0 24 24"
          width="24"
          height="24"
          stroke="currentColor"
          strokeWidth="2"
          fill="none"
        >
          <path d="M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z" />
        </svg>
      </button>
    </form>
  );
};

// 主AI聊天组件
const AIChatAssistant: React.FC = () => {
  const { user } = useAuth();
  const [messages, setMessages] = useLocalStorage<Message[]>(
    `chat-history-${user?.id}`,
    [
      {
        id: "system-1",
        role: "system",
        content: "你是一个友好的AI助手,帮助用户解答前端开发问题。",
        timestamp: Date.now(),
      },
    ]
  );
  const [isLoading, setIsLoading] = useState(false);
  const abortControllerRef = useRef<AbortController | null>(null);

  // 发送消息
  const handleSendMessage = async (content: string) => {
    if (isLoading) return;

    // 创建用户消息
    const userMessage: Message = {
      id: `user-${Date.now()}`,
      role: "user",
      content,
      timestamp: Date.now(),
    };

    // 更新消息列表,添加用户消息
    setMessages((prev) => [...prev, userMessage]);

    // 开始加载状态
    setIsLoading(true);

    // 创建一个新的AbortController实例,用于取消请求
    abortControllerRef.current = new AbortController();

    try {
      // 准备API请求的消息格式
      const apiMessages = messages.map(({ role, content }) => ({
        role,
        content,
      }));

      apiMessages.push({
        role: "user",
        content,
      });

      // 发送API请求
      const response = await fetch("/api/chat", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          messages: apiMessages,
          user_id: user?.id,
          max_tokens: 1000,
          temperature: 0.7,
          stream: true,
        }),
        signal: abortControllerRef.current.signal,
      });

      if (!response.ok) {
        throw new Error("API请求失败");
      }

      if (!response.body) {
        throw new Error("响应体为空");
      }

      // 处理流式响应
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let assistantMessage = "";

      // 创建临时消息ID
      const assistantMessageId = `assistant-${Date.now()}`;

      // 添加空的助手消息
      setMessages((prev) => [
        ...prev,
        {
          id: assistantMessageId,
          role: "assistant",
          content: "",
          timestamp: Date.now(),
        },
      ]);

      // 处理流式数据
      while (true) {
        const { done, value } = await reader.read();

        if (done) {
          break;
        }

        // 解码响应数据
        const chunk = decoder.decode(value, { stream: true });

        // 将块添加到助手消息中
        assistantMessage += chunk;

        // 更新助手消息
        setMessages((prev) =>
          prev.map((msg) =>
            msg.id === assistantMessageId
              ? { ...msg, content: assistantMessage }
              : msg
          )
        );
      }
    } catch (error) {
      if (error.name === "AbortError") {
        console.log("请求已取消");
      } else {
        console.error("API请求错误:", error);

        // 添加错误消息
        setMessages((prev) => [
          ...prev,
          {
            id: `assistant-error-${Date.now()}`,
            role: "assistant",
            content: "抱歉,处理您的请求时出错了。请稍后再试。",
            timestamp: Date.now(),
          },
        ]);
      }
    } finally {
      setIsLoading(false);
      abortControllerRef.current = null;
    }
  };

  // 取消正在进行的请求
  const handleCancel = () => {
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
      setIsLoading(false);
    }
  };

  // 清空聊天记录
  const handleClearChat = () => {
    if (window.confirm("确定要清空聊天记录吗?")) {
      setMessages([
        {
          id: "system-1",
          role: "system",
          content: "你是一个友好的AI助手,帮助用户解答前端开发问题。",
          timestamp: Date.now(),
        },
      ]);
    }
  };

  return (
    <div className="ai-chat-container">
      <div className="chat-header">
        <h2>AI助手</h2>
        <div className="chat-actions">
          {isLoading && (
            <button className="cancel-button" onClick={handleCancel}>
              停止生成
            </button>
          )}
          <button className="clear-button" onClick={handleClearChat}>
            清空对话
          </button>
        </div>
      </div>

      <ChatHistory messages={messages} isLoading={isLoading} />
      <ChatInput onSendMessage={handleSendMessage} isLoading={isLoading} />
    </div>
  );
};

export default AIChatAssistant;

API 接口实现:

typescript 复制代码
// pages/api/chat.ts
import { NextApiRequest, NextApiResponse } from "next";
import OpenAI from "openai";

// 初始化OpenAI客户端
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

// 处理请求
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== "POST") {
    return res.status(405).json({ error: "只支持POST请求" });
  }

  try {
    const {
      messages,
      user_id,
      max_tokens = 1000,
      temperature = 0.7,
      stream = false,
    } = req.body;

    // 验证消息格式
    if (!Array.isArray(messages) || !messages.length) {
      return res.status(400).json({ error: "无效的messages参数" });
    }

    // 设置头部,支持流式响应
    if (stream) {
      res.writeHead(200, {
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache, no-transform",
        Connection: "keep-alive",
      });
    }

    // 创建聊天完成请求
    const completion = await openai.chat.completions.create({
      model: "gpt-4-1106-preview",
      messages,
      max_tokens,
      temperature,
      stream,
      user: user_id,
    });

    // 流式响应
    if (stream) {
      for await (const chunk of completion as any) {
        const content = chunk.choices[0]?.delta?.content || "";
        if (content) {
          res.write(content);
        }
      }
      res.end();
    }
    // 普通响应
    else {
      res.status(200).json(completion);
    }
  } catch (error) {
    console.error("API错误:", error);
    // 如果连接已关闭,不尝试发送响应
    if (!res.writableEnded) {
      res.status(500).json({ error: "处理请求时出错" });
    }
  }
}

4. 前端模型集成:浏览器中的 AI 能力

使用 TensorFlow.js 实现浏览器内图像识别:

tsx 复制代码
// src/components/ImageClassifier.tsx
import React, { useState, useRef, useEffect } from "react";
import * as tf from "@tensorflow/tfjs";
import { load } from "@tensorflow-models/mobilenet";

const ImageClassifier: React.FC = () => {
  const [model, setModel] = useState<any>(null);
  const [isModelLoading, setIsModelLoading] = useState(true);
  const [predictions, setPredictions] = useState<
    Array<{
      className: string;
      probability: number;
    }>
  >([]);
  const [imageURL, setImageURL] = useState<string | null>(null);
  const [isAnalyzing, setIsAnalyzing] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fileInputRef = useRef<HTMLInputElement>(null);
  const imageRef = useRef<HTMLImageElement>(null);

  // 加载模型
  useEffect(() => {
    const loadModel = async () => {
      try {
        setIsModelLoading(true);

        // 设置加载进度回调
        tf.loadLayersModel.progressBar = {
          start: () => console.log("模型加载开始"),
          update: (fraction: number) =>
            console.log(`加载进度: ${(fraction * 100).toFixed(1)}%`),
          end: () => console.log("模型加载完成"),
        };

        // 加载MobileNet模型
        const mobilenet = await load({
          version: 2,
          alpha: 1.0,
        });

        setModel(mobilenet);
        console.log("MobileNet模型加载成功");
      } catch (err) {
        console.error("模型加载失败:", err);
        setError("模型加载失败,请刷新页面重试");
      } finally {
        setIsModelLoading(false);
      }
    };

    loadModel();

    // 组件卸载时清理
    return () => {
      if (model) {
        console.log("清理模型资源");
      }
    };
  }, []);

  // 处理文件选择
  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    // 重置状态
    setPredictions([]);
    setError(null);

    // 检查文件类型
    if (!file.type.match("image.*")) {
      setError("请选择图片文件");
      return;
    }

    // 创建预览URL
    const reader = new FileReader();
    reader.onload = (e) => {
      setImageURL(e.target?.result as string);
    };
    reader.readAsDataURL(file);
  };

  // 分析图像
  const analyzeImage = async () => {
    if (!model || !imageRef.current || !imageURL) {
      return;
    }

    try {
      setIsAnalyzing(true);
      setError(null);

      // 使用模型进行预测
      const result = await model.classify(imageRef.current, 5);

      // 更新预测结果
      setPredictions(result);
    } catch (err) {
      console.error("图像分析失败:", err);
      setError("图像分析失败,请重试");
    } finally {
      setIsAnalyzing(false);
    }
  };

  // 触发文件选择
  const triggerFileSelect = () => {
    fileInputRef.current?.click();
  };

  return (
    <div className="image-classifier">
      <h2>浏览器AI图像识别</h2>

      <div className="model-status">
        {isModelLoading ? (
          <div className="loading-indicator">
            <div className="spinner"></div>
            <p>加载AI模型中(约5-10MB)...</p>
          </div>
        ) : (
          <p className="model-ready">AI模型已加载完成,可以开始使用</p>
        )}
      </div>

      <div className="upload-section">
        <input
          type="file"
          accept="image/*"
          onChange={handleFileChange}
          ref={fileInputRef}
          style={{ display: "none" }}
        />

        <button
          className="upload-button"
          onClick={triggerFileSelect}
          disabled={isModelLoading}
        >
          选择图片
        </button>

        {imageURL && (
          <button
            className="analyze-button"
            onClick={analyzeImage}
            disabled={isModelLoading || isAnalyzing}
          >
            {isAnalyzing ? "分析中..." : "分析图片"}
          </button>
        )}
      </div>

      {error && <div className="error-message">{error}</div>}

      <div className="image-preview-container">
        {imageURL && (
          <div className="image-preview">
            <img
              src={imageURL}
              alt="上传的图片"
              ref={imageRef}
              crossOrigin="anonymous"
            />
          </div>
        )}
      </div>

      {predictions.length > 0 && (
        <div className="prediction-results">
          <h3>识别结果</h3>
          <ul>
            {predictions.map((prediction, index) => (
              <li key={index}>
                <span className="prediction-label">{prediction.className}</span>
                <div className="prediction-bar-container">
                  <div
                    className="prediction-bar"
                    style={{ width: `${prediction.probability * 100}%` }}
                  ></div>
                  <span className="prediction-percent">
                    {(prediction.probability * 100).toFixed(2)}%
                  </span>
                </div>
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
};

export default ImageClassifier;

使用 transformers.js 实现浏览器中的文本生成:

tsx 复制代码
// src/components/TextGenerator.tsx
import React, { useState, useEffect, useRef } from "react";
import { pipeline, env } from "@xenova/transformers";

// 配置transformers.js
env.allowLocalModels = false;
env.useBrowserCache = true;

const TextGenerator: React.FC = () => {
  const [prompt, setPrompt] = useState("");
  const [generatedText, setGeneratedText] = useState("");
  const [isGenerating, setIsGenerating] = useState(false);
  const [isModelLoading, setIsModelLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [loadingProgress, setLoadingProgress] = useState(0);

  const generatorRef = useRef<any>(null);
  const maxLength = 100;

  // 加载模型
  useEffect(() => {
    const loadModel = async () => {
      try {
        setIsModelLoading(true);
        setError(null);

        // 加载文本生成模型(选择较小的模型以适应浏览器环境)
        generatorRef.current = await pipeline(
          "text-generation",
          "Xenova/distilgpt2",
          {
            progress_callback: (progress) => {
              setLoadingProgress(Math.round(progress * 100));
            },
          }
        );

        console.log("文本生成模型加载成功");
      } catch (err) {
        console.error("模型加载失败:", err);
        setError("模型加载失败,请刷新页面重试");
      } finally {
        setIsModelLoading(false);
      }
    };

    loadModel();
  }, []);

  // 生成文本
  const generateText = async () => {
    if (!generatorRef.current || !prompt.trim()) {
      return;
    }

    try {
      setIsGenerating(true);
      setError(null);
      setGeneratedText("");

      // 使用模型生成文本
      const result = await generatorRef.current(prompt, {
        max_new_tokens: maxLength,
        temperature: 0.7,
        repetition_penalty: 1.2,
      });

      setGeneratedText(result[0].generated_text);
    } catch (err) {
      console.error("文本生成失败:", err);
      setError("文本生成失败,请重试");
    } finally {
      setIsGenerating(false);
    }
  };

  return (
    <div className="text-generator">
      <h2>浏览器内文本生成</h2>

      <div className="model-status">
        {isModelLoading ? (
          <div className="loading-indicator">
            <div className="progress-bar">
              <div
                className="progress-fill"
                style={{ width: `${loadingProgress}%` }}
              ></div>
            </div>
            <p>加载AI模型中: {loadingProgress}%</p>
          </div>
        ) : (
          <p className="model-ready">AI模型已加载完成,可以开始使用</p>
        )}
      </div>

      <div className="prompt-container">
        <textarea
          value={prompt}
          onChange={(e) => setPrompt(e.target.value)}
          placeholder="输入提示词..."
          rows={3}
          disabled={isModelLoading || isGenerating}
        />

        <button
          onClick={generateText}
          disabled={isModelLoading || isGenerating || !prompt.trim()}
        >
          {isGenerating ? "生成中..." : "生成文本"}
        </button>
      </div>

      {error && <div className="error-message">{error}</div>}

      {generatedText && (
        <div className="generated-text-container">
          <h3>生成结果</h3>
          <div className="generated-text">{generatedText}</div>
        </div>
      )}
    </div>
  );
};

export default TextGenerator;

5. 智能 UI 组件:AI 驱动的用户界面

自动表单填充组件:

tsx 复制代码
// src/components/AIFormAssistant.tsx
import React, { useState } from "react";
import { useFormContext, Controller } from "react-hook-form";

interface AIFormAssistantProps {
  fieldMappings: {
    [key: string]: string;
  };
  formContext: ReturnType<typeof useFormContext>;
  onError?: (error: Error) => void;
}

const AIFormAssistant: React.FC<AIFormAssistantProps> = ({
  fieldMappings,
  formContext,
  onError,
}) => {
  const { control, setValue } = formContext;
  const [isAnalyzing, setIsAnalyzing] = useState(false);
  const [rawText, setRawText] = useState("");

  // 智能分析文本并填充表单
  const analyzeAndFill = async () => {
    if (!rawText.trim()) return;

    setIsAnalyzing(true);

    try {
      // 调用分析API
      const response = await fetch("/api/analyze-text", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          text: rawText,
          fields: Object.keys(fieldMappings),
        }),
      });

      if (!response.ok) {
        throw new Error("分析请求失败");
      }

      const data = await response.json();

      // 根据分析结果填充表单
      Object.entries(data.results).forEach(([field, value]) => {
        if (fieldMappings[field]) {
          setValue(fieldMappings[field], value, {
            shouldValidate: true,
            shouldDirty: true,
            shouldTouch: true,
          });
        }
      });
    } catch (error) {
      console.error("分析文本失败:", error);
      onError?.(error);
    } finally {
      setIsAnalyzing(false);
    }
  };

  return (
    <div className="ai-form-assistant">
      <h3>AI表单助手</h3>
      <p>粘贴简历文本或任何相关信息,AI将自动分析并填充表单</p>

      <Controller
        name="rawText"
        control={control}
        render={({ field }) => (
          <textarea
            {...field}
            value={rawText}
            onChange={(e) => setRawText(e.target.value)}
            placeholder="在此粘贴文本..."
            rows={5}
            className="raw-text-input"
          />
        )}
      />

      <button
        onClick={analyzeAndFill}
        disabled={isAnalyzing || !rawText.trim()}
        className="analyze-button"
      >
        {isAnalyzing ? (
          <>
            <span className="spinner"></span>
            分析中...
          </>
        ) : (
          "AI智能填充"
        )}
      </button>
    </div>
  );
};

export default AIFormAssistant;

实时内容推荐组件:

tsx 复制代码
// src/components/AIContentRecommender.tsx
import React, { useState, useEffect } from "react";
import { useDebounce } from "../hooks/useDebounce";

interface RecommendationItem {
  id: string;
  title: string;
  description: string;
  url: string;
  type: "article" | "product" | "video";
  relevanceScore: number;
}

interface AIContentRecommenderProps {
  contextText: string;
  userId?: string;
  maxItems?: number;
  onRecommendationClick?: (item: RecommendationItem) => void;
  onError?: (error: Error) => void;
}

const AIContentRecommender: React.FC<AIContentRecommenderProps> = ({
  contextText,
  userId,
  maxItems = 3,
  onRecommendationClick,
  onError,
}) => {
  const [recommendations, setRecommendations] = useState<RecommendationItem[]>(
    []
  );
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // 使用防抖,避免频繁API调用
  const debouncedContextText = useDebounce(contextText, 500);

  // 获取推荐内容
  useEffect(() => {
    const fetchRecommendations = async () => {
      if (!debouncedContextText || debouncedContextText.length < 10) {
        setRecommendations([]);
        return;
      }

      setIsLoading(true);
      setError(null);

      try {
        const response = await fetch("/api/recommend-content", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            context: debouncedContextText,
            userId,
            maxItems,
          }),
        });

        if (!response.ok) {
          throw new Error("推荐请求失败");
        }

        const data = await response.json();
        setRecommendations(data.recommendations || []);
      } catch (err) {
        console.error("获取推荐内容失败:", err);
        setError("无法获取推荐内容");
        onError?.(err);
      } finally {
        setIsLoading(false);
      }
    };

    fetchRecommendations();
  }, [debouncedContextText, userId, maxItems, onError]);

  // 处理推荐点击
  const handleItemClick = (item: RecommendationItem) => {
    // 记录点击事件
    fetch("/api/track-recommendation-click", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        recommendationId: item.id,
        userId,
        context: contextText,
      }),
    }).catch((err) => {
      console.error("追踪点击失败:", err);
    });

    // 调用外部点击处理函数
    onRecommendationClick?.(item);
  };

  // 如果没有足够的上下文或没有推荐结果,不显示组件
  if (
    (!debouncedContextText || debouncedContextText.length < 10) &&
    !isLoading
  ) {
    return null;
  }

  return (
    <div className="ai-content-recommender">
      <h3>为您推荐</h3>

      {isLoading && (
        <div className="loading-indicator">
          <div className="spinner"></div>
          <p>正在分析内容,查找相关推荐...</p>
        </div>
      )}

      {error && <div className="error-message">{error}</div>}

      {!isLoading &&
        recommendations.length === 0 &&
        debouncedContextText.length >= 10 && (
          <p className="no-recommendations">暂无相关推荐</p>
        )}

      <ul className="recommendations-list">
        {recommendations.map((item) => (
          <li key={item.id} className={`recommendation-item ${item.type}`}>
            <a
              href={item.url}
              onClick={(e) => {
                e.preventDefault();
                handleItemClick(item);
                window.open(item.url, "_blank");
              }}
              target="_blank"
              rel="noopener noreferrer"
            >
              <div className="recommendation-content">
                <span className="recommendation-type">
                  {getTypeLabel(item.type)}
                </span>
                <h4 className="recommendation-title">{item.title}</h4>
                <p className="recommendation-description">{item.description}</p>
              </div>
              <div className="recommendation-score">
                <span className="relevance-label">相关度</span>
                <div className="relevance-bar">
                  <div
                    className="relevance-fill"
                    style={{ width: `${item.relevanceScore * 100}%` }}
                  ></div>
                </div>
              </div>
            </a>
          </li>
        ))}
      </ul>
    </div>
  );
};

// 获取内容类型标签
function getTypeLabel(type: RecommendationItem["type"]): string {
  switch (type) {
    case "article":
      return "文章";
    case "product":
      return "产品";
    case "video":
      return "视频";
    default:
      return "内容";
  }
}

export default AIContentRecommender;

AI 驱动的搜索组件:

tsx 复制代码
// src/components/AIEnhancedSearch.tsx
import React, { useState, useEffect, useRef } from "react";
import { useDebounce } from "../hooks/useDebounce";

interface SearchResult {
  id: string;
  title: string;
  excerpt: string;
  url: string;
  category: string;
  score: number;
  highlights?: {
    title?: string[];
    excerpt?: string[];
  };
}

interface AIEnhancedSearchProps {
  placeholder?: string;
  minSearchLength?: number;
  maxResults?: number;
  onResultClick?: (result: SearchResult) => void;
  onSearch?: (query: string, results: SearchResult[]) => void;
}

const AIEnhancedSearch: React.FC<AIEnhancedSearchProps> = ({
  placeholder = "智能搜索...",
  minSearchLength = 2,
  maxResults = 10,
  onResultClick,
  onSearch,
}) => {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState<SearchResult[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const [isOpen, setIsOpen] = useState(false);
  const [selectedIndex, setSelectedIndex] = useState(-1);
  const [error, setError] = useState<string | null>(null);

  const searchContainerRef = useRef<HTMLDivElement>(null);
  const searchInputRef = useRef<HTMLInputElement>(null);

  // 使用防抖处理搜索查询
  const debouncedQuery = useDebounce(query, 300);

  // 处理搜索
  useEffect(() => {
    const fetchSearchResults = async () => {
      if (!debouncedQuery || debouncedQuery.length < minSearchLength) {
        setResults([]);
        if (isOpen) setIsOpen(false);
        return;
      }

      setIsLoading(true);
      setError(null);

      try {
        const response = await fetch("/api/ai-search", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            query: debouncedQuery,
            maxResults,
          }),
        });

        if (!response.ok) {
          throw new Error("搜索请求失败");
        }

        const data = await response.json();
        setResults(data.results || []);

        // 打开结果下拉框
        if (data.results.length > 0) {
          setIsOpen(true);
        } else {
          setIsOpen(false);
        }

        // 调用外部搜索回调
        onSearch?.(debouncedQuery, data.results || []);
      } catch (err) {
        console.error("搜索失败:", err);
        setError("搜索过程中出错");
        setResults([]);
      } finally {
        setIsLoading(false);
      }
    };

    fetchSearchResults();
  }, [debouncedQuery, minSearchLength, maxResults, onSearch]);

  // 处理点击结果
  const handleResultClick = (result: SearchResult) => {
    onResultClick?.(result);
    setIsOpen(false);
    setQuery("");
  };

  // 处理键盘导航
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (!isOpen) return;

    switch (e.key) {
      case "ArrowDown":
        e.preventDefault();
        setSelectedIndex((prevIndex) =>
          prevIndex < results.length - 1 ? prevIndex + 1 : prevIndex
        );
        break;
      case "ArrowUp":
        e.preventDefault();
        setSelectedIndex((prevIndex) => (prevIndex > 0 ? prevIndex - 1 : 0));
        break;
      case "Enter":
        e.preventDefault();
        if (selectedIndex >= 0 && selectedIndex < results.length) {
          handleResultClick(results[selectedIndex]);
        }
        break;
      case "Escape":
        e.preventDefault();
        setIsOpen(false);
        break;
      default:
        break;
    }
  };

  // 处理点击外部关闭下拉框
  useEffect(() => {
    const handleClickOutside = (event: MouseEvent) => {
      if (
        searchContainerRef.current &&
        !searchContainerRef.current.contains(event.target as Node)
      ) {
        setIsOpen(false);
      }
    };

    document.addEventListener("mousedown", handleClickOutside);

    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, []);

  // 呈现高亮文本
  const renderHighlightedText = (text: string, highlights?: string[]) => {
    if (!highlights || highlights.length === 0) {
      return <span>{text}</span>;
    }

    const parts: React.ReactNode[] = [];
    let lastIndex = 0;

    highlights.forEach((highlight) => {
      const index = text
        .toLowerCase()
        .indexOf(highlight.toLowerCase(), lastIndex);

      if (index === -1) return;

      if (index > lastIndex) {
        parts.push(
          <span key={`text-${lastIndex}-${index}`}>
            {text.substring(lastIndex, index)}
          </span>
        );
      }

      parts.push(
        <mark key={`highlight-${index}`}>
          {text.substring(index, index + highlight.length)}
        </mark>
      );

      lastIndex = index + highlight.length;
    });

    if (lastIndex < text.length) {
      parts.push(
        <span key={`text-${lastIndex}-end`}>{text.substring(lastIndex)}</span>
      );
    }

    return <>{parts}</>;
  };

  return (
    <div className="ai-enhanced-search" ref={searchContainerRef}>
      <div className="search-input-container">
        <input
          ref={searchInputRef}
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          onKeyDown={handleKeyDown}
          onFocus={() => results.length > 0 && setIsOpen(true)}
          placeholder={placeholder}
          className="search-input"
          aria-label="搜索"
        />

        {query.length > 0 && (
          <button
            className="clear-search"
            onClick={() => {
              setQuery("");
              setResults([]);
              setIsOpen(false);
              searchInputRef.current?.focus();
            }}
            aria-label="清除搜索"
          >
            ×
          </button>
        )}

        <div className="search-icon">
          {isLoading ? (
            <div className="search-spinner"></div>
          ) : (
            <svg viewBox="0 0 24 24" width="18" height="18">
              <path
                fill="currentColor"
                d="M15.5 14h-.79l-.28-.27a6.5 6.5 0 0 0 1.48-5.34c-.47-2.78-2.79-5-5.59-5.34a6.505 6.505 0 0 0-7.27 7.27c.34 2.8 2.56 5.12 5.34 5.59a6.5 6.5 0 0 0 5.34-1.48l.27.28v.79l4.25 4.25c.41.41 1.08.41 1.49 0 .41-.41.41-1.08 0-1.49L15.5 14zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
              />
            </svg>
          )}
        </div>
      </div>

      {isOpen && (
        <div className="search-results">
          {error && (
            <div className="search-error">
              <p>{error}</p>
            </div>
          )}

          {results.length === 0 && !error && !isLoading && (
            <div className="no-results">
              <p>没有找到匹配的结果</p>
            </div>
          )}

          {results.length > 0 && (
            <ul className="results-list">
              {results.map((result, index) => (
                <li
                  key={result.id}
                  className={`result-item ${
                    index === selectedIndex ? "selected" : ""
                  }`}
                  onClick={() => handleResultClick(result)}
                >
                  <div className="result-category">{result.category}</div>
                  <h4 className="result-title">
                    {renderHighlightedText(
                      result.title,
                      result.highlights?.title
                    )}
                  </h4>
                  <p className="result-excerpt">
                    {renderHighlightedText(
                      result.excerpt,
                      result.highlights?.excerpt
                    )}
                  </p>
                </li>
              ))}
            </ul>
          )}

          <div className="search-footer">
            <span className="ai-powered">AI增强搜索</span>
          </div>
        </div>
      )}
    </div>
  );
};

export default AIEnhancedSearch;

6. 生成式 UI:AI 驱动的动态界面组件

智能数据可视化组件:

tsx 复制代码
// src/components/AIDataVisualizer.tsx
import React, { useState, useEffect } from "react";
import { Chart, registerables } from "chart.js";
import { Bar, Line, Pie, Doughnut, Scatter } from "react-chartjs-2";

// 注册Chart.js组件
Chart.register(...registerables);

interface DataPoint {
  [key: string]: any;
}

interface VisualizationConfig {
  type: "bar" | "line" | "pie" | "doughnut" | "scatter";
  title: string;
  description: string;
  labels: string[];
  datasets: Array<{
    label: string;
    data: number[];
    backgroundColor?: string | string[];
    borderColor?: string | string[];
    borderWidth?: number;
  }>;
  xAxisLabel?: string;
  yAxisLabel?: string;
}

interface AIDataVisualizerProps {
  data: DataPoint[];
  includeFields?: string[];
  excludeFields?: string[];
  onError?: (error: Error) => void;
}

const AIDataVisualizer: React.FC<AIDataVisualizerProps> = ({
  data,
  includeFields,
  excludeFields,
  onError,
}) => {
  const [visualizationConfig, setVisualizationConfig] =
    useState<VisualizationConfig | null>(null);
  const [alternativeVisualizations, setAlternativeVisualizations] = useState<
    VisualizationConfig[]
  >([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  // 获取可视化配置
  useEffect(() => {
    const generateVisualization = async () => {
      if (!data || data.length === 0) {
        setIsLoading(false);
        return;
      }

      setIsLoading(true);
      setError(null);

      try {
        // 准备数据
        const dataFields = Object.keys(data[0]).filter((key) => {
          if (includeFields && includeFields.length > 0) {
            return includeFields.includes(key);
          }
          if (excludeFields && excludeFields.length > 0) {
            return !excludeFields.includes(key);
          }
          return true;
        });

        // 调用API获取可视化配置
        const response = await fetch("/api/generate-visualization", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            data: data.slice(0, 100), // 限制大小
            fields: dataFields,
          }),
        });

        if (!response.ok) {
          throw new Error("可视化请求失败");
        }

        const visualizationData = await response.json();

        // 设置主可视化和备选可视化
        if (
          visualizationData.visualizations &&
          visualizationData.visualizations.length > 0
        ) {
          setVisualizationConfig(visualizationData.visualizations[0]);
          setAlternativeVisualizations(
            visualizationData.visualizations.slice(1)
          );
        } else {
          throw new Error("未能生成可视化配置");
        }
      } catch (err) {
        console.error("生成可视化失败:", err);
        setError("无法自动生成数据可视化");
        onError?.(err);
      } finally {
        setIsLoading(false);
      }
    };

    generateVisualization();
  }, [data, includeFields, excludeFields, onError]);

  // 切换到备选可视化
  const switchVisualization = (index: number) => {
    if (index >= 0 && index < alternativeVisualizations.length) {
      // 将当前可视化添加到备选列表
      if (visualizationConfig) {
        setAlternativeVisualizations([
          visualizationConfig,
          ...alternativeVisualizations.filter((_, i) => i !== index),
        ]);
      }

      // 设置新的主可视化
      setVisualizationConfig(alternativeVisualizations[index]);
    }
  };

  // 渲染图表
  const renderChart = () => {
    if (!visualizationConfig) return null;

    const commonOptions = {
      responsive: true,
      maintainAspectRatio: false,
      plugins: {
        title: {
          display: true,
          text: visualizationConfig.title,
          font: {
            size: 16,
            weight: "bold",
          },
          padding: {
            bottom: 10,
          },
        },
        legend: {
          display: true,
          position: "top" as const,
        },
        tooltip: {
          enabled: true,
        },
      },
    };

    const data = {
      labels: visualizationConfig.labels,
      datasets: visualizationConfig.datasets,
    };

    switch (visualizationConfig.type) {
      case "bar":
        return (
          <Bar
            data={data}
            options={{
              ...commonOptions,
              scales: {
                x: {
                  title: {
                    display: !!visualizationConfig.xAxisLabel,
                    text: visualizationConfig.xAxisLabel,
                  },
                },
                y: {
                  title: {
                    display: !!visualizationConfig.yAxisLabel,
                    text: visualizationConfig.yAxisLabel,
                  },
                  beginAtZero: true,
                },
              },
            }}
          />
        );
      case "line":
        return (
          <Line
            data={data}
            options={{
              ...commonOptions,
              scales: {
                x: {
                  title: {
                    display: !!visualizationConfig.xAxisLabel,
                    text: visualizationConfig.xAxisLabel,
                  },
                },
                y: {
                  title: {
                    display: !!visualizationConfig.yAxisLabel,
                    text: visualizationConfig.yAxisLabel,
                  },
                  beginAtZero: true,
                },
              },
            }}
          />
        );
      case "pie":
        return <Pie data={data} options={commonOptions} />;
      case "doughnut":
        return <Doughnut data={data} options={commonOptions} />;
      case "scatter":
        return (
          <Scatter
            data={data}
            options={{
              ...commonOptions,
              scales: {
                x: {
                  title: {
                    display: !!visualizationConfig.xAxisLabel,
                    text: visualizationConfig.xAxisLabel,
                  },
                },
                y: {
                  title: {
                    display: !!visualizationConfig.yAxisLabel,
                    text: visualizationConfig.yAxisLabel,
                  },
                },
              },
            }}
          />
        );
      default:
        return null;
    }
  };

  return (
    <div className="ai-data-visualizer">
      {isLoading && (
        <div className="loading-container">
          <div className="spinner"></div>
          <p>正在分析数据,生成最佳可视化...</p>
        </div>
      )}

      {error && <div className="error-message">{error}</div>}

      {!isLoading && !error && visualizationConfig && (
        <div className="visualization-container">
          <div className="visualization-header">
            <h3>{visualizationConfig.title}</h3>
            <p className="visualization-description">
              {visualizationConfig.description}
            </p>
          </div>

          <div className="chart-container">{renderChart()}</div>

          {alternativeVisualizations.length > 0 && (
            <div className="alternative-visualizations">
              <h4>其他可视化选项</h4>
              <div className="alternatives-grid">
                {alternativeVisualizations.map((viz, index) => (
                  <div
                    key={`alt-${index}`}
                    className="alternative-item"
                    onClick={() => switchVisualization(index)}
                  >
                    <div className="alternative-type">
                      {getChartTypeLabel(viz.type)}
                    </div>
                    <div className="alternative-title">{viz.title}</div>
                  </div>
                ))}
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
};

// 获取图表类型标签
function getChartTypeLabel(type: string): string {
  switch (type) {
    case "bar":
      return "柱状图";
    case "line":
      return "折线图";
    case "pie":
      return "饼图";
    case "doughnut":
      return "环形图";
    case "scatter":
      return "散点图";
    default:
      return "图表";
  }
}

export default AIDataVisualizer;

7. API 端点实现:连接 AI 服务与前端

文本分析 API 端点:

typescript 复制代码
// pages/api/analyze-text.ts
import { NextApiRequest, NextApiResponse } from "next";
import OpenAI from "openai";

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== "POST") {
    return res.status(405).json({ error: "只支持POST请求" });
  }

  const { text, fields } = req.body;

  if (!text || !fields || !Array.isArray(fields)) {
    return res.status(400).json({ error: "缺少必要参数" });
  }

  try {
    // 构建提示词
    const prompt = `分析以下文本,提取这些字段的信息: ${fields.join(", ")}。
                    文本内容:
                    """
                    ${text}
                    """

                    以JSON格式返回结果,按照以下例子格式:
                    {
                      "results": {
                        "${fields[0]}": "提取的值",
                        "${fields[1]}": "提取的值",
                        ...
                      }
                    }
                    只返回JSON,不要有其他内容。`;

    // 发送到OpenAI API
    const completion = await openai.chat.completions.create({
      model: "gpt-4-1106-preview",
      response_format: { type: "json_object" },
      messages: [
        {
          role: "system",
          content: "你是一个擅长提取文本信息的AI助手,只返回指定格式的JSON。",
        },
        {
          role: "user",
          content: prompt,
        },
      ],
      temperature: 0.1,
    });

    // 解析响应内容
    const responseContent = completion.choices[0].message.content;

    try {
      const parsedData = JSON.parse(responseContent || "{}");
      return res.status(200).json(parsedData);
    } catch (jsonError) {
      console.error("JSON解析错误:", jsonError);
      return res.status(500).json({ error: "响应格式错误" });
    }
  } catch (error) {
    console.error("OpenAI API错误:", error);
    return res.status(500).json({ error: "处理请求时出错" });
  }
}

内容推荐 API 端点:

typescript 复制代码
// pages/api/recommend-content.ts
import { NextApiRequest, NextApiResponse } from "next";
import OpenAI from "openai";
import { PrismaClient } from "@prisma/client";

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

const prisma = new PrismaClient();

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== "POST") {
    return res.status(405).json({ error: "只支持POST请求" });
  }

  const { context, userId, maxItems = 3 } = req.body;

  if (!context) {
    return res.status(400).json({ error: "缺少必要参数" });
  }

  try {
    // 查询用户的浏览历史和兴趣标签(如果有userId)
    let userProfile = null;
    if (userId) {
      userProfile = await prisma.userProfile.findUnique({
        where: { userId },
        include: {
          interests: true,
          viewHistory: {
            take: 20,
            orderBy: { viewedAt: "desc" },
            include: { content: true },
          },
        },
      });
    }

    // 获取最新内容
    const recentContent = await prisma.content.findMany({
      take: 50,
      orderBy: { createdAt: "desc" },
      include: {
        categories: true,
        tags: true,
      },
    });

    // 利用OpenAI API进行内容推荐
    const completion = await openai.chat.completions.create({
      model: "gpt-4-1106-preview",
      response_format: { type: "json_object" },
      messages: [
        {
          role: "system",
          content: `你是一个内容推荐系统。根据提供的上下文和用户信息,推荐最相关的内容。
          返回JSON格式的推荐列表,包含id、title、description、url、type和relevanceScore字段。`,
        },
        {
          role: "user",
          content: `用户当前阅读的内容:
                    """
                    ${context}
                    """

                    ${
                      userProfile
                        ? `用户兴趣标签: ${userProfile.interests
                            .map((i) => i.name)
                            .join(", ")}`
                        : ""
                    }

                    可选内容:
                    ${JSON.stringify(
                      recentContent.map((c) => ({
                        id: c.id,
                        title: c.title,
                        excerpt: c.excerpt,
                        url: c.url,
                        type: c.type,
                        categories: c.categories.map((cat) => cat.name),
                        tags: c.tags.map((tag) => tag.name),
                      }))
                    )}

                    根据上下文推荐${maxItems}个最相关的内容项,格式如下:
                    {
                      "recommendations": [
                        {
                          "id": "内容ID",
                          "title": "内容标题",
                          "description":"内容摘要",
                          "url": "内容URL",
                          "type": "article|product|video",
                          "relevanceScore": 0.95  // 0到1之间的相关度分数
                        },
                        ...
                      ]
                    }
                    只返回JSON,不要有其他内容。`,
        },
      ],
      temperature: 0.5,
    });

    // 解析响应内容
    const responseContent = completion.choices[0].message.content;

    try {
      const parsedData = JSON.parse(responseContent || "{}");

      // 记录推荐历史
      if (userId) {
        await prisma.recommendationEvent.create({
          data: {
            userId,
            context: context.slice(0, 500), // 限制长度
            recommendedItems: {
              create: parsedData.recommendations.map((rec: any) => ({
                contentId: rec.id,
                score: rec.relevanceScore,
              })),
            },
          },
        });
      }

      return res.status(200).json(parsedData);
    } catch (jsonError) {
      console.error("JSON解析错误:", jsonError);
      return res.status(500).json({ error: "响应格式错误" });
    }
  } catch (error) {
    console.error("API错误:", error);
    return res.status(500).json({ error: "处理请求时出错" });
  }
}

8. AI 功能集成的最佳实践

用户体验与性能优化

  1. 显示加载状态:AI 处理通常需要时间,务必提供清晰的加载反馈
  2. 增量响应:使用流式 API 获取部分结果并立即显示
  3. 本地缓存:缓存常见查询结果减少 API 调用
  4. 优雅降级:当 AI 服务不可用时,提供备选功能
  5. 批处理请求:合并多个请求减少 API 调用次数

成本控制策略

  1. 模型选择:根据任务复杂度选择合适的模型(GPT-4/GPT-3.5 等)
  2. 令牌优化:精简提示词和上下文长度
  3. 请求节流:限制单个用户的请求频率
  4. 使用微调模型:对特定任务使用专门微调的模型
  5. 混合客户端和服务端 AI:简单任务使用本地模型,复杂任务使用云 API

隐私与安全考量

typescript 复制代码
// src/utils/aiSanitizer.ts
export function sanitizeUserInput(input: string): string {
  // 移除潜在的敏感信息
  const sanitized = input
    // 移除可能的电话号码
    .replace(
      /(\+\d{1,3}[\s-])?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}/g,
      "[电话号码已移除]"
    )
    // 移除可能的邮箱地址
    .replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, "[邮箱已移除]")
    // 移除可能的身份证号
    .replace(
      /[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]/g,
      "[身份证号已移除]"
    )
    // 移除可能的信用卡号
    .replace(/\b(?:\d{4}[ -]?){3}(?:\d{4})\b/g, "[信用卡号已移除]");

  return sanitized;
}

// 在发送到AI服务前进行敏感信息检查
export function checkSensitiveContent(text: string): boolean {
  const sensitivePatterns = [
    /密码|password|pwd/i,
    /social security|社保|社会保险/i,
    /私人|隐私|保密|confidential|private/i,
    /secret|秘密/i,
  ];

  return sensitivePatterns.some((pattern) => pattern.test(text));
}

AI 功能集成的核心原则

  1. 透明度:清晰标明哪些功能使用了 AI
  2. 用户控制:让用户能够选择是否使用 AI 功能
  3. 持续改进:收集用户反馈不断优化 AI 功能
  4. 可解释性:确保用户理解 AI 的决策过程
  5. 补充而非替代:AI 应该增强用户体验,而不是完全取代传统 UI

9. 实际案例:AI 驱动的代码编辑器

以下是一个集成了 AI 辅助功能的代码编辑器组件:

tsx 复制代码
// src/components/AICodeEditor.tsx
import React, { useState, useRef, useEffect } from "react";
import Editor from "@monaco-editor/react";
import OpenAI from "openai";

interface AICodeEditorProps {
  initialCode: string;
  language: string;
  theme?: "vs-dark" | "light";
  onChange?: (code: string) => void;
  onSave?: (code: string) => void;
  aiApiKey?: string;
}

const AICodeEditor: React.FC<AICodeEditorProps> = ({
  initialCode,
  language,
  theme = "vs-dark",
  onChange,
  onSave,
  aiApiKey,
}) => {
  const [code, setCode] = useState(initialCode);
  const [isGenerating, setIsGenerating] = useState(false);
  const [aiSuggestion, setAiSuggestion] = useState<string | null>(null);
  const [showAiPanel, setShowAiPanel] = useState(false);
  const [aiPrompt, setAiPrompt] = useState("");
  const [errorMessage, setErrorMessage] = useState<string | null>(null);

  const editorRef = useRef<any>(null);
  const openai = aiApiKey
    ? new OpenAI({ apiKey: aiApiKey, dangerouslyAllowBrowser: true })
    : null;

  // 处理编辑器挂载
  const handleEditorDidMount = (editor: any) => {
    editorRef.current = editor;

    // 添加快捷键支持
    editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
      handleSave();
    });

    // 添加AI建议快捷键
    editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Space, () => {
      const selection = editor.getSelection();
      const selectedText = editor.getModel().getValueInRange(selection);

      if (selectedText) {
        setAiPrompt(`完善这段代码: ${selectedText}`);
        handleAiSuggest(`完善这段代码: ${selectedText}`);
      } else {
        setShowAiPanel(true);
      }
    });
  };

  // 处理代码变更
  const handleCodeChange = (value: string | undefined) => {
    if (value !== undefined) {
      setCode(value);
      onChange?.(value);
    }
  };

  // 保存代码
  const handleSave = () => {
    onSave?.(code);
  };

  // 应用AI建议
  const applyAiSuggestion = () => {
    if (!aiSuggestion) return;

    const editor = editorRef.current;
    if (!editor) return;

    const selection = editor.getSelection();

    // 如果有选择区域,替换它
    if (!selection.isEmpty()) {
      editor.executeEdits("ai-suggestion", [
        {
          range: selection,
          text: aiSuggestion,
        },
      ]);
    } else {
      // 否则在当前光标位置插入
      const position = editor.getPosition();
      editor.executeEdits("ai-suggestion", [
        {
          range: new monaco.Range(
            position.lineNumber,
            position.column,
            position.lineNumber,
            position.column
          ),
          text: aiSuggestion,
        },
      ]);
    }

    // 清除建议
    setAiSuggestion(null);
    setShowAiPanel(false);
  };

  // 请求AI建议
  const handleAiSuggest = async (prompt: string) => {
    if (!openai) {
      setErrorMessage("未配置AI API密钥");
      return;
    }

    setIsGenerating(true);
    setErrorMessage(null);

    try {
      const editor = editorRef.current;
      const currentCode = editor.getValue();
      const selection = editor.getSelection();
      const selectedText =
        selection && !selection.isEmpty()
          ? editor.getModel().getValueInRange(selection)
          : "";

      const selectedLines = selectedText
        ? `选中的代码:\n${selectedText}\n`
        : "";

      const completion = await openai.chat.completions.create({
        model: "gpt-4-1106-preview",
        messages: [
          {
            role: "system",
            content: `你是一个代码助手。你需要帮助用户编写或完善${language}代码。
            只返回代码部分,不要包含解释或其他文本。确保代码可以直接运行或集成到现有代码中。`,
          },
          {
            role: "user",
            content: `${prompt}\n\n当前文件内容:\n${currentCode}\n\n${selectedLines}`,
          },
        ],
        temperature: 0.3,
      });

      const suggestion = completion.choices[0].message.content;

      // 移除可能的代码块标记
      const cleanedSuggestion = suggestion
        ?.replace(/```[\w]*\n/g, "")
        .replace(/```$/g, "")
        .trim();

      setAiSuggestion(cleanedSuggestion || null);
      setShowAiPanel(true);
    } catch (error) {
      console.error("AI建议生成失败:", error);
      setErrorMessage("AI建议生成失败,请重试");
    } finally {
      setIsGenerating(false);
    }
  };

  // 处理AI操作
  const handleAiAction = (action: string) => {
    const editor = editorRef.current;
    if (!editor) return;

    const selection = editor.getSelection();
    const selectedText =
      selection && !selection.isEmpty()
        ? editor.getModel().getValueInRange(selection)
        : "";

    let prompt = "";

    switch (action) {
      case "complete":
        prompt = `完成这段代码: ${selectedText}`;
        break;
      case "refactor":
        prompt = `重构这段代码,提高可读性和性能: ${selectedText}`;
        break;
      case "explain":
        prompt = `解释这段代码的功能: ${selectedText}`;
        break;
      case "optimize":
        prompt = `优化这段代码的性能: ${selectedText}`;
        break;
      case "comment":
        prompt = `为这段代码添加详细注释: ${selectedText}`;
        break;
      default:
        return;
    }

    setAiPrompt(prompt);
    handleAiSuggest(prompt);
  };

  return (
    <div className="ai-code-editor">
      <div className="editor-toolbar">
        <button onClick={handleSave} className="save-button">
          保存 (Ctrl+S)
        </button>

        <div className="ai-actions">
          <button
            onClick={() => setShowAiPanel(!showAiPanel)}
            className="ai-button"
          >
            AI助手 (Ctrl+Space)
          </button>

          <div className="ai-quick-actions">
            <button onClick={() => handleAiAction("complete")}>完成代码</button>
            <button onClick={() => handleAiAction("refactor")}>重构</button>
            <button onClick={() => handleAiAction("optimize")}>优化</button>
            <button onClick={() => handleAiAction("comment")}>添加注释</button>
          </div>
        </div>
      </div>

      <div className="editor-container">
        <Editor
          height="70vh"
          language={language}
          theme={theme}
          value={code}
          onChange={handleCodeChange}
          onMount={handleEditorDidMount}
          options={{
            minimap: { enabled: true },
            scrollBeyondLastLine: false,
            fontSize: 14,
            wordWrap: "on",
            autoIndent: "full",
            formatOnPaste: true,
            formatOnType: true,
          }}
        />

        {showAiPanel && (
          <div className="ai-panel">
            <div className="ai-panel-header">
              <h3>AI助手</h3>
              <button
                onClick={() => setShowAiPanel(false)}
                className="close-button"
              >
                ×
              </button>
            </div>

            <div className="ai-prompt-container">
              <input
                type="text"
                value={aiPrompt}
                onChange={(e) => setAiPrompt(e.target.value)}
                placeholder="输入提示,如'重构这段代码'或'优化性能'"
                className="ai-prompt-input"
              />
              <button
                onClick={() => handleAiSuggest(aiPrompt)}
                disabled={isGenerating || !aiPrompt.trim()}
                className="generate-button"
              >
                {isGenerating ? "生成中..." : "生成"}
              </button>
            </div>

            {errorMessage && (
              <div className="error-message">{errorMessage}</div>
            )}

            {isGenerating && (
              <div className="generating-indicator">
                <div className="spinner"></div>
                <p>AI正在分析代码并生成建议...</p>
              </div>
            )}

            {aiSuggestion && (
              <div className="ai-suggestion">
                <div className="suggestion-header">
                  <h4>AI建议</h4>
                  <button onClick={applyAiSuggestion} className="apply-button">
                    应用
                  </button>
                </div>
                <pre className="suggestion-code">
                  <code>{aiSuggestion}</code>
                </pre>
              </div>
            )}
          </div>
        )}
      </div>
    </div>
  );
};

export default AICodeEditor;

10. 前端 AI 技术的未来展望

随着前端与 AI 技术的融合不断深入,我们可以预见以下趋势将在未来几年内成为主流:

1. 更智能的用户界面

  • 自适应 UI:根据用户行为和偏好动态调整界面布局
  • 预测性交互:预测用户意图提前加载内容和功能
  • 情感感知界面:通过分析用户行为和输入调整界面风格和反馈

2. 前端开发的 AI 赋能

  • AI 辅助编码:更智能的代码生成和完成
  • 自动化测试:AI 生成测试用例和执行测试
  • 代码优化:自动分析和优化性能瓶颈
  • 设计转代码:从设计草图直接生成高质量前端代码

3. 本地化 AI 能力

  • 边缘 AI:更多计算在用户设备上完成
  • 轻量级模型:专为前端优化的小型 AI 模型
  • 混合推理:结合本地和云端推理能力
  • 持续学习:基于用户行为不断改进本地模型

4. 无缝多模态体验

  • 语音 UI:自然语言控制界面和功能
  • 视觉理解:摄像头/图像输入的智能解析
  • AR/VR 集成:将 AI 能力延伸到虚拟和增强现实环境
  • 跨设备协同:桌面和移动设备间的智能交互协调

总结与实践建议

在将 AI 集成到前端应用中时,关键是找到人工智能增强而非取代用户体验的平衡点。遵循以下原则可以帮助你成功地构建 AI 驱动的前端应用:

  1. 以用户为中心:AI 功能应该解决真实用户问题,而不是为技术而技术
  2. 渐进增强:保持应用的基本功能在没有 AI 的情况下也能正常工作
  3. 开发迭代:从小功能开始,收集用户反馈,不断改进
  4. 性能与隐私:密切关注前端 AI 功能对性能的影响,始终注重用户隐私
  5. 持续学习:AI 领域发展迅速,保持学习最新技术和最佳实践

下一篇预告

敬请期待我们的下一篇文章:《【React 全栈开发】服务端渲染与 API 整合最佳实践》,我们将深入探讨:

  1. 构建高性能的 React 服务端渲染应用
  2. 前端与后端 API 的无缝集成策略
  3. 数据获取与状态管理的全栈方案
  4. 身份验证与安全最佳实践

关于作者

Hi,我是 hyy,一位热爱技术的全栈开发者:

  • 🚀 专注 TypeScript 全栈开发,偏前端技术栈
  • 💼 多元工作背景(跨国企业、技术外包、创业公司)
  • 📝 掘金活跃技术作者
  • 🎵 电子音乐爱好者
  • 🎮 游戏玩家
  • 💻 技术分享达人

加入我们

欢迎加入前端技术交流圈,与 10000+开发者一起:

  • 探讨前端最新技术趋势
  • 解决开发难题
  • 分享职场经验
  • 获取优质学习资源

添加方式:掘金摸鱼沸点 👈 扫码进群

相关推荐
葡萄城技术团队4 分钟前
AI 不会淘汰电子表格,反而会让电子表格成为 AI 操作系统
前端·人工智能
BigTopOne5 分钟前
Short Put and Short Call
前端
TrisighT8 分钟前
Agent 并行工具调用快了 0.8 秒——200 组实测发现,多花的那 3100 token 全打了水漂
aigc·agent·ai编程
Cache技术分享16 分钟前
465. Java 反射 - 获取类的简单名
前端·后端
Sirius Wu21 分钟前
OpenClaw 并发安全完整详解
网络·人工智能·安全·ai·架构·aigc
BigTopOne25 分钟前
什么是行权价,什么是权利金,什么是行权?
前端
xlxxy_27 分钟前
sap获取批次特性报表
java·linux·开发语言·前端·数据库·abap·mm
手写码匠27 分钟前
从零实现大模型推理引擎:Continuous Batching 与动态调度系统深度解析
人工智能·深度学习·算法·aigc
BigTopOne28 分钟前
Long put and Long call
前端
石像鬼₧魂石36 分钟前
【Y2Ksoft】贵阳陈桥饭店ERP管理系统
大数据·前端·物联网·html·数据库架构