496 lines
21 KiB
JavaScript
496 lines
21 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
import './../styles/App.css';
|
||
|
||
const DebateConfiguration = ({ onCreateDebate, onViewSessions, onViewSettings, isGuest }) => {
|
||
const [formData, setFormData] = useState({
|
||
topic: '',
|
||
proProvider: 'openai',
|
||
proModel: 'gpt-4',
|
||
conProvider: 'claude',
|
||
conModel: 'claude-3-opus',
|
||
maxRounds: 5,
|
||
maxTokens: 500,
|
||
webSearchEnabled: false,
|
||
webSearchMode: 'auto'
|
||
});
|
||
|
||
const [availableModels, setAvailableModels] = useState({
|
||
openai: [],
|
||
claude: [],
|
||
qwen: [],
|
||
deepseek: []
|
||
});
|
||
|
||
const [availableProviders, setAvailableProviders] = useState([
|
||
{ provider: 'openai', display_name: 'OpenAI' },
|
||
{ provider: 'claude', display_name: 'Claude' },
|
||
{ provider: 'qwen', display_name: 'Qwen' },
|
||
{ provider: 'deepseek', display_name: 'DeepSeek' }
|
||
]);
|
||
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [modelsLoading, setModelsLoading] = useState({});
|
||
|
||
useEffect(() => {
|
||
// Load available providers when component mounts
|
||
loadAvailableProviders();
|
||
}, []);
|
||
|
||
const loadAvailableProviders = async () => {
|
||
try {
|
||
// Get all API keys from backend to determine which providers are available
|
||
const providers = ['openai', 'claude', 'qwen', 'deepseek'];
|
||
const available = [];
|
||
|
||
for (const provider of providers) {
|
||
try {
|
||
const response = await fetch(`http://localhost:8000/api-keys/${provider}`);
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.api_key) {
|
||
// For Qwen and DeepSeek, we can check if the key is valid
|
||
if (provider === 'qwen' || provider === 'deepseek') {
|
||
// For now, just check if key exists - we'll implement validation later if needed
|
||
available.push({
|
||
provider: provider,
|
||
display_name: provider.charAt(0).toUpperCase() + provider.slice(1)
|
||
});
|
||
} else {
|
||
// For OpenAI and Claude, we're skipping validation as requested
|
||
available.push({
|
||
provider: provider,
|
||
display_name: provider.charAt(0).toUpperCase() + provider.slice(1)
|
||
});
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
// If there's an error getting the API key, skip this provider
|
||
console.error(`Error getting API key for ${provider}:`, error);
|
||
}
|
||
}
|
||
|
||
if (available.length > 0) {
|
||
setAvailableProviders(available);
|
||
|
||
// Update form data to use valid providers
|
||
if (!available.some(p => p.provider === formData.proProvider)) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
proProvider: available[0]?.provider || prev.proProvider
|
||
}));
|
||
}
|
||
|
||
if (!available.some(p => p.provider === formData.conProvider)) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
conProvider: available[0]?.provider || prev.conProvider
|
||
}));
|
||
}
|
||
} else {
|
||
// If no providers have API keys, show all providers but disable functionality
|
||
setAvailableProviders([
|
||
{ provider: 'openai', display_name: 'OpenAI' },
|
||
{ provider: 'claude', display_name: 'Claude' },
|
||
{ provider: 'qwen', display_name: 'Qwen' },
|
||
{ provider: 'deepseek', display_name: 'DeepSeek' }
|
||
]);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading available providers:', error);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
// Update model options when provider changes
|
||
if (formData.proProvider) {
|
||
loadModelsForProvider(formData.proProvider);
|
||
}
|
||
if (formData.conProvider) {
|
||
loadModelsForProvider(formData.conProvider);
|
||
}
|
||
}, [formData.proProvider, formData.conProvider]);
|
||
|
||
const loadModelsForProvider = async (provider) => {
|
||
setModelsLoading(prev => ({ ...prev, [provider]: true }));
|
||
try {
|
||
// Special handling for Qwen - fetch directly from Qwen API
|
||
if (provider === 'qwen') {
|
||
try {
|
||
// Call backend to get Qwen models (backend will fetch from Qwen API)
|
||
const qwenResponse = await fetch(`http://localhost:8000/models/${provider}`);
|
||
if (qwenResponse.ok) {
|
||
const qwenData = await qwenResponse.json();
|
||
console.log('Backend Qwen models response:', qwenData); // Debug log
|
||
|
||
const models = qwenData.models || [];
|
||
console.log('Fetched Qwen models:', models); // Debug log
|
||
|
||
setAvailableModels(prev => ({
|
||
...prev,
|
||
[provider]: models
|
||
}));
|
||
|
||
// Update selected model if current selection is not in the new list
|
||
if (provider === formData.proProvider && models.length > 0) {
|
||
const currentModelExists = models.some(model => model.model_identifier === formData.proModel);
|
||
if (!currentModelExists) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
proModel: models[0].model_identifier
|
||
}));
|
||
}
|
||
}
|
||
|
||
if (provider === formData.conProvider && models.length > 0) {
|
||
const currentModelExists = models.some(model => model.model_identifier === formData.conModel);
|
||
if (!currentModelExists) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
conModel: models[0].model_identifier
|
||
}));
|
||
}
|
||
}
|
||
} else {
|
||
console.error('Error fetching models from backend for Qwen:', qwenResponse.status, await qwenResponse.text());
|
||
// Use fallback models if API call fails
|
||
const fallbackModels = [
|
||
{ model_identifier: 'qwen3-max', display_name: 'Qwen3 Max' },
|
||
{ model_identifier: 'qwen3-plus', display_name: 'Qwen3 Plus' },
|
||
{ model_identifier: 'qwen3-flash', display_name: 'Qwen3 Flash' },
|
||
{ model_identifier: 'qwen-max', display_name: 'Qwen Max' },
|
||
{ model_identifier: 'qwen-plus', display_name: 'Qwen Plus' },
|
||
{ model_identifier: 'qwen-turbo', display_name: 'Qwen Turbo' }
|
||
];
|
||
|
||
setAvailableModels(prev => ({
|
||
...prev,
|
||
[provider]: fallbackModels
|
||
}));
|
||
|
||
if (provider === formData.proProvider) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
proModel: fallbackModels[0].model_identifier
|
||
}));
|
||
}
|
||
|
||
if (provider === formData.conProvider) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
conModel: fallbackModels[0].model_identifier
|
||
}));
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Network error when fetching Qwen models from backend:', error);
|
||
// Use fallback models if network request fails
|
||
const fallbackModels = [
|
||
{ model_identifier: 'qwen-max', display_name: 'Qwen Max' },
|
||
{ model_identifier: 'qwen-plus', display_name: 'Qwen Plus' },
|
||
{ model_identifier: 'qwen-turbo', display_name: 'Qwen Turbo' }
|
||
];
|
||
|
||
setAvailableModels(prev => ({
|
||
...prev,
|
||
[provider]: fallbackModels
|
||
}));
|
||
|
||
if (provider === formData.proProvider) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
proModel: fallbackModels[0].model_identifier
|
||
}));
|
||
}
|
||
|
||
if (provider === formData.conProvider) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
conModel: fallbackModels[0].model_identifier
|
||
}));
|
||
}
|
||
}
|
||
} else {
|
||
// For other providers, use the backend API
|
||
const response = await fetch(`http://localhost:8000/models/${provider}`);
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
setAvailableModels(prev => ({
|
||
...prev,
|
||
[provider]: data.models || []
|
||
}));
|
||
|
||
// Update selected model if current selection is not in the new list
|
||
if (provider === formData.proProvider && data.models && data.models.length > 0) {
|
||
const currentModelExists = data.models.some(model => model.model_identifier === formData.proModel);
|
||
if (!currentModelExists) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
proModel: data.models[0].model_identifier
|
||
}));
|
||
}
|
||
}
|
||
|
||
if (provider === formData.conProvider && data.models && data.models.length > 0) {
|
||
const currentModelExists = data.models.some(model => model.model_identifier === formData.conModel);
|
||
if (!currentModelExists) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
conModel: data.models[0].model_identifier
|
||
}));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error(`Error loading models for ${provider}:`, error);
|
||
} finally {
|
||
setModelsLoading(prev => ({ ...prev, [provider]: false }));
|
||
}
|
||
};
|
||
|
||
const handleChange = (e) => {
|
||
const { name, value, type, checked } = e.target;
|
||
setFormData(prev => ({
|
||
...prev,
|
||
[name]: type === 'checkbox' ? checked : value
|
||
}));
|
||
};
|
||
|
||
|
||
const handleSubmit = async (e) => {
|
||
e.preventDefault();
|
||
setIsLoading(true);
|
||
|
||
// Validate inputs
|
||
if (!formData.topic.trim()) {
|
||
alert('请输入辩论主题');
|
||
setIsLoading(false);
|
||
return;
|
||
}
|
||
|
||
// Create debate request object
|
||
const debateRequest = {
|
||
topic: formData.topic,
|
||
participants: [
|
||
{
|
||
model_identifier: formData.proModel,
|
||
provider: formData.proProvider,
|
||
stance: "pro"
|
||
},
|
||
{
|
||
model_identifier: formData.conModel,
|
||
provider: formData.conProvider,
|
||
stance: "con"
|
||
}
|
||
],
|
||
constraints: {
|
||
max_rounds: parseInt(formData.maxRounds),
|
||
max_tokens_per_turn: parseInt(formData.maxTokens),
|
||
web_search_enabled: formData.webSearchEnabled,
|
||
web_search_mode: formData.webSearchMode
|
||
}
|
||
};
|
||
|
||
try {
|
||
const response = await fetch('http://localhost:8000/debate/create', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(debateRequest)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`创建辩论失败: ${response.statusText}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
alert(`辩论创建成功!会话ID: ${result.session_id}`);
|
||
|
||
// Pass the session ID to the parent component
|
||
onCreateDebate(result.session_id);
|
||
} catch (error) {
|
||
console.error('Error creating debate:', error);
|
||
alert(`创建辩论失败: ${error.message}`);
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<section className="section">
|
||
<h2>辩论配置</h2>
|
||
<form onSubmit={handleSubmit}>
|
||
<div className="form-group">
|
||
<label htmlFor="topic" className="label">辩论主题:</label>
|
||
<input
|
||
type="text"
|
||
id="topic"
|
||
name="topic"
|
||
className="input"
|
||
value={formData.topic}
|
||
onChange={handleChange}
|
||
required
|
||
placeholder="请输入辩论主题"
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label className="label">参与模型:</label>
|
||
<div className="model-selection">
|
||
<div className="model-item">
|
||
<select
|
||
className="select"
|
||
name="proProvider"
|
||
value={formData.proProvider}
|
||
onChange={handleChange}
|
||
>
|
||
{availableProviders.map((provider) => (
|
||
<option key={provider.provider} value={provider.provider}>
|
||
{provider.display_name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
|
||
{modelsLoading[formData.proProvider] ? (
|
||
<div className="input">加载模型中...</div>
|
||
) : (
|
||
<select
|
||
className="select"
|
||
name="proModel"
|
||
value={formData.proModel}
|
||
onChange={handleChange}
|
||
>
|
||
{availableModels[formData.proProvider]?.map((model) => (
|
||
<option key={model.model_identifier} value={model.model_identifier}>
|
||
{model.display_name || model.model_identifier}
|
||
</option>
|
||
))}
|
||
</select>
|
||
)}
|
||
|
||
<span className="stance-label">正方</span>
|
||
</div>
|
||
|
||
<div className="model-item">
|
||
<select
|
||
className="select"
|
||
name="conProvider"
|
||
value={formData.conProvider}
|
||
onChange={handleChange}
|
||
>
|
||
{availableProviders.map((provider) => (
|
||
<option key={provider.provider} value={provider.provider}>
|
||
{provider.display_name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
|
||
{modelsLoading[formData.conProvider] ? (
|
||
<div className="input">加载模型中...</div>
|
||
) : (
|
||
<select
|
||
className="select"
|
||
name="conModel"
|
||
value={formData.conModel}
|
||
onChange={handleChange}
|
||
>
|
||
{availableModels[formData.conProvider]?.map((model) => (
|
||
<option key={model.model_identifier} value={model.model_identifier}>
|
||
{model.display_name || model.model_identifier}
|
||
</option>
|
||
))}
|
||
</select>
|
||
)}
|
||
|
||
<span className="stance-label">反方</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label htmlFor="maxRounds" className="label">最大轮数:</label>
|
||
<input
|
||
type="number"
|
||
id="maxRounds"
|
||
name="maxRounds"
|
||
className="input"
|
||
min="1"
|
||
max="10"
|
||
value={formData.maxRounds}
|
||
onChange={handleChange}
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label htmlFor="maxTokens" className="label">每轮最大Token数:</label>
|
||
<input
|
||
type="number"
|
||
id="maxTokens"
|
||
name="maxTokens"
|
||
className="input"
|
||
min="100"
|
||
max="2000"
|
||
value={formData.maxTokens}
|
||
onChange={handleChange}
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label className="label checkbox-label">
|
||
<input
|
||
type="checkbox"
|
||
name="webSearchEnabled"
|
||
checked={formData.webSearchEnabled}
|
||
onChange={handleChange}
|
||
/>
|
||
启用网络搜索
|
||
</label>
|
||
{formData.webSearchEnabled && (
|
||
<div className="search-mode-selector" style={{ marginTop: '0.5rem' }}>
|
||
<label htmlFor="webSearchMode" className="label">搜索模式:</label>
|
||
<select
|
||
className="select"
|
||
name="webSearchMode"
|
||
id="webSearchMode"
|
||
value={formData.webSearchMode}
|
||
onChange={handleChange}
|
||
>
|
||
<option value="auto">自动搜索 (每轮自动检索)</option>
|
||
<option value="tool">工具调用 (模型决定是否搜索)</option>
|
||
<option value="both">两者结合</option>
|
||
</select>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
className={`button create-debate-button`}
|
||
disabled={isLoading || isGuest}
|
||
>
|
||
{isLoading ? '创建中...' : '创建辩论'}
|
||
</button>
|
||
{isGuest && (
|
||
<p style={{ color: '#e53e3e', marginTop: '0.5rem', textAlign: 'center' }}>
|
||
请先登录后再创建辩论
|
||
</p>
|
||
)}
|
||
</form>
|
||
|
||
<div style={{ marginTop: '1rem', textAlign: 'center' }}>
|
||
<button className="button view-sessions-button" onClick={onViewSessions}>
|
||
查看历史辩论
|
||
</button>
|
||
{onViewSettings && (
|
||
<button className="button view-sessions-button" style={{ marginLeft: '1rem' }} onClick={onViewSettings}>
|
||
设置 API 密钥
|
||
</button>
|
||
)}
|
||
</div>
|
||
</section>
|
||
);
|
||
};
|
||
|
||
export default DebateConfiguration; |