Files
Dialectic.Frontend/src/components/DebateConfiguration.js
2026-02-13 03:42:04 +00:00

496 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;