完整指南:用 React + Deepseek 开发智能客服助手

307 阅读5分钟

完整指南:用 React + Deepseek 开发智能客服助手

在这个教程中,我们将详细介绍如何使用 React、TypeScript 和 Deepseek API 开发一个智能客服助手组件。这个组件不仅界面美观,而且具有良好的交互体验和完整的功能实现。

:::tip 在线演示 🔗 演示地址:智能客服助手

可以在该页面右侧看到一个悬浮的客服图标,点击即可体验完整功能。 :::

1. 功能特点

1.1 核心功能

  • 悬浮图标:固定位置显示,支持悬停动画
  • 聊天对话框:优雅的弹出动画和毛玻璃效果
  • AI 对话:集成 Deepseek API,提供智能问答服务
  • 暗色模式:完整支持明暗主题切换

1.2 交互体验

  • 平滑的动画效果
  • 即时的用户反馈
  • 优雅的加载状态
  • 清晰的错误提示

2. 技术栈

技术用途说明
React前端框架组件化开发
TypeScript开发语言类型安全
Tailwind CSS样式框架原子化 CSS
Deepseek APIAI 对话智能问答服务

3. 代码实现

3.1 项目结构

src/components/
├── FloatChat/
│   └── index.tsx      # 悬浮图标和对话框组件
├── DeepseekChat/
│   └── index.tsx      # AI 对话实现组件
└── utils/
    └── deepseek.ts    # Deepseek API 封装

3.2 FloatChat 组件实现

这是主要的悬浮图标和对话框组件:

import React, { useState } from 'react';
import clsx from 'clsx';
import DeepseekChat from '@site/src/components/DeepseekChat';

export default function FloatChat() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      {/* 悬浮图标 */}
      <div
        className={clsx(
          'fixed top-[400px] right-[calc(50%-400px)] z-50 cursor-pointer transition-all duration-300 hover:scale-110 hover:shadow-lg',
          { 'opacity-0': isOpen }
        )}
        onClick={() => setIsOpen(true)}
      >
        <img
          src="https://bianliangrensheng.cn/gImage/title/customer-service-beauty.jpg"
          alt="客服"
          className="w-16 h-16 rounded-full shadow-lg border-2 border-blue-100 hover:border-blue-200"
        />
      </div>

      {/* 聊天对话框 */}
      {isOpen && (
        <div className="fixed inset-0 bg-black/30 backdrop-blur-sm z-50 flex items-end justify-end transition-all duration-300">
          <div className="w-[400px] bg-white dark:bg-gray-800 rounded-tl-2xl shadow-2xl transition-all duration-300 animate-slideUp">
            {/* 对话框头部 */}
            <div className="flex justify-between items-center px-6 py-4 bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/30 dark:to-blue-800/30 rounded-tl-2xl border-b border-blue-100 dark:border-blue-800">
              <div className="flex items-center gap-3">
                <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
                <h3 className="text-lg font-semibold bg-gradient-to-r from-blue-600 to-blue-800 dark:from-blue-400 dark:to-blue-300 text-transparent bg-clip-text">
                  智能客服助手
                </h3>
              </div>
              <button
                onClick={() => setIsOpen(false)}
                className="w-8 h-8 rounded-lg flex items-center justify-center bg-white/80 dark:bg-gray-800/80 hover:bg-white dark:hover:bg-gray-700 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-all duration-200 border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
                aria-label="关闭对话框"
              >
                <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" />
                </svg>
              </button>
            </div>

            {/* 欢迎消息 */}
            <div className="px-6 py-4 bg-gradient-to-b from-blue-50/50 to-transparent dark:from-blue-900/10 dark:to-transparent">
              <p className="text-sm text-gray-600 dark:text-gray-400">
                您好!我是智能客服助手,很高兴为您服务。我可以:
              </p>
              <ul className="mt-2 space-y-1 text-sm text-gray-600 dark:text-gray-400">
                <li className="flex items-center gap-2">
                  <span className="w-1 h-1 bg-blue-500 rounded-full"></span>
                  解答技术开发相关问题
                </li>
                <li className="flex items-center gap-2">
                  <span className="w-1 h-1 bg-blue-500 rounded-full"></span>
                  提供项目合作咨询
                </li>
                <li className="flex items-center gap-2">
                  <span className="w-1 h-1 bg-blue-500 rounded-full"></span>
                  介绍我们的服务内容
                </li>
              </ul>
            </div>

            {/* 聊天内容区域 */}
            <div className="h-[400px] overflow-y-auto bg-gradient-to-b from-white to-blue-50 dark:from-gray-800 dark:to-gray-800/95">
              <DeepseekChat />
            </div>
          </div>
        </div>
      )}

      <style>{`
        @keyframes slideUp {
          from {
            transform: translateY(100%);
            opacity: 0;
          }
          to {
            transform: translateY(0);
            opacity: 1;
          }
        }
        .animate-slideUp {
          animation: slideUp 0.3s ease-out forwards;
        }
      `}</style>
    </>
  );
}

3.3 DeepseekChat 组件实现

AI 对话的核心实现组件:

import React, { useState } from 'react';
import { chatWithSystem } from '@site/src/utils/deepseek';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import clsx from 'clsx';

export default function DeepseekChat() {
    const [input, setInput] = useState('');
    const [response, setResponse] = useState('');
    const [loading, setLoading] = useState(false);
    const {siteConfig} = useDocusaurusContext();
    const apiKey = siteConfig.customFields?.deepseekApiKey as string;

    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        if (!input.trim() || !apiKey) return;

        setLoading(true);
        try {
            const result = await chatWithSystem(
                input, 
                apiKey,
                '你是一个专业的技术顾问,负责解答用户关于软件开发、项目合作等方面的问题。请用简洁专业的语言回答。'
            );
            setResponse(result || '抱歉,没有得到回应');
        } catch (error: any) {
            console.error('聊天出错:', error);
            let errorMessage = '抱歉,发生了错误,请稍后再试';
            
            if (error?.message?.includes('402 Insufficient Balance')) {
                errorMessage = '抱歉,API 账户余额不足,请联系管理员充值。';
            } else if (error?.message?.includes('429')) {
                errorMessage = '请求太频繁,请稍后再试。';
            } else if (error?.message?.includes('401')) {
                errorMessage = 'API Key 无效,请检查配置。';
            }
            
            setResponse(errorMessage);
        } finally {
            setLoading(false);
        }
    };

    return (
        <div className="p-4 space-y-4">
            {response && (
                <div className={clsx(
                    'p-4 rounded-lg transition-all duration-300',
                    response.includes('抱歉') 
                        ? 'bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400' 
                        : 'bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800/50'
                )}>
                    <div className="flex items-center gap-2 mb-2">
                        <div className="w-6 h-6 rounded-full bg-gradient-to-r from-blue-500 to-blue-600 flex items-center justify-center text-white text-xs">
                            AI
                        </div>
                        <h3 className="font-medium text-blue-600 dark:text-blue-400">
                            {response.includes('抱歉') ? '错误提示' : 'AI 回应'}
                        </h3>
                    </div>
                    <p className="whitespace-pre-wrap text-sm text-gray-700 dark:text-gray-300 pl-8">
                        {response}
                    </p>
                </div>
            )}

            <form onSubmit={handleSubmit} className="space-y-3 sticky bottom-0 bg-white dark:bg-gray-800 p-4 border-t border-blue-100 dark:border-blue-800/50">
                <div className="flex flex-col gap-2">
                    <div className="relative flex-1">
                        <textarea
                            value={input}
                            onChange={(e) => setInput(e.target.value)}
                            onKeyDown={(e) => {
                                if (e.key === 'Enter' && !e.shiftKey) {
                                    e.preventDefault();
                                    handleSubmit(e);
                                }
                            }}
                            placeholder="请输入您的问题..."
                            className="w-full p-3 pr-4 border border-blue-200 dark:border-blue-700 rounded-xl min-h-[80px] max-h-[160px] bg-white dark:bg-gray-800 focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y placeholder-gray-400 dark:placeholder-gray-500 scrollbar-thin scrollbar-thumb-blue-200 dark:scrollbar-thumb-blue-700 scrollbar-track-transparent"
                            style={{
                                scrollbarWidth: 'thin',
                                scrollbarColor: 'rgb(191 219 254) transparent'
                            }}
                        />
                    </div>
                    <div className="flex items-center justify-between">
                        <p className="text-xs text-gray-500 dark:text-gray-400">
                            按 Enter 发送消息,Shift + Enter 换行
                        </p>
                        <button
                            type="submit"
                            disabled={loading}
                            className="px-6 py-2 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white rounded-lg transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium flex items-center gap-2 shadow-sm hover:shadow-md"
                        >
                            {loading ? (
                                <>
                                    <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
                                    思考中...
                                </>
                            ) : (
                                <>
                                    <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
                                    </svg>
                                    发送
                                </>
                            )}
                        </button>
                    </div>
                </div>
            </form>
        </div>
    );
}

3.4 Deepseek API 封装

import OpenAI from 'openai';
import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions';

function createOpenAIClient(apiKey: string) {
    return new OpenAI({
        baseURL: 'https://api.deepseek.com',
        apiKey,
        dangerouslyAllowBrowser: true
    });
}

export async function chatWithSystem(
    content: string,
    apiKey: string,
    systemPrompt: string = 'You are a helpful assistant.'
) {
    const messages: ChatCompletionMessageParam[] = [
        { role: 'system', content: systemPrompt },
        { role: 'user', content }
    ];
    
    return chatWithDeepseek(messages, apiKey);
} 

export async function chatWithDeepseek(
    messages: ChatCompletionMessageParam[],
    apiKey: string,
    model: string = 'deepseek-chat'
) {
    const openai = createOpenAIClient(apiKey);

    try {
        const completion = await openai.chat.completions.create({
            messages,
            model,
        });

        return completion.choices[0]?.message?.content || '';
    } catch (error) {
        console.error('Deepseek API 调用错误:', error);
        throw error;
    }
}

4. 使用方法

4.1 配置 Deepseek API

docusaurus.config.ts 中配置 API Key:

customFields: {
  deepseekApiKey: process.env.DEEPSEEK_API_KEY || '',
}

4.2 添加到页面

在需要显示客服助手的页面中引入组件:

import FloatChat from '@site/src/components/FloatChat';

<FloatChat />

5. 设计亮点

5.1 UI/UX 设计

  • 使用渐变色和毛玻璃效果提升视觉体验
  • 添加平滑的动画过渡
  • 优化暗色模式下的显示效果
  • 精心设计的间距和布局

5.2 交互优化

  • 输入框支持自适应高度
  • Enter 快捷发送消息
  • 清晰的加载状态反馈
  • 优雅的错误提示样式

5.3 性能考虑

  • 组件按需渲染
  • 优化动画性能
  • 合理的状态管理
  • 防抖处理用户输入

6. 注意事项

:::warning 安全提示

  • 生产环境中不要在前端暴露 API Key
  • 建议通过后端代理调用 Deepseek API
  • 注意控制 API 调用频率 :::

欲了解更多信息,欢迎访问我的博客:AI 客服助手开发指南 | 变量人生,为您提供更好的阅读体验与技术资源。

参考资源