第十二篇:【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+开发者一起:

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

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

相关推荐
2501_9153738813 分钟前
Vue 3零基础入门:从环境搭建到第一个组件
前端·javascript·vue.js
沙振宇3 小时前
【Web】使用Vue3开发鸿蒙的HelloWorld!
前端·华为·harmonyos
运维@小兵3 小时前
vue开发用户注册功能
前端·javascript·vue.js
蓝婷儿4 小时前
前端面试每日三题 - Day 30
前端·面试·职场和发展
oMMh4 小时前
使用C# ASP.NET创建一个可以由服务端推送信息至客户端的WEB应用(2)
前端·c#·asp.net
EndingCoder4 小时前
跨平台移动开发框架React Native和Flutter性能对比
flutter·react native·react.js
一口一个橘子4 小时前
[ctfshow web入门] web69
前端·web安全·网络安全
读心悦5 小时前
CSS:盒子阴影与渐变完全解析:从基础语法到创意应用
前端·css
湛海不过深蓝6 小时前
【ts】defineProps数组的类型声明
前端·javascript·vue.js
chennalC#c.h.JA Ptho6 小时前
生成了一个AI算法
经验分享·笔记·aigc