React集成示例
本页面展示了如何在 React 应用中集成和使用 XAI-SDK,包括基础集成、Hooks 使用、组件开发和最佳实践。
📋 示例列表
基础 React 应用
简单的 React AI 聊天应用
代码编辑器组件
带 AI 辅助功能的 React 代码编辑器
项目分析器
React 项目代码分析工具
自定义 Hooks
可复用的 XAI-SDK React Hooks
🚀 快速开始
安装依赖
bash
npm install xai-sdk @xai-sdk/react-adapter react react-dom
# 或者
yarn add xai-sdk @xai-sdk/react-adapter react react-dom
项目结构
src/
├── components/
│ ├── ChatInterface.tsx
│ ├── CodeEditor.tsx
│ └── AIAssistant.tsx
├── hooks/
│ ├── useXAI.ts
│ ├── useChat.ts
│ └── useCodeAnalysis.ts
├── contexts/
│ └── XAIContext.tsx
├── utils/
│ └── xai-config.ts
└── App.tsx
📚 基础示例
XAI Context Provider
首先创建一个 Context 来管理 XAI-SDK 实例:
tsx
// src/contexts/XAIContext.tsx
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { XAI_SDK } from 'xai-sdk';
import { ReactAdapterPlugin } from '@xai-sdk/react-adapter';
import { OpenAIProviderPlugin } from '@xai-sdk/openai-provider';
interface XAIContextType {
sdk: XAI_SDK | null;
isInitialized: boolean;
error: string | null;
}
const XAIContext = createContext<XAIContextType>({
sdk: null,
isInitialized: false,
error: null
});
interface XAIProviderProps {
children: ReactNode;
apiKey: string;
}
export const XAIProvider: React.FC<XAIProviderProps> = ({ children, apiKey }) => {
const [sdk, setSdk] = useState<XAI_SDK | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const initializeSDK = async () => {
try {
const sdkInstance = new XAI_SDK({ environment: 'browser' });
// 注册插件
await sdkInstance.registerPlugin(new ReactAdapterPlugin());
await sdkInstance.registerPlugin(new OpenAIProviderPlugin({
apiKey: apiKey
}));
await sdkInstance.initialize();
setSdk(sdkInstance);
setIsInitialized(true);
} catch (err) {
setError(err instanceof Error ? err.message : '初始化失败');
}
};
if (apiKey) {
initializeSDK();
}
}, [apiKey]);
return (
<XAIContext.Provider value={{ sdk, isInitialized, error }}>
{children}
</XAIContext.Provider>
);
};
export const useXAI = () => {
const context = useContext(XAIContext);
if (!context) {
throw new Error('useXAI must be used within an XAIProvider');
}
return context;
};
基础聊天组件
tsx
// src/components/ChatInterface.tsx
import React, { useState, useRef, useEffect } from 'react';
import { useXAI } from '../contexts/XAIContext';
interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: Date;
}
const ChatInterface: React.FC = () => {
const { sdk, isInitialized, error } = useXAI();
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
useEffect(() => {
if (isInitialized) {
addMessage('system', 'AI 助手已就绪,请输入您的问题。');
}
}, [isInitialized]);
const addMessage = (role: Message['role'], content: string): string => {
const id = Date.now().toString();
const message: Message = {
id,
role,
content,
timestamp: new Date()
};
setMessages(prev => [...prev, message]);
return id;
};
const updateMessage = (id: string, content: string) => {
setMessages(prev => prev.map(msg =>
msg.id === id ? { ...msg, content } : msg
));
};
const sendMessage = async () => {
if (!input.trim() || !sdk || isLoading) return;
const userMessage = input.trim();
setInput('');
addMessage('user', userMessage);
setIsLoading(true);
const loadingId = addMessage('assistant', '正在思考...');
try {
const response = await sdk.chat(userMessage, {
stream: false,
maxTokens: 1000,
temperature: 0.7
});
updateMessage(loadingId, response.content);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '处理请求时出现错误';
updateMessage(loadingId, `抱歉,${errorMessage}`);
} finally {
setIsLoading(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
if (error) {
return (
<div className="chat-error">
<h3>初始化错误</h3>
<p>{error}</p>
</div>
);
}
if (!isInitialized) {
return (
<div className="chat-loading">
<div className="spinner"></div>
<p>正在初始化 AI 助手...</p>
</div>
);
}
return (
<div className="chat-interface">
<div className="chat-header">
<h2>AI 助手</h2>
<div className="status-indicator online"></div>
</div>
<div className="chat-messages">
{messages.map((message) => (
<div key={message.id} className={`message ${message.role}`}>
<div className="message-header">
<span className="role">
{message.role === 'user' ? '用户' :
message.role === 'assistant' ? 'AI助手' : '系统'}
</span>
<span className="timestamp">
{message.timestamp.toLocaleTimeString()}
</span>
</div>
<div className="message-content">
{message.content}
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="chat-input">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="输入您的问题... (Shift+Enter 换行)"
disabled={isLoading}
rows={3}
/>
<button
onClick={sendMessage}
disabled={!input.trim() || isLoading}
className="send-button"
>
{isLoading ? '发送中...' : '发送'}
</button>
</div>
</div>
);
};
export default ChatInterface;
样式文件
css
/* src/styles/ChatInterface.css */
.chat-interface {
display: flex;
flex-direction: column;
height: 600px;
border: 1px solid #e1e5e9;
border-radius: 8px;
overflow: hidden;
background: white;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #e1e5e9;
}
.chat-header h2 {
margin: 0;
font-size: 18px;
color: #24292e;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #28a745;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
max-width: 80%;
padding: 12px;
border-radius: 8px;
word-wrap: break-word;
}
.message.user {
align-self: flex-end;
background: #0366d6;
color: white;
}
.message.assistant {
align-self: flex-start;
background: #f1f3f4;
border: 1px solid #d0d7de;
}
.message.system {
align-self: center;
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
font-style: italic;
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
font-size: 12px;
opacity: 0.7;
}
.message-content {
line-height: 1.4;
white-space: pre-wrap;
}
.chat-input {
display: flex;
padding: 16px;
border-top: 1px solid #e1e5e9;
background: #f8f9fa;
gap: 12px;
}
.chat-input textarea {
flex: 1;
padding: 8px 12px;
border: 1px solid #d0d7de;
border-radius: 6px;
resize: none;
font-family: inherit;
font-size: 14px;
}
.chat-input textarea:focus {
outline: none;
border-color: #0366d6;
box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1);
}
.send-button {
padding: 8px 16px;
background: #0366d6;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.send-button:hover:not(:disabled) {
background: #0256cc;
}
.send-button:disabled {
background: #6c757d;
cursor: not-allowed;
}
.chat-loading, .chat-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
text-align: center;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #0366d6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
🔧 高级示例
自定义 Hooks
tsx
// src/hooks/useChat.ts
import { useState, useCallback } from 'react';
import { useXAI } from '../contexts/XAIContext';
interface ChatMessage {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
}
interface UseChatOptions {
maxTokens?: number;
temperature?: number;
onError?: (error: Error) => void;
}
export const useChat = (options: UseChatOptions = {}) => {
const { sdk } = useXAI();
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [isLoading, setIsLoading] = useState(false);
const sendMessage = useCallback(async (content: string) => {
if (!sdk || !content.trim()) return;
const userMessage: ChatMessage = {
id: Date.now().toString(),
role: 'user',
content: content.trim(),
timestamp: new Date()
};
setMessages(prev => [...prev, userMessage]);
setIsLoading(true);
try {
const response = await sdk.chat(content, {
maxTokens: options.maxTokens || 1000,
temperature: options.temperature || 0.7,
stream: false
});
const assistantMessage: ChatMessage = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: response.content,
timestamp: new Date()
};
setMessages(prev => [...prev, assistantMessage]);
} catch (error) {
const err = error instanceof Error ? error : new Error('Unknown error');
options.onError?.(err);
} finally {
setIsLoading(false);
}
}, [sdk, options]);
const clearMessages = useCallback(() => {
setMessages([]);
}, []);
return {
messages,
isLoading,
sendMessage,
clearMessages
};
};
tsx
// src/hooks/useCodeAnalysis.ts
import { useState, useCallback } from 'react';
import { useXAI } from '../contexts/XAIContext';
interface AnalysisResult {
score: number;
suggestions: string[];
metrics: {
lines: number;
complexity: number;
maintainability: number;
};
issues: Array<{
type: 'error' | 'warning' | 'info';
message: string;
line?: number;
}>;
}
export const useCodeAnalysis = () => {
const { sdk } = useXAI();
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [result, setResult] = useState<AnalysisResult | null>(null);
const [error, setError] = useState<string | null>(null);
const analyzeCode = useCallback(async (code: string, language: string = 'javascript') => {
if (!sdk || !code.trim()) return;
setIsAnalyzing(true);
setError(null);
try {
const analysis = await sdk.analyze(code, {
type: 'code-quality',
language,
includeMetrics: true,
includeSuggestions: true
});
setResult(analysis as AnalysisResult);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '分析失败';
setError(errorMessage);
} finally {
setIsAnalyzing(false);
}
}, [sdk]);
const clearResult = useCallback(() => {
setResult(null);
setError(null);
}, []);
return {
analyzeCode,
isAnalyzing,
result,
error,
clearResult
};
};
代码编辑器组件
tsx
// src/components/CodeEditor.tsx
import React, { useState, useRef, useEffect } from 'react';
import { useCodeAnalysis } from '../hooks/useCodeAnalysis';
interface CodeEditorProps {
initialCode?: string;
language?: string;
onChange?: (code: string) => void;
}
const CodeEditor: React.FC<CodeEditorProps> = ({
initialCode = '',
language = 'javascript',
onChange
}) => {
const [code, setCode] = useState(initialCode);
const [showAnalysis, setShowAnalysis] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const { analyzeCode, isAnalyzing, result, error } = useCodeAnalysis();
const handleCodeChange = (newCode: string) => {
setCode(newCode);
onChange?.(newCode);
};
const handleAnalyze = async () => {
await analyzeCode(code, language);
setShowAnalysis(true);
};
const insertTab = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Tab') {
e.preventDefault();
const textarea = e.currentTarget;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const newCode = code.substring(0, start) + ' ' + code.substring(end);
setCode(newCode);
// 设置光标位置
setTimeout(() => {
textarea.selectionStart = textarea.selectionEnd = start + 2;
}, 0);
}
};
return (
<div className="code-editor">
<div className="editor-toolbar">
<div className="toolbar-left">
<span className="language-label">{language}</span>
<span className="lines-count">{code.split('\n').length} 行</span>
</div>
<div className="toolbar-right">
<button
onClick={handleAnalyze}
disabled={!code.trim() || isAnalyzing}
className="analyze-button"
>
{isAnalyzing ? '分析中...' : '🔍 分析代码'}
</button>
<button
onClick={() => setShowAnalysis(!showAnalysis)}
className="toggle-analysis"
disabled={!result}
>
{showAnalysis ? '隐藏分析' : '显示分析'}
</button>
</div>
</div>
<div className="editor-container">
<div className="editor-main">
<textarea
ref={textareaRef}
value={code}
onChange={(e) => handleCodeChange(e.target.value)}
onKeyDown={insertTab}
className="code-textarea"
placeholder="在这里输入代码..."
spellCheck={false}
/>
</div>
{showAnalysis && (result || error) && (
<div className="analysis-panel">
<div className="analysis-header">
<h3>分析结果</h3>
<button
onClick={() => setShowAnalysis(false)}
className="close-button"
>
×
</button>
</div>
{error ? (
<div className="analysis-error">
<p>分析失败: {error}</p>
</div>
) : result ? (
<div className="analysis-content">
<div className="metrics">
<div className="metric">
<span className="metric-label">质量评分</span>
<span className={`metric-value score-${getScoreClass(result.score)}`}>
{result.score}/100
</span>
</div>
<div className="metric">
<span className="metric-label">代码行数</span>
<span className="metric-value">{result.metrics.lines}</span>
</div>
<div className="metric">
<span className="metric-label">复杂度</span>
<span className="metric-value">{result.metrics.complexity}</span>
</div>
</div>
{result.suggestions.length > 0 && (
<div className="suggestions">
<h4>改进建议</h4>
<ul>
{result.suggestions.map((suggestion, index) => (
<li key={index}>{suggestion}</li>
))}
</ul>
</div>
)}
{result.issues.length > 0 && (
<div className="issues">
<h4>发现的问题</h4>
<ul>
{result.issues.map((issue, index) => (
<li key={index} className={`issue-${issue.type}`}>
<span className="issue-type">{issue.type.toUpperCase()}</span>
<span className="issue-message">{issue.message}</span>
{issue.line && <span className="issue-line">第 {issue.line} 行</span>}
</li>
))}
</ul>
</div>
)}
</div>
) : null}
</div>
)}
</div>
</div>
);
};
const getScoreClass = (score: number): string => {
if (score >= 80) return 'good';
if (score >= 60) return 'medium';
return 'poor';
};
export default CodeEditor;
主应用组件
tsx
// src/App.tsx
import React, { useState } from 'react';
import { XAIProvider } from './contexts/XAIContext';
import ChatInterface from './components/ChatInterface';
import CodeEditor from './components/CodeEditor';
import './styles/App.css';
const App: React.FC = () => {
const [apiKey, setApiKey] = useState(localStorage.getItem('openai-api-key') || '');
const [activeTab, setActiveTab] = useState<'chat' | 'editor'>('chat');
const [showSettings, setShowSettings] = useState(!apiKey);
const handleApiKeySubmit = (key: string) => {
setApiKey(key);
localStorage.setItem('openai-api-key', key);
setShowSettings(false);
};
if (showSettings) {
return (
<div className="app">
<div className="settings-modal">
<div className="settings-content">
<h2>配置 API Key</h2>
<p>请输入您的 OpenAI API Key 以开始使用:</p>
<ApiKeyForm onSubmit={handleApiKeySubmit} />
</div>
</div>
</div>
);
}
return (
<XAIProvider apiKey={apiKey}>
<div className="app">
<header className="app-header">
<h1>XAI-SDK React 示例</h1>
<nav className="app-nav">
<button
className={activeTab === 'chat' ? 'active' : ''}
onClick={() => setActiveTab('chat')}
>
💬 聊天助手
</button>
<button
className={activeTab === 'editor' ? 'active' : ''}
onClick={() => setActiveTab('editor')}
>
📝 代码编辑器
</button>
<button
onClick={() => setShowSettings(true)}
className="settings-button"
>
⚙️ 设置
</button>
</nav>
</header>
<main className="app-main">
{activeTab === 'chat' ? (
<ChatInterface />
) : (
<CodeEditor
initialCode="// 在这里编写代码\nfunction hello() {\n console.log('Hello, World!');\n}"
language="javascript"
/>
)}
</main>
</div>
</XAIProvider>
);
};
interface ApiKeyFormProps {
onSubmit: (key: string) => void;
}
const ApiKeyForm: React.FC<ApiKeyFormProps> = ({ onSubmit }) => {
const [key, setKey] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (key.trim()) {
onSubmit(key.trim());
}
};
return (
<form onSubmit={handleSubmit} className="api-key-form">
<input
type="password"
value={key}
onChange={(e) => setKey(e.target.value)}
placeholder="sk-..."
className="api-key-input"
required
/>
<button type="submit" className="submit-button">
保存并开始
</button>
</form>
);
};
export default App;
🎨 TypeScript 类型定义
typescript
// src/types/xai.ts
export interface ChatMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: Date;
}
export interface AnalysisResult {
score: number;
suggestions: string[];
metrics: {
lines: number;
complexity: number;
maintainability: number;
};
issues: Issue[];
}
export interface Issue {
type: 'error' | 'warning' | 'info';
message: string;
line?: number;
column?: number;
}
export interface ChatOptions {
maxTokens?: number;
temperature?: number;
stream?: boolean;
}
export interface AnalysisOptions {
type: 'code-quality' | 'security' | 'performance';
language: string;
includeMetrics?: boolean;
includeSuggestions?: boolean;
}
🧪 测试示例
tsx
// src/__tests__/ChatInterface.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { XAIProvider } from '../contexts/XAIContext';
import ChatInterface from '../components/ChatInterface';
// Mock XAI SDK
jest.mock('xai-sdk', () => ({
XAI_SDK: jest.fn().mockImplementation(() => ({
registerPlugin: jest.fn(),
initialize: jest.fn(),
chat: jest.fn().mockResolvedValue({ content: 'Mock response' })
}))
}));
const renderWithProvider = (component: React.ReactElement) => {
return render(
<XAIProvider apiKey="test-key">
{component}
</XAIProvider>
);
};
describe('ChatInterface', () => {
test('renders chat interface', async () => {
renderWithProvider(<ChatInterface />);
await waitFor(() => {
expect(screen.getByText('AI 助手')).toBeInTheDocument();
});
});
test('sends message when button clicked', async () => {
renderWithProvider(<ChatInterface />);
await waitFor(() => {
expect(screen.getByText('AI 助手已就绪')).toBeInTheDocument();
});
const input = screen.getByPlaceholderText(/输入您的问题/);
const sendButton = screen.getByText('发送');
fireEvent.change(input, { target: { value: 'Hello' } });
fireEvent.click(sendButton);
await waitFor(() => {
expect(screen.getByText('Hello')).toBeInTheDocument();
});
});
});
📖 最佳实践
1. 错误边界
tsx
// src/components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('XAI SDK Error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>出现了错误</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
重试
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
2. 性能优化
tsx
// 使用 React.memo 优化组件渲染
const ChatMessage = React.memo<{ message: ChatMessage }>(({ message }) => {
return (
<div className={`message ${message.role}`}>
<div className="message-content">{message.content}</div>
</div>
);
});
// 使用 useMemo 缓存计算结果
const MessageList: React.FC<{ messages: ChatMessage[] }> = ({ messages }) => {
const sortedMessages = useMemo(() => {
return messages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
}, [messages]);
return (
<div className="message-list">
{sortedMessages.map(message => (
<ChatMessage key={message.id} message={message} />
))}
</div>
);
};
3. 自定义 Hook 模式
tsx
// src/hooks/useLocalStorage.ts
import { useState, useEffect } from 'react';
export const useLocalStorage = <T>(key: string, initialValue: T) => {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
return [storedValue, setValue] as const;
};
🚀 部署配置
Vite 配置
typescript
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
define: {
'process.env': {}
},
build: {
rollupOptions: {
external: ['xai-sdk'],
output: {
globals: {
'xai-sdk': 'XAI_SDK'
}
}
}
}
});
环境变量
bash
# .env.local
VITE_OPENAI_API_KEY=your-api-key-here
VITE_XAI_SDK_DEBUG=true
📝 相关资源
💡 提示: 这些示例展示了 XAI-SDK 在 React 应用中的完整集成方案。您可以根据项目需求选择合适的组件和模式。