From 93ec1562996617dbaed57a15b8179cb4f7ba8474 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=B4=AA=E9=93=AD?= <3495516838@qq.com>
Date: Thu, 23 Apr 2026 18:03:14 +0800
Subject: [PATCH 1/2] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E6=B7=BB?=
=?UTF-8?q?=E5=8A=A0=E7=94=A8=E4=BA=8E=E8=BE=A9=E8=AE=BA=E5=88=86=E6=9E=90?=
=?UTF-8?q?=E5=92=8C=E4=BA=A4=E4=BA=92=E7=9A=84=E5=88=86=E6=9E=90=E9=9D=A2?=
=?UTF-8?q?=E6=9D=BF=EF=BC=88AnalysisPanel=EF=BC=89=E5=92=8C=E8=81=8A?=
=?UTF-8?q?=E5=A4=A9=E6=A1=86=EF=BC=88ChatBox=EF=BC=89=E7=BB=84=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现了分析面板,用于展示辩论分析结果,包括论点、分析、建议和置信度得分。
- 创建了聊天框组件,用于与人工智能辩论助手进行交互式聊天,具备消息处理和输入提交功能。
- 添加了消息组件,以规范消息和论点的显示。
- 引入辩论 API 服务,用于与后端进行通信,包括健康检查、辩论生成、分析和聊天功能。
- 配置了 Tailwind CSS 进行样式设计和自定义滚动条。
- 添加了全局样式和动画,用于加载指示器和消息过渡效果。
- 设置了 index.js 和 index.css 作为应用程序的入口点和样式文件。
---
.gitignore | 56 +++
debate-hackathon/backend/app/__init__.py | 5 +
debate-hackathon/backend/app/api/__init__.py | 7 +
debate-hackathon/backend/app/api/debate.py | 202 ++++++++
debate-hackathon/backend/app/core/__init__.py | 7 +
.../backend/app/core/llm_client.py | 254 ++++++++++
debate-hackathon/backend/app/main.py | 72 +++
.../backend/app/schemas/__init__.py | 12 +
.../backend/app/schemas/debate.py | 92 ++++
debate-hackathon/backend/requirements.txt | 10 +
debate-hackathon/frontend/package.json | 38 ++
debate-hackathon/frontend/postcss.config.js | 6 +
debate-hackathon/frontend/public/index.html | 17 +
debate-hackathon/frontend/src/App.jsx | 471 ++++++++++++++++++
.../frontend/src/components/AnalysisPanel.jsx | 112 +++++
.../frontend/src/components/ChatBox.jsx | 183 +++++++
.../frontend/src/components/Message.jsx | 129 +++++
debate-hackathon/frontend/src/index.css | 122 +++++
debate-hackathon/frontend/src/index.js | 11 +
.../frontend/src/services/debateApi.js | 99 ++++
debate-hackathon/frontend/tailwind.config.js | 31 ++
21 files changed, 1936 insertions(+)
create mode 100644 .gitignore
create mode 100644 debate-hackathon/backend/app/__init__.py
create mode 100644 debate-hackathon/backend/app/api/__init__.py
create mode 100644 debate-hackathon/backend/app/api/debate.py
create mode 100644 debate-hackathon/backend/app/core/__init__.py
create mode 100644 debate-hackathon/backend/app/core/llm_client.py
create mode 100644 debate-hackathon/backend/app/main.py
create mode 100644 debate-hackathon/backend/app/schemas/__init__.py
create mode 100644 debate-hackathon/backend/app/schemas/debate.py
create mode 100644 debate-hackathon/backend/requirements.txt
create mode 100644 debate-hackathon/frontend/package.json
create mode 100644 debate-hackathon/frontend/postcss.config.js
create mode 100644 debate-hackathon/frontend/public/index.html
create mode 100644 debate-hackathon/frontend/src/App.jsx
create mode 100644 debate-hackathon/frontend/src/components/AnalysisPanel.jsx
create mode 100644 debate-hackathon/frontend/src/components/ChatBox.jsx
create mode 100644 debate-hackathon/frontend/src/components/Message.jsx
create mode 100644 debate-hackathon/frontend/src/index.css
create mode 100644 debate-hackathon/frontend/src/index.js
create mode 100644 debate-hackathon/frontend/src/services/debateApi.js
create mode 100644 debate-hackathon/frontend/tailwind.config.js
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d6ee583
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,56 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+*.egg-info/
+dist/
+build/
+*.egg
+.eggs/
+*.whl
+
+# Virtual environments
+venv/
+env/
+.venv/
+
+# Environment files
+.env
+.env.local
+.env.*.local
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+.DS_Store
+
+# Node
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# React build
+frontend/build/
+
+# Logs
+*.log
+logs/
+
+# Database
+*.db
+*.sqlite
+*.sqlite3
+
+# OS
+Thumbs.db
+ehthumbs.db
+Desktop.ini
+
+# Docker
+.docker/
+docker-compose.override.yml
diff --git a/debate-hackathon/backend/app/__init__.py b/debate-hackathon/backend/app/__init__.py
new file mode 100644
index 0000000..ed65cdc
--- /dev/null
+++ b/debate-hackathon/backend/app/__init__.py
@@ -0,0 +1,5 @@
+"""
+Debate Hackathon Backend Application
+"""
+
+__version__ = "1.0.0"
\ No newline at end of file
diff --git a/debate-hackathon/backend/app/api/__init__.py b/debate-hackathon/backend/app/api/__init__.py
new file mode 100644
index 0000000..6f0d6ad
--- /dev/null
+++ b/debate-hackathon/backend/app/api/__init__.py
@@ -0,0 +1,7 @@
+"""
+API routes for the Debate Hackathon backend
+"""
+
+from .debate import router as debate_router
+
+__all__ = ["debate_router"]
\ No newline at end of file
diff --git a/debate-hackathon/backend/app/api/debate.py b/debate-hackathon/backend/app/api/debate.py
new file mode 100644
index 0000000..a67f2ef
--- /dev/null
+++ b/debate-hackathon/backend/app/api/debate.py
@@ -0,0 +1,202 @@
+"""
+Debate API routes
+"""
+
+from fastapi import APIRouter, HTTPException, Depends
+from typing import List, Optional, Dict, Any
+
+from app.schemas.debate import (
+ DebateTopic,
+ DebateResponse,
+ DebateArgument,
+ DebateAnalysis,
+ DebateSide,
+ ChatRequest
+)
+from app.core.llm_client import LLMClient
+
+router = APIRouter(prefix="/api/debate", tags=["debate"])
+
+
+def get_llm_client() -> LLMClient:
+ """Dependency to get LLM client instance"""
+ return LLMClient()
+
+
+@router.get("/health")
+async def health_check():
+ """Health check endpoint"""
+ return {
+ "status": "healthy",
+ "message": "Debate Hackathon API is running"
+ }
+
+
+@router.post("/generate", response_model=DebateResponse)
+async def generate_debate(
+ request: DebateTopic,
+ llm_client: LLMClient = Depends(get_llm_client)
+):
+ """
+ Generate debate arguments for a given topic and side
+
+ - **topic**: The debate topic
+ - **side**: Which side to argue for ("pro" or "con")
+ - **complexity**: Difficulty level ("easy", "medium", "hard")
+ """
+ try:
+ result = llm_client.generate_debate(
+ topic=request.topic,
+ side=request.side.value,
+ complexity=request.complexity
+ )
+
+ # Convert to DebateResponse format
+ arguments = [
+ DebateArgument(
+ argument=arg["argument"],
+ supporting_evidence=arg.get("supporting_evidence"),
+ strength=arg.get("strength", 0.5)
+ )
+ for arg in result.get("arguments", [])
+ ]
+
+ return DebateResponse(
+ topic=result.get("topic", request.topic),
+ side=request.side,
+ arguments=arguments,
+ summary=result.get("summary", "No summary available")
+ )
+
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to generate debate: {str(e)}"
+ )
+
+
+@router.post("/analyze", response_model=DebateAnalysis)
+async def analyze_debate(
+ request: DebateTopic,
+ llm_client: LLMClient = Depends(get_llm_client)
+):
+ """
+ Analyze a debate topic from both sides
+
+ - **topic**: The debate topic to analyze
+ """
+ try:
+ result = llm_client.analyze_debate(topic=request.topic)
+
+ # Convert to DebateAnalysis format
+ pro_arguments = [
+ DebateArgument(
+ argument=arg["argument"],
+ supporting_evidence=arg.get("supporting_evidence"),
+ strength=arg.get("strength", 0.5)
+ )
+ for arg in result.get("pro_arguments", [])
+ ]
+
+ con_arguments = [
+ DebateArgument(
+ argument=arg["argument"],
+ supporting_evidence=arg.get("supporting_evidence"),
+ strength=arg.get("strength", 0.5)
+ )
+ for arg in result.get("con_arguments", [])
+ ]
+
+ return DebateAnalysis(
+ topic=result.get("topic", request.topic),
+ pro_arguments=pro_arguments,
+ con_arguments=con_arguments,
+ analysis=result.get("analysis", "No analysis available"),
+ recommendation=result.get("recommendation"),
+ confidence_score=result.get("confidence_score", 0.5)
+ )
+
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to analyze debate: {str(e)}"
+ )
+
+
+@router.post("/chat")
+async def chat_with_ai(
+ request: ChatRequest,
+ llm_client: LLMClient = Depends(get_llm_client)
+):
+ """
+ Chat with AI debate assistant
+
+ - **message**: User's message
+ - **history**: Optional chat history
+ """
+ try:
+ response = llm_client.chat(message=request.message, history=request.history)
+ return {"response": response}
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Chat failed: {str(e)}"
+ )
+
+
+@router.get("/topics")
+async def get_sample_topics():
+ """Get a list of sample debate topics"""
+ topics = [
+ {
+ "id": 1,
+ "topic": "Artificial intelligence will benefit humanity more than harm it",
+ "category": "Technology"
+ },
+ {
+ "id": 2,
+ "topic": "Climate change is the most pressing issue of our time",
+ "category": "Environment"
+ },
+ {
+ "id": 3,
+ "topic": "Social media does more harm than good to society",
+ "category": "Society"
+ },
+ {
+ "id": 4,
+ "topic": "Universal Basic Income should be implemented globally",
+ "category": "Economics"
+ },
+ {
+ "id": 5,
+ "topic": "Space exploration is worth the investment",
+ "category": "Science"
+ },
+ {
+ "id": 6,
+ "topic": "Remote work is better than office work for productivity",
+ "category": "Work"
+ },
+ {
+ "id": 7,
+ "topic": "Genetic engineering in humans should be allowed",
+ "category": "Bioethics"
+ },
+ {
+ "id": 8,
+ "topic": "Nuclear energy is the best solution for clean energy",
+ "category": "Energy"
+ },
+ {
+ "id": 9,
+ "topic": "College education is essential for success in today's world",
+ "category": "Education"
+ },
+ {
+ "id": 10,
+ "topic": "Cryptocurrency will replace traditional banking systems",
+ "category": "Finance"
+ }
+ ]
+ return {"topics": topics}
\ No newline at end of file
diff --git a/debate-hackathon/backend/app/core/__init__.py b/debate-hackathon/backend/app/core/__init__.py
new file mode 100644
index 0000000..3a25fa0
--- /dev/null
+++ b/debate-hackathon/backend/app/core/__init__.py
@@ -0,0 +1,7 @@
+"""
+Core modules for the Debate Hackathon backend
+"""
+
+from .llm_client import LLMClient
+
+__all__ = ["LLMClient"]
\ No newline at end of file
diff --git a/debate-hackathon/backend/app/core/llm_client.py b/debate-hackathon/backend/app/core/llm_client.py
new file mode 100644
index 0000000..06182b2
--- /dev/null
+++ b/debate-hackathon/backend/app/core/llm_client.py
@@ -0,0 +1,254 @@
+"""
+LLM Client for generating debate arguments
+Supports OpenAI and compatible APIs (DeepSeek, etc.)
+"""
+
+import os
+import json
+from typing import List, Optional, Dict, Any
+from openai import OpenAI
+from dotenv import load_dotenv
+
+load_dotenv()
+
+
+class LLMClient:
+ """Client for interacting with LLM APIs to generate debate content"""
+
+ def __init__(self):
+ self.api_key = os.getenv("OPENAI_API_KEY", "")
+ self.base_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
+ self.model = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo")
+
+ # Try DeepSeek as fallback
+ if not self.api_key or self.api_key == "your_openai_api_key_here":
+ self.api_key = os.getenv("DEEPSEEK_API_KEY", "")
+ self.base_url = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
+ self.model = "deepseek-chat"
+
+ self.client = OpenAI(
+ api_key=self.api_key,
+ base_url=self.base_url
+ )
+
+ def _build_debate_prompt(
+ self,
+ topic: str,
+ side: str,
+ complexity: str = "medium"
+ ) -> str:
+ """Build the prompt for debate argument generation"""
+
+ complexity_instructions = {
+ "easy": "Provide 2-3 simple, clear arguments suitable for beginners.",
+ "medium": "Provide 3-4 well-structured arguments with moderate depth.",
+ "hard": "Provide 4-5 sophisticated arguments with deep analysis and counter-arguments."
+ }
+
+ instruction = complexity_instructions.get(complexity, complexity_instructions["medium"])
+
+ prompt = f"""You are an expert debater arguing for the "{side}" side of the following topic:
+
+Topic: {topic}
+
+{instruction}
+
+For each argument, provide:
+1. The main argument point
+2. Supporting evidence or facts (if applicable)
+3. A strength score (0.0 to 1.0)
+
+Also provide a concise summary of your overall position.
+
+Format your response as a JSON object with the following structure:
+{{
+ "arguments": [
+ {{
+ "argument": "Main argument text",
+ "supporting_evidence": ["Evidence 1", "Evidence 2"],
+ "strength": 0.85
+ }}
+ ],
+ "summary": "Overall summary of the position"
+}}
+
+Ensure the response is valid JSON and arguments are persuasive and well-reasoned."""
+
+ return prompt
+
+ def _build_analysis_prompt(self, topic: str) -> str:
+ """Build the prompt for debate analysis"""
+
+ prompt = f"""You are an expert debate analyst. Analyze the following debate topic from both sides:
+
+Topic: {topic}
+
+Provide:
+1. Arguments for the PRO side (supporting the topic)
+2. Arguments for the CON side (opposing the topic)
+3. A comparative analysis
+4. A recommendation on which side has stronger arguments
+5. A confidence score (0.0 to 1.0)
+
+Format your response as a JSON object with the following structure:
+{{
+ "pro_arguments": [
+ {{
+ "argument": "Pro argument text",
+ "supporting_evidence": ["Evidence 1", "Evidence 2"],
+ "strength": 0.85
+ }}
+ ],
+ "con_arguments": [
+ {{
+ "argument": "Con argument text",
+ "supporting_evidence": ["Evidence 1", "Evidence 2"],
+ "strength": 0.75
+ }}
+ ],
+ "analysis": "Comparative analysis text",
+ "recommendation": "Recommended position",
+ "confidence_score": 0.7
+}}
+
+Ensure the response is valid JSON and the analysis is balanced and insightful."""
+
+ return prompt
+
+ def generate_debate(
+ self,
+ topic: str,
+ side: str,
+ complexity: str = "medium"
+ ) -> Dict[str, Any]:
+ """Generate debate arguments for a given topic and side"""
+
+ prompt = self._build_debate_prompt(topic, side, complexity)
+
+ try:
+ response = self.client.chat.completions.create(
+ model=self.model,
+ messages=[
+ {"role": "system", "content": "You are an expert debate assistant. Always respond with valid JSON."},
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.7,
+ max_tokens=2000
+ )
+
+ content = response.choices[0].message.content
+
+ # Try to parse JSON from the response
+ # Handle cases where the response might have markdown code blocks
+ if "```json" in content:
+ content = content.split("```json")[1].split("```")[0].strip()
+ elif "```" in content:
+ content = content.split("```")[1].split("```")[0].strip()
+
+ result = json.loads(content)
+
+ # Add metadata
+ result["topic"] = topic
+ result["side"] = side
+
+ return result
+
+ except json.JSONDecodeError as e:
+ return {
+ "topic": topic,
+ "side": side,
+ "arguments": [
+ {
+ "argument": f"Error parsing AI response: {str(e)}",
+ "supporting_evidence": None,
+ "strength": 0.0
+ }
+ ],
+ "summary": "Failed to generate valid debate arguments. Please try again."
+ }
+ except Exception as e:
+ return {
+ "topic": topic,
+ "side": side,
+ "arguments": [
+ {
+ "argument": f"Error generating debate: {str(e)}",
+ "supporting_evidence": None,
+ "strength": 0.0
+ }
+ ],
+ "summary": "An error occurred while generating the debate. Please check your API configuration."
+ }
+
+ def analyze_debate(self, topic: str) -> Dict[str, Any]:
+ """Analyze a debate topic from both sides"""
+
+ prompt = self._build_analysis_prompt(topic)
+
+ try:
+ response = self.client.chat.completions.create(
+ model=self.model,
+ messages=[
+ {"role": "system", "content": "You are an expert debate analyst. Always respond with valid JSON."},
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.7,
+ max_tokens=2000
+ )
+
+ content = response.choices[0].message.content
+
+ # Handle markdown code blocks
+ if "```json" in content:
+ content = content.split("```json")[1].split("```")[0].strip()
+ elif "```" in content:
+ content = content.split("```")[1].split("```")[0].strip()
+
+ result = json.loads(content)
+ result["topic"] = topic
+
+ return result
+
+ except json.JSONDecodeError as e:
+ return {
+ "topic": topic,
+ "pro_arguments": [],
+ "con_arguments": [],
+ "analysis": f"Error parsing analysis response: {str(e)}",
+ "recommendation": None,
+ "confidence_score": 0.0
+ }
+ except Exception as e:
+ return {
+ "topic": topic,
+ "pro_arguments": [],
+ "con_arguments": [],
+ "analysis": f"An error occurred: {str(e)}",
+ "recommendation": None,
+ "confidence_score": 0.0
+ }
+
+ def chat(self, message: str, history: Optional[List[Dict]] = None) -> str:
+ """Chat with the LLM for interactive debate"""
+
+ messages = [
+ {"role": "system", "content": "You are a helpful debate assistant. Help users refine their arguments and think critically about debate topics."}
+ ]
+
+ if history:
+ messages.extend(history)
+
+ messages.append({"role": "user", "content": message})
+
+ try:
+ response = self.client.chat.completions.create(
+ model=self.model,
+ messages=messages,
+ temperature=0.7,
+ max_tokens=1000
+ )
+
+ return response.choices[0].message.content
+
+ except Exception as e:
+ return f"Error: {str(e)}"
\ No newline at end of file
diff --git a/debate-hackathon/backend/app/main.py b/debate-hackathon/backend/app/main.py
new file mode 100644
index 0000000..d0f7b1d
--- /dev/null
+++ b/debate-hackathon/backend/app/main.py
@@ -0,0 +1,72 @@
+"""
+Debate Hackathon Backend - Main Application Entry Point
+"""
+
+import os
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from dotenv import load_dotenv
+
+from app.api import debate_router
+
+load_dotenv()
+
+# Application configuration
+APP_NAME = os.getenv("APP_NAME", "DebateHackathon")
+APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
+DEBUG = os.getenv("DEBUG", "True").lower() == "true"
+ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000").split(",")
+
+app = FastAPI(
+ title=APP_NAME,
+ version=APP_VERSION,
+ description="Backend API for the Debate Hackathon project",
+ docs_url="/docs" if DEBUG else None,
+ redoc_url="/redoc" if DEBUG else None,
+)
+
+# Configure CORS
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=ALLOWED_ORIGINS,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Register routers
+app.include_router(debate_router)
+
+
+@app.get("/")
+async def root():
+ """Root endpoint"""
+ return {
+ "app": APP_NAME,
+ "version": APP_VERSION,
+ "status": "running",
+ "docs": "/docs" if DEBUG else None
+ }
+
+
+@app.get("/api/health")
+async def api_health():
+ """API health check"""
+ return {
+ "status": "healthy",
+ "timestamp": __import__("datetime").datetime.now().isoformat()
+ }
+
+
+if __name__ == "__main__":
+ import uvicorn
+
+ host = os.getenv("HOST", "0.0.0.0")
+ port = int(os.getenv("PORT", "8000"))
+
+ uvicorn.run(
+ "app.main:app",
+ host=host,
+ port=port,
+ reload=DEBUG
+ )
\ No newline at end of file
diff --git a/debate-hackathon/backend/app/schemas/__init__.py b/debate-hackathon/backend/app/schemas/__init__.py
new file mode 100644
index 0000000..c3659be
--- /dev/null
+++ b/debate-hackathon/backend/app/schemas/__init__.py
@@ -0,0 +1,12 @@
+"""
+Pydantic schemas for the Debate Hackathon API
+"""
+
+from .debate import DebateTopic, DebateArgument, DebateResponse, DebateAnalysis
+
+__all__ = [
+ "DebateTopic",
+ "DebateArgument",
+ "DebateResponse",
+ "DebateAnalysis"
+]
\ No newline at end of file
diff --git a/debate-hackathon/backend/app/schemas/debate.py b/debate-hackathon/backend/app/schemas/debate.py
new file mode 100644
index 0000000..e059d1e
--- /dev/null
+++ b/debate-hackathon/backend/app/schemas/debate.py
@@ -0,0 +1,92 @@
+"""
+Debate-related Pydantic schemas
+"""
+
+from pydantic import BaseModel, Field
+from typing import List, Optional, Literal, Dict
+from datetime import datetime
+from enum import Enum
+
+
+class DebateSide(str, Enum):
+ """Debate sides"""
+ PRO = "pro"
+ CON = "con"
+
+
+class DebateTopic(BaseModel):
+ """Schema for debate topic request"""
+ topic: str = Field(..., description="The debate topic")
+ side: DebateSide = Field(..., description="Which side to argue for")
+ complexity: Literal["easy", "medium", "hard"] = Field(
+ default="medium",
+ description="Complexity level of the debate"
+ )
+
+ class Config:
+ schema_extra = {
+ "example": {
+ "topic": "Artificial intelligence will benefit humanity more than harm it",
+ "side": "pro",
+ "complexity": "medium"
+ }
+ }
+
+
+class DebateArgument(BaseModel):
+ """Schema for a single debate argument"""
+ argument: str = Field(..., description="The argument text")
+ supporting_evidence: Optional[List[str]] = Field(
+ default=None,
+ description="Supporting evidence or facts"
+ )
+ strength: float = Field(
+ default=0.5,
+ ge=0.0,
+ le=1.0,
+ description="Argument strength score (0.0 to 1.0)"
+ )
+
+
+class DebateResponse(BaseModel):
+ """Schema for debate response"""
+ topic: str = Field(..., description="The debate topic")
+ side: DebateSide = Field(..., description="Which side was argued")
+ arguments: List[DebateArgument] = Field(..., description="List of arguments")
+ summary: str = Field(..., description="Summary of the debate position")
+ generated_at: datetime = Field(default_factory=datetime.now)
+
+
+class ChatRequest(BaseModel):
+ """Schema for chat request"""
+ message: str = Field(..., description="User's message")
+ history: Optional[List[Dict[str, str]]] = Field(
+ default=None,
+ description="Optional chat history"
+ )
+
+ class Config:
+ schema_extra = {
+ "example": {
+ "message": "What are the main arguments for universal basic income?",
+ "history": [
+ {"role": "user", "content": "Hello"},
+ {"role": "assistant", "content": "How can I help you with your debate?"}
+ ]
+ }
+ }
+
+
+class DebateAnalysis(BaseModel):
+ """Schema for debate analysis"""
+ topic: str = Field(..., description="The debate topic")
+ pro_arguments: List[DebateArgument] = Field(..., description="Pro side arguments")
+ con_arguments: List[DebateArgument] = Field(..., description="Con side arguments")
+ analysis: str = Field(..., description="Comparative analysis")
+ recommendation: Optional[str] = Field(None, description="Recommended position")
+ confidence_score: float = Field(
+ default=0.5,
+ ge=0.0,
+ le=1.0,
+ description="Confidence in the analysis (0.0 to 1.0)"
+ )
diff --git a/debate-hackathon/backend/requirements.txt b/debate-hackathon/backend/requirements.txt
new file mode 100644
index 0000000..6b31b6f
--- /dev/null
+++ b/debate-hackathon/backend/requirements.txt
@@ -0,0 +1,10 @@
+fastapi==0.104.1
+uvicorn[standard]==0.24.0
+python-dotenv==1.0.0
+pydantic==1.10.13
+httpx==0.25.2
+openai==1.3.0
+python-multipart==0.0.6
+# Optional: uncomment if you need database support
+# sqlalchemy==2.0.23
+# alembic==1.12.1
\ No newline at end of file
diff --git a/debate-hackathon/frontend/package.json b/debate-hackathon/frontend/package.json
new file mode 100644
index 0000000..3e13d9e
--- /dev/null
+++ b/debate-hackathon/frontend/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "debate-hackathon-frontend",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Frontend for the Debate Hackathon project",
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "dependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-scripts": "5.0.1",
+ "axios": "^1.6.0",
+ "react-markdown": "^9.0.1",
+ "react-icons": "^4.12.0",
+ "web-vitals": "^2.1.4"
+ },
+ "devDependencies": {
+ "tailwindcss": "^3.3.5",
+ "autoprefixer": "^10.4.16",
+ "postcss": "^8.4.31"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/debate-hackathon/frontend/postcss.config.js b/debate-hackathon/frontend/postcss.config.js
new file mode 100644
index 0000000..33ad091
--- /dev/null
+++ b/debate-hackathon/frontend/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/debate-hackathon/frontend/public/index.html b/debate-hackathon/frontend/public/index.html
new file mode 100644
index 0000000..dda74da
--- /dev/null
+++ b/debate-hackathon/frontend/public/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+ 辩论黑客松 - Debate Hackathon
+
+
+
+
+
+
diff --git a/debate-hackathon/frontend/src/App.jsx b/debate-hackathon/frontend/src/App.jsx
new file mode 100644
index 0000000..6913620
--- /dev/null
+++ b/debate-hackathon/frontend/src/App.jsx
@@ -0,0 +1,471 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import ChatBox from './components/ChatBox';
+import AnalysisPanel from './components/AnalysisPanel';
+import { generateDebate, analyzeDebate, chatWithAI, getSampleTopics, healthCheck } from './services/debateApi';
+
+/**
+ * Main App component for the Debate Hackathon
+ */
+function App() {
+ // State
+ const [activeTab, setActiveTab] = useState('generate');
+ const [topic, setTopic] = useState('');
+ const [side, setSide] = useState('pro');
+ const [complexity, setComplexity] = useState('medium');
+ const [sampleTopics, setSampleTopics] = useState([]);
+ const [debateResult, setDebateResult] = useState(null);
+ const [analysisResult, setAnalysisResult] = useState(null);
+ const [chatMessages, setChatMessages] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isAnalyzing, setIsAnalyzing] = useState(false);
+ const [isChatLoading, setIsChatLoading] = useState(false);
+ const [apiStatus, setApiStatus] = useState('checking');
+ const [error, setError] = useState(null);
+
+ // Load sample topics on mount
+ useEffect(() => {
+ const init = async () => {
+ try {
+ const health = await healthCheck();
+ setApiStatus(health.status === 'healthy' ? 'connected' : 'error');
+ } catch {
+ setApiStatus('disconnected');
+ }
+
+ try {
+ const data = await getSampleTopics();
+ setSampleTopics(data.topics || []);
+ } catch {
+ // Topics are optional, ignore error
+ }
+ };
+ init();
+ }, []);
+
+ // Handle debate generation - auto switch to generate tab
+ const handleGenerate = useCallback(async () => {
+ if (!topic.trim()) return;
+
+ setIsLoading(true);
+ setError(null);
+ setDebateResult(null);
+ setActiveTab('generate');
+
+ try {
+ const result = await generateDebate(topic.trim(), side, complexity);
+ setDebateResult(result);
+ } catch (err) {
+ setError(err.response?.data?.detail || '生成辩论失败,请检查API配置');
+ } finally {
+ setIsLoading(false);
+ }
+ }, [topic, side, complexity]);
+
+ // Handle analysis - auto switch to analyze tab
+ const handleAnalyze = useCallback(async () => {
+ if (!topic.trim()) return;
+
+ setIsAnalyzing(true);
+ setError(null);
+ setAnalysisResult(null);
+ setActiveTab('analyze');
+
+ try {
+ const result = await analyzeDebate(topic.trim());
+ setAnalysisResult(result);
+ } catch (err) {
+ setError(err.response?.data?.detail || '分析辩论失败,请检查API配置');
+ } finally {
+ setIsAnalyzing(false);
+ }
+ }, [topic]);
+
+ // Handle chat message
+ const handleChatMessage = useCallback(async (message) => {
+ const userMessage = {
+ type: 'user',
+ text: message,
+ side: 'neutral'
+ };
+
+ setChatMessages(prev => [...prev, userMessage]);
+ setIsChatLoading(true);
+
+ try {
+ const history = chatMessages.map(msg => ({
+ role: msg.type === 'user' ? 'user' : 'assistant',
+ content: msg.text || msg.argument || ''
+ }));
+
+ const result = await chatWithAI(message, history);
+
+ const aiMessage = {
+ type: 'ai',
+ text: result.response,
+ side: 'neutral'
+ };
+
+ setChatMessages(prev => [...prev, aiMessage]);
+ } catch (err) {
+ const errorMessage = {
+ type: 'ai',
+ text: `错误: ${err.response?.data?.detail || '聊天失败,请检查API配置'}`,
+ side: 'neutral'
+ };
+ setChatMessages(prev => [...prev, errorMessage]);
+ } finally {
+ setIsChatLoading(false);
+ }
+ }, [chatMessages]);
+
+ // Handle topic selection from sample topics
+ const handleSelectTopic = (selectedTopic) => {
+ setTopic(selectedTopic.topic);
+ };
+
+ // Render API status indicator
+ const renderApiStatus = () => {
+ const statusConfig = {
+ connected: { color: 'bg-green-500', text: 'API 已连接' },
+ disconnected: { color: 'bg-red-500', text: 'API 未连接' },
+ checking: { color: 'bg-yellow-500', text: '检查中...' },
+ error: { color: 'bg-red-500', text: 'API 错误' }
+ };
+
+ const config = statusConfig[apiStatus] || statusConfig.checking;
+
+ return (
+
+
+ {config.text}
+
+ );
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
AI 辩论助手
+
智能辩论分析与对话平台
+
+
+ {renderApiStatus()}
+
+
+
+
+
+ {/* Error display */}
+ {error && (
+
+
+
+
{error}
+
+
+
+ )}
+
+ {/* Input section */}
+
+
+ {/* Topic input */}
+
+
+ setTopic(e.target.value)}
+ placeholder="输入你想辩论的话题,例如:人工智能对人类有益吗?"
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
+ onKeyDown={(e) => e.key === 'Enter' && handleGenerate()}
+ />
+
+
+ {/* Side selection */}
+
+
+
+
+
+
+
+
+ {/* Complexity selection */}
+
+
+
+
+
+
+ {/* Action buttons */}
+
+
+
+
+
+ {/* Sample topics */}
+ {sampleTopics.length > 0 && (
+
+
💡 试试这些话题:
+
+ {sampleTopics.map((t) => (
+
+ ))}
+
+
+ )}
+
+
+ {/* Tabs */}
+
+
+
+
+
+
+ {activeTab === 'generate' && (
+
+ {isLoading ? (
+
+
+
+
+
+
+
+
AI 正在思考并生成辩论论点...
+
这可能需要几秒钟
+
+
+ ) : debateResult ? (
+
+
+
+
+
📋 辩论话题
+
{debateResult.topic}
+
+
+ {debateResult.side === 'pro' ? '👍 正方' : '👎 反方'}
+
+
+
+
+
+ 📌 论点列表
+ (共 {debateResult.arguments?.length || 0} 个论点)
+
+ {debateResult.arguments?.map((arg, index) => (
+
+
+
+
+ {index + 1}
+
+
+
{arg.argument}
+ {arg.supporting_evidence && arg.supporting_evidence.length > 0 && (
+
+
📎 支持证据:
+
+ {arg.supporting_evidence.map((evidence, i) => (
+ - {evidence}
+ ))}
+
+
+ )}
+ {arg.strength !== undefined && (
+
+
+ 论证强度
+ {Math.round(arg.strength * 100)}%
+
+
+
= 0.7 ? 'bg-green-500' : arg.strength >= 0.4 ? 'bg-yellow-500' : 'bg-red-500'
+ }`}
+ style={{ width: `${Math.round(arg.strength * 100)}%` }}
+ />
+
+
+ )}
+
+
+ ))}
+
+ {debateResult.summary && (
+
+
📝 总结
+
{debateResult.summary}
+
+ )}
+
+ ) : (
+
+
+
🎯
+
输入话题开始辩论
+
在上方输入话题,选择立场和深度,点击"生成辩论"
+
💡 也可以点击示例话题快速开始
+
+
+ )}
+
+ )}
+
+ {activeTab === 'analyze' && (
+
+ )}
+
+ {activeTab === 'chat' && (
+
+
+
+ )}
+
+
+
+
+ {/* Footer */}
+
+
+ );
+}
+
+export default App;
diff --git a/debate-hackathon/frontend/src/components/AnalysisPanel.jsx b/debate-hackathon/frontend/src/components/AnalysisPanel.jsx
new file mode 100644
index 0000000..e1c9fe3
--- /dev/null
+++ b/debate-hackathon/frontend/src/components/AnalysisPanel.jsx
@@ -0,0 +1,112 @@
+import React from 'react';
+import Message from './Message';
+
+/**
+ * AnalysisPanel component for displaying debate analysis results
+ */
+const AnalysisPanel = ({ analysis, isLoading = false }) => {
+ if (isLoading) {
+ return (
+
+
+
+
+
+
+
+
AI 正在分析辩论话题...
+
正在从正反双方角度进行分析
+
+
+ );
+ }
+
+ if (!analysis) {
+ return (
+
+
+
📊
+
选择一个话题进行分析
+
AI 将分析正反双方的论点并给出建议
+
💡 在上方输入话题后点击"分析辩论"
+
+
+ );
+ }
+
+ const confidencePercentage = Math.round((analysis.confidence_score || 0) * 100);
+
+ return (
+
+ {/* Topic header */}
+
+
📋 分析话题
+
{analysis.topic}
+
+
+ {/* Pro arguments */}
+ {analysis.pro_arguments && analysis.pro_arguments.length > 0 && (
+
+
+
+ 正方论点({analysis.pro_arguments.length} 个)
+
+ {analysis.pro_arguments.map((arg, index) => (
+
+ ))}
+
+ )}
+
+ {/* Con arguments */}
+ {analysis.con_arguments && analysis.con_arguments.length > 0 && (
+
+
+
+ 反方论点({analysis.con_arguments.length} 个)
+
+ {analysis.con_arguments.map((arg, index) => (
+
+ ))}
+
+ )}
+
+ {/* Analysis */}
+ {analysis.analysis && (
+
+
📊 综合分析
+
{analysis.analysis}
+
+ )}
+
+ {/* Recommendation */}
+ {analysis.recommendation && (
+
+
💡 建议立场
+
{analysis.recommendation}
+
+ )}
+
+ {/* Confidence score */}
+
+
+ 分析置信度
+ {confidencePercentage}%
+
+
+
= 70
+ ? 'bg-green-500'
+ : confidencePercentage >= 40
+ ? 'bg-yellow-500'
+ : 'bg-red-500'
+ }`}
+ style={{ width: `${confidencePercentage}%` }}
+ />
+
+
+
+ );
+};
+
+export default AnalysisPanel;
diff --git a/debate-hackathon/frontend/src/components/ChatBox.jsx b/debate-hackathon/frontend/src/components/ChatBox.jsx
new file mode 100644
index 0000000..940920b
--- /dev/null
+++ b/debate-hackathon/frontend/src/components/ChatBox.jsx
@@ -0,0 +1,183 @@
+import React, { useState, useRef, useEffect } from 'react';
+import Message from './Message';
+
+/**
+ * ChatBox component for interactive debate chat
+ */
+const ChatBox = ({ onSendMessage, messages = [], isLoading = false }) => {
+ const [input, setInput] = useState('');
+ const messagesEndRef = useRef(null);
+
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ };
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [messages]);
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ if (input.trim() && !isLoading) {
+ onSendMessage(input.trim());
+ setInput('');
+ }
+ };
+
+ const handleKeyDown = (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ handleSubmit(e);
+ }
+ };
+
+ return (
+
+ {/* Chat header */}
+
+
+
+
+
AI 辩论助手
+
随时为你解答辩论相关问题
+
+
+
+
+ {/* Messages area */}
+
+ {messages.length === 0 ? (
+
+
+
💬
+
开始对话吧!
+
输入你的论点或问题,AI 将与你进行辩论
+
+
+
+
+
+
+
+ ) : (
+ messages.map((msg, index) => (
+
+ {msg.type === 'ai' && (
+
+ )}
+
+
+
+ {msg.type === 'user' && (
+
+ )}
+
+ ))
+ )}
+
+ {/* Loading indicator */}
+ {isLoading && (
+
+ )}
+
+
+
+
+ {/* Input area */}
+
+
+ );
+};
+
+export default ChatBox;
diff --git a/debate-hackathon/frontend/src/components/Message.jsx b/debate-hackathon/frontend/src/components/Message.jsx
new file mode 100644
index 0000000..7d3b719
--- /dev/null
+++ b/debate-hackathon/frontend/src/components/Message.jsx
@@ -0,0 +1,129 @@
+import React from 'react';
+
+/**
+ * Message component for displaying debate arguments and chat messages
+ */
+const Message = ({ message, type = 'argument', side = 'neutral' }) => {
+ const getTypeStyles = () => {
+ switch (type) {
+ case 'user':
+ return 'bg-blue-500 text-white rounded-2xl rounded-br-sm';
+ case 'ai':
+ return 'bg-white border border-gray-200 rounded-2xl rounded-bl-sm shadow-sm';
+ case 'argument':
+ return side === 'pro'
+ ? 'bg-green-50 border border-green-200 rounded-xl'
+ : side === 'con'
+ ? 'bg-red-50 border border-red-200 rounded-xl'
+ : 'bg-gray-50 border border-gray-200 rounded-xl';
+ case 'summary':
+ return 'bg-gradient-to-r from-indigo-50 to-purple-50 border border-indigo-200 rounded-xl';
+ case 'analysis':
+ return 'bg-purple-50 border border-purple-200 rounded-xl';
+ default:
+ return 'bg-white border border-gray-200 rounded-xl';
+ }
+ };
+
+ const getSideLabel = () => {
+ switch (side) {
+ case 'pro':
+ return (
+
+ 👍 正方
+
+ );
+ case 'con':
+ return (
+
+ 👎 反方
+
+ );
+ default:
+ return null;
+ }
+ };
+
+ const getStrengthBar = (strength) => {
+ if (strength === undefined || strength === null) return null;
+
+ const percentage = Math.round(strength * 100);
+ const color = percentage >= 70
+ ? 'bg-green-500'
+ : percentage >= 40
+ ? 'bg-yellow-500'
+ : 'bg-red-500';
+
+ return (
+
+
+ 论证强度
+ {percentage}%
+
+
+
+ );
+ };
+
+ const content = message.argument || message.text || message;
+
+ // For chat messages (user/ai), render as simple text
+ if (type === 'user' || type === 'ai') {
+ return (
+
+ );
+ }
+
+ // For argument/analysis/summary types, render with full details
+ return (
+
+
+
+ {getSideLabel()}
+ {type === 'summary' && (
+
+ 📝 总结
+
+ )}
+ {type === 'analysis' && (
+
+ 📊 分析
+
+ )}
+
+
+
+
+ {content}
+
+
+ {message.supporting_evidence && message.supporting_evidence.length > 0 && (
+
+
📎 支持证据:
+
+ {message.supporting_evidence.map((evidence, index) => (
+ -
+ {evidence}
+
+ ))}
+
+
+ )}
+
+ {getStrengthBar(message.strength)}
+
+ );
+};
+
+export default Message;
diff --git a/debate-hackathon/frontend/src/index.css b/debate-hackathon/frontend/src/index.css
new file mode 100644
index 0000000..17d8d5e
--- /dev/null
+++ b/debate-hackathon/frontend/src/index.css
@@ -0,0 +1,122 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
+
+/* Custom scrollbar styles */
+::-webkit-scrollbar {
+ width: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #888;
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #555;
+}
+
+/* Animation for loading dots */
+@keyframes dot-pulse {
+ 0%, 80%, 100% {
+ transform: scale(0);
+ }
+ 40% {
+ transform: scale(1);
+ }
+}
+
+.loading-dot {
+ animation: dot-pulse 1.4s infinite ease-in-out both;
+}
+
+.loading-dot:nth-child(1) {
+ animation-delay: -0.32s;
+}
+
+.loading-dot:nth-child(2) {
+ animation-delay: -0.16s;
+}
+
+.loading-dot:nth-child(3) {
+ animation-delay: 0s;
+}
+
+/* Fade in animation */
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.fade-in {
+ animation: fadeIn 0.3s ease-out;
+}
+
+/* Slide down animation for error messages */
+@keyframes slideDown {
+ from {
+ opacity: 0;
+ transform: translateY(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.animate-slide-down {
+ animation: slideDown 0.3s ease-out;
+}
+
+/* Spin animation for loading */
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.animate-spin {
+ animation: spin 1s linear infinite;
+}
+
+/* Pulse animation for API status */
+@keyframes pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+.animate-pulse {
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+}
diff --git a/debate-hackathon/frontend/src/index.js b/debate-hackathon/frontend/src/index.js
new file mode 100644
index 0000000..2cb1087
--- /dev/null
+++ b/debate-hackathon/frontend/src/index.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/debate-hackathon/frontend/src/services/debateApi.js b/debate-hackathon/frontend/src/services/debateApi.js
new file mode 100644
index 0000000..46a6ce9
--- /dev/null
+++ b/debate-hackathon/frontend/src/services/debateApi.js
@@ -0,0 +1,99 @@
+/**
+ * Debate API service
+ * Handles all communication with the backend API
+ */
+
+import axios from 'axios';
+
+const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
+
+const api = axios.create({
+ baseURL: API_BASE_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ timeout: 30000, // 30 seconds timeout for LLM requests
+});
+
+/**
+ * Health check
+ */
+export const healthCheck = async () => {
+ try {
+ const response = await api.get('/api/health');
+ return response.data;
+ } catch (error) {
+ console.error('Health check failed:', error);
+ throw error;
+ }
+};
+
+/**
+ * Generate debate arguments for a given topic and side
+ * @param {string} topic - The debate topic
+ * @param {string} side - "pro" or "con"
+ * @param {string} complexity - "easy", "medium", or "hard"
+ */
+export const generateDebate = async (topic, side, complexity = 'medium') => {
+ try {
+ const response = await api.post('/api/debate/generate', {
+ topic,
+ side,
+ complexity,
+ });
+ return response.data;
+ } catch (error) {
+ console.error('Failed to generate debate:', error);
+ throw error;
+ }
+};
+
+/**
+ * Analyze a debate topic from both sides
+ * @param {string} topic - The debate topic to analyze
+ */
+export const analyzeDebate = async (topic) => {
+ try {
+ const response = await api.post('/api/debate/analyze', {
+ topic,
+ side: 'pro', // side is required but not used for analysis
+ });
+ return response.data;
+ } catch (error) {
+ console.error('Failed to analyze debate:', error);
+ throw error;
+ }
+};
+
+/**
+ * Chat with AI debate assistant
+ * @param {string} message - User's message
+ * @param {Array} history - Optional chat history
+ */
+export const chatWithAI = async (message, history = []) => {
+ try {
+ const response = await api.post('/api/debate/chat', {
+ message,
+ history,
+ });
+ return response.data;
+ } catch (error) {
+ console.error('Chat failed:', error);
+ throw error;
+ }
+};
+
+/**
+ * Get sample debate topics
+ */
+export const getSampleTopics = async () => {
+ try {
+ const response = await api.get('/api/debate/topics');
+ return response.data;
+ } catch (error) {
+ console.error('Failed to get topics:', error);
+ throw error;
+ }
+};
+
+export default api;
diff --git a/debate-hackathon/frontend/tailwind.config.js b/debate-hackathon/frontend/tailwind.config.js
new file mode 100644
index 0000000..a486f6a
--- /dev/null
+++ b/debate-hackathon/frontend/tailwind.config.js
@@ -0,0 +1,31 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ "./src/**/*.{js,jsx,ts,tsx}",
+ "./public/index.html"
+ ],
+ theme: {
+ extend: {
+ colors: {
+ primary: {
+ 50: '#eff6ff',
+ 100: '#dbeafe',
+ 200: '#bfdbfe',
+ 300: '#93c5fd',
+ 400: '#60a5fa',
+ 500: '#3b82f6',
+ 600: '#2563eb',
+ 700: '#1d4ed8',
+ 800: '#1e40af',
+ 900: '#1e3a8a',
+ },
+ debate: {
+ pro: '#10b981',
+ con: '#ef4444',
+ neutral: '#6b7280',
+ }
+ }
+ },
+ },
+ plugins: [],
+}
--
Gitee
From eb73395b043698ec1d6f41023d2abd336508f473 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=B4=AA=E9=93=AD?= <3495516838@qq.com>
Date: Thu, 23 Apr 2026 18:41:24 +0800
Subject: [PATCH 2/2] =?UTF-8?q?=E5=AE=8C=E7=BB=93?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 173 ++++++++--
debate-hackathon/backend/app/api/debate.py | 62 +++-
.../backend/app/core/llm_client.py | 60 +++-
debate-hackathon/frontend/src/App.jsx | 300 ++++++++++++++----
.../frontend/src/components/AnalysisPanel.jsx | 38 +--
.../frontend/src/components/ChatBox.jsx | 26 +-
.../frontend/src/components/HistoryPanel.jsx | 244 ++++++++++++++
.../frontend/src/components/ImageUploader.jsx | 190 +++++++++++
.../frontend/src/components/Message.jsx | 36 +--
.../frontend/src/services/debateApi.js | 102 +++++-
debate-hackathon/frontend/tailwind.config.js | 8 +
image-1.png | Bin 0 -> 268365 bytes
image-2.png | Bin 0 -> 130990 bytes
image-3.png | Bin 0 -> 310282 bytes
image-4.png | Bin 0 -> 220486 bytes
image.png | Bin 0 -> 207139 bytes
16 files changed, 1102 insertions(+), 137 deletions(-)
create mode 100644 debate-hackathon/frontend/src/components/HistoryPanel.jsx
create mode 100644 debate-hackathon/frontend/src/components/ImageUploader.jsx
create mode 100644 image-1.png
create mode 100644 image-2.png
create mode 100644 image-3.png
create mode 100644 image-4.png
create mode 100644 image.png
diff --git a/README.md b/README.md
index 47ded3e..2c066e0 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,160 @@
-# UPC-开源黑客松
+# 🎯 AI 辩论助手 - UPC 开源黑客松
-#### 介绍
-UPC-开源黑客松:一个专注于技术创新与协作的开源项目,旨在通过黑客松活动促进开发者之间的交流与合作,共同推动技术进步。
+> 一个基于 AI 的智能辩论分析与对话平台,支持辩论生成、正反方分析、AI 实时对话等功能。
-#### 软件架构
-软件架构说明
+## 📖 项目介绍
+**AI 辩论助手** 是一个利用大语言模型(LLM)技术构建的智能辩论平台。用户只需输入一个话题,AI 即可自动生成结构化的辩论论点、从正反双方进行深度分析,并支持与 AI 进行实时辩论对话。
-#### 安装教程
+本项目为 UPC 开源黑客松参赛作品,采用前后端分离架构,后端使用 FastAPI + 阿里千问大模型,前端使用 React + Tailwind CSS。
-1. xxxx
-2. xxxx
-3. xxxx
+### ✨ 核心功能
-#### 使用说明
+| 功能 | 描述 |
+|------|------|
+| 📝 **辩论生成** | 输入话题,选择立场(正方/反方)和论证深度,AI 自动生成结构化论点 |
+
+| 📊 **辩论分析** | AI 从正反双方角度全面分析话题,给出综合建议和置信度评分 |
+
+| 💬 **AI 对话** | 与 AI 辩论助手实时对话,探讨辩论技巧、论点构建等 |
+
+| 📋 **历史记录** | 自动保存所有讨论记录,支持搜索、筛选、恢复查看 |
+
+| 🌙 **深色模式** | 一键切换深色/亮色主题,自动跟随系统偏好 |
+
-1. xxxx
-2. xxxx
-3. xxxx
+## 🏗️ 软件架构
-#### 参与贡献
+```
+├── debate-hackathon/
+│ ├── backend/ # 后端服务 (FastAPI)
+│ │ ├── app/
+│ │ │ ├── api/ # API 路由层
+│ │ │ │ └── debate.py # 辩论相关接口
+│ │ │ ├── core/
+│ │ │ │ └── llm_client.py # 大语言模型客户端
+│ │ │ ├── schemas/
+│ │ │ │ └── debate.py # 数据模型定义
+│ │ │ └── main.py # 应用入口
+│ │ ├── requirements.txt # Python 依赖
+│ │ └── .env # 环境配置
+│ └── frontend/ # 前端应用 (React)
+│ ├── src/
+│ │ ├── components/ # UI 组件
+│ │ │ ├── ChatBox.jsx # 聊天对话框
+│ │ │ ├── Message.jsx # 消息气泡组件
+│ │ │ ├── AnalysisPanel.jsx # 分析面板
+│ │ │ └── HistoryPanel.jsx # 历史记录面板
+│ │ ├── services/
+│ │ │ └── debateApi.js # API 服务层
+│ │ ├── App.jsx # 主应用组件
+│ │ └── index.js # 入口文件
+│ ├── package.json
+│ └── tailwind.config.js
+└── README.md
+```
-1. Fork 本仓库
-2. 新建 Feat_xxx 分支
-3. 提交代码
-4. 新建 Pull Request
+### 技术栈
+- **后端**: Python 3.10+, FastAPI, Uvicorn, 阿里千问 DashScope SDK
+- **前端**: React 18, Tailwind CSS 3, Axios
+- **AI 模型**: 阿里千问大语言模型 (Qwen)
-#### 特技
+## 🔧 安装教程
-1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
-2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
-3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
-4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
-5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
-6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
+### 环境要求
+
+- Python 3.10 或更高版本
+- Node.js 18+ 和 npm
+- 阿里云 DashScope API Key
+
+### 后端安装
+
+```bash
+# 1. 进入后端目录
+cd debate-hackathon/backend
+
+# 2. 创建并激活虚拟环境(推荐使用 conda)
+conda create -n debate python=3.10 -y
+conda activate debate
+
+# 3. 安装依赖
+pip install -r requirements.txt
+
+# 4. 配置环境变量
+# 编辑 .env 文件,填入你的阿里千问 API Key
+# DASHSCOPE_API_KEY=your_api_key_here
+```
+
+### 前端安装
+
+```bash
+# 1. 进入前端目录
+cd debate-hackathon/frontend
+
+# 2. 安装依赖
+npm install
+
+# 3. 启动开发服务器
+npm start
+```
+
+## 🚀 使用说明
+
+### 启动项目
+
+**1. 启动后端服务**
+
+```bash
+cd debate-hackathon/backend
+conda run -n debate python -m app.main
+```
+
+后端默认运行在 `http://localhost:8000`,API 文档访问 `http://localhost:8000/docs`
+
+**2. 启动前端服务**
+
+```bash
+cd debate-hackathon/frontend
+npm start
+```
+
+前端默认运行在 `http://localhost:3000`
+
+### 使用流程
+
+1. **打开浏览器** 访问 `http://localhost:3000`
+2. **输入话题** 在顶部输入框输入你想辩论的话题
+3. **选择立场** 点击"正方"或"反方"按钮
+4. **选择深度** 选择论证深度(简单/中等/深入)
+5. **生成辩论** 点击"生成辩论"按钮,AI 将生成结构化论点
+6. **分析辩论** 点击"分析辩论"按钮,AI 从正反双方分析
+7. **AI 对话** 切换到"AI 对话"Tab,与 AI 实时交流
+8. **查看历史** 切换到"历史记录"Tab,查看和恢复之前的讨论
+
+### API 接口
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | `/` | 服务状态检查 |
+| GET | `/api/health` | API 健康检查 |
+| POST | `/api/debate/generate` | 生成辩论论点 |
+| POST | `/api/debate/analyze` | 分析辩论话题 |
+| POST | `/api/debate/chat` | AI 对话 |
+| GET | `/api/debate/topics` | 获取示例话题 |
+
+## 🤝 参与贡献
+
+1. Fork 本仓库
+2. 新建 `Feat_xxx` 分支
+3. 提交代码
+4. 新建 Pull Request
+
+## 📄 开源协议
+
+本项目基于 MIT 协议开源,详见 [LICENSE](LICENSE) 文件。
+
+## 🙏 致谢
+
+- 感谢 UPC 开源黑客松提供的平台
+- 感谢阿里云 DashScope 提供的大模型 API 支持
diff --git a/debate-hackathon/backend/app/api/debate.py b/debate-hackathon/backend/app/api/debate.py
index a67f2ef..652c88b 100644
--- a/debate-hackathon/backend/app/api/debate.py
+++ b/debate-hackathon/backend/app/api/debate.py
@@ -2,7 +2,8 @@
Debate API routes
"""
-from fastapi import APIRouter, HTTPException, Depends
+import base64
+from fastapi import APIRouter, HTTPException, Depends, File, UploadFile, Form
from typing import List, Optional, Dict, Any
from app.schemas.debate import (
@@ -144,6 +145,63 @@ async def chat_with_ai(
)
+@router.post("/analyze-image")
+async def analyze_image(
+ file: UploadFile = File(..., description="Image file to analyze"),
+ prompt: str = Form("请详细描述这张图片的内容,包括其中的文字、物体、场景等"),
+ llm_client: LLMClient = Depends(get_llm_client)
+):
+ """
+ Upload and analyze an image using AI vision capabilities
+
+ - **file**: Image file (jpg, png, gif, webp)
+ - **prompt**: Optional custom prompt for image analysis
+ """
+ try:
+ # Validate file type
+ allowed_types = ["image/jpeg", "image/png", "image/gif", "image/webp"]
+ if file.content_type not in allowed_types:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Unsupported image type: {file.content_type}. Supported types: jpg, png, gif, webp"
+ )
+
+ # Read file content
+ contents = await file.read()
+
+ # Check file size (max 10MB)
+ max_size = 10 * 1024 * 1024 # 10MB
+ if len(contents) > max_size:
+ raise HTTPException(
+ status_code=400,
+ detail="Image too large. Maximum size is 10MB."
+ )
+
+ # Convert to base64
+ image_base64 = base64.b64encode(contents).decode('utf-8')
+
+ # Get image data URI
+ image_data_uri = f"data:{file.content_type};base64,{image_base64}"
+
+ # Analyze with LLM
+ result = llm_client.analyze_image(image_data_uri, prompt)
+
+ return {
+ "filename": file.filename,
+ "content_type": file.content_type,
+ "size": len(contents),
+ "analysis": result
+ }
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Image analysis failed: {str(e)}"
+ )
+
+
@router.get("/topics")
async def get_sample_topics():
"""Get a list of sample debate topics"""
@@ -199,4 +257,4 @@ async def get_sample_topics():
"category": "Finance"
}
]
- return {"topics": topics}
\ No newline at end of file
+ return {"topics": topics}
diff --git a/debate-hackathon/backend/app/core/llm_client.py b/debate-hackathon/backend/app/core/llm_client.py
index 06182b2..ff7659f 100644
--- a/debate-hackathon/backend/app/core/llm_client.py
+++ b/debate-hackathon/backend/app/core/llm_client.py
@@ -5,6 +5,7 @@ Supports OpenAI and compatible APIs (DeepSeek, etc.)
import os
import json
+import base64
from typing import List, Optional, Dict, Any
from openai import OpenAI
from dotenv import load_dotenv
@@ -20,11 +21,20 @@ class LLMClient:
self.base_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
self.model = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo")
+ # Determine vision model based on the API provider
+ # For Alibaba Cloud DashScope, use qwen-vl-ocr for OCR/text extraction tasks
+ # For OpenAI, use gpt-4o-mini for vision tasks
+ if "dashscope" in self.base_url.lower():
+ self.vision_model = os.getenv("VISION_MODEL", "qwen-vl-ocr")
+ else:
+ self.vision_model = os.getenv("VISION_MODEL", "gpt-4o-mini")
+
# Try DeepSeek as fallback
if not self.api_key or self.api_key == "your_openai_api_key_here":
self.api_key = os.getenv("DEEPSEEK_API_KEY", "")
self.base_url = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
self.model = "deepseek-chat"
+ self.vision_model = "deepseek-chat" # DeepSeek may not support vision
self.client = OpenAI(
api_key=self.api_key,
@@ -251,4 +261,52 @@ Ensure the response is valid JSON and the analysis is balanced and insightful.""
return response.choices[0].message.content
except Exception as e:
- return f"Error: {str(e)}"
\ No newline at end of file
+ return f"Error: {str(e)}"
+
+ def analyze_image(self, image_data: str, prompt: str = "请详细描述这张图片的内容") -> str:
+ """
+ Analyze an image using vision-capable model
+
+ Args:
+ image_data: Base64 encoded image data (with or without data URI prefix)
+ prompt: The prompt to ask about the image
+
+ Returns:
+ str: The model's description/analysis of the image
+ """
+ try:
+ # Ensure image data has the proper data URI prefix
+ if not image_data.startswith("data:"):
+ image_data = f"data:image/jpeg;base64,{image_data}"
+
+ messages = [
+ {
+ "role": "system",
+ "content": "你是一个智能图片分析助手。请按以下优先级处理:\n1. 如果图片中有文字,请只提取并输出图片中的文字内容,不要添加任何额外说明\n2. 如果图片中没有文字,请简要描述这张图片的主题和内容(如人物、场景、物体等),作为可以讨论的话题"
+ },
+ {
+ "role": "user",
+ "content": [
+ {"type": "text", "text": prompt},
+ {
+ "type": "image_url",
+ "image_url": {
+ "url": image_data,
+ "detail": "high"
+ }
+ }
+ ]
+ }
+ ]
+
+ response = self.client.chat.completions.create(
+ model=self.vision_model,
+ messages=messages,
+ temperature=0.3,
+ max_tokens=2000
+ )
+
+ return response.choices[0].message.content
+
+ except Exception as e:
+ return f"图片分析失败: {str(e)}"
diff --git a/debate-hackathon/frontend/src/App.jsx b/debate-hackathon/frontend/src/App.jsx
index 6913620..89d5fb7 100644
--- a/debate-hackathon/frontend/src/App.jsx
+++ b/debate-hackathon/frontend/src/App.jsx
@@ -1,7 +1,9 @@
import React, { useState, useEffect, useCallback } from 'react';
import ChatBox from './components/ChatBox';
import AnalysisPanel from './components/AnalysisPanel';
-import { generateDebate, analyzeDebate, chatWithAI, getSampleTopics, healthCheck } from './services/debateApi';
+import HistoryPanel from './components/HistoryPanel';
+import ImageUploader from './components/ImageUploader';
+import { generateDebate, analyzeDebate, chatWithAI, getSampleTopics, healthCheck, saveHistoryItem } from './services/debateApi';
/**
* Main App component for the Debate Hackathon
@@ -21,6 +23,24 @@ function App() {
const [isChatLoading, setIsChatLoading] = useState(false);
const [apiStatus, setApiStatus] = useState('checking');
const [error, setError] = useState(null);
+ const [darkMode, setDarkMode] = useState(() => {
+ const saved = localStorage.getItem('darkMode');
+ if (saved !== null) return saved === 'true';
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
+ });
+ const [historyRefreshKey, setHistoryRefreshKey] = useState(0);
+ const [imageAnalysisResult, setImageAnalysisResult] = useState(null);
+
+ // Apply dark mode class to html element
+ useEffect(() => {
+ const root = document.documentElement;
+ if (darkMode) {
+ root.classList.add('dark');
+ } else {
+ root.classList.remove('dark');
+ }
+ localStorage.setItem('darkMode', darkMode);
+ }, [darkMode]);
// Load sample topics on mount
useEffect(() => {
@@ -54,6 +74,16 @@ function App() {
try {
const result = await generateDebate(topic.trim(), side, complexity);
setDebateResult(result);
+
+ // Save to history
+ saveHistoryItem({
+ type: 'generate',
+ topic: topic.trim(),
+ side: side,
+ complexity: complexity,
+ result: result
+ });
+ setHistoryRefreshKey(k => k + 1);
} catch (err) {
setError(err.response?.data?.detail || '生成辩论失败,请检查API配置');
} finally {
@@ -73,6 +103,14 @@ function App() {
try {
const result = await analyzeDebate(topic.trim());
setAnalysisResult(result);
+
+ // Save to history
+ saveHistoryItem({
+ type: 'analyze',
+ topic: topic.trim(),
+ result: result
+ });
+ setHistoryRefreshKey(k => k + 1);
} catch (err) {
setError(err.response?.data?.detail || '分析辩论失败,请检查API配置');
} finally {
@@ -105,7 +143,33 @@ function App() {
side: 'neutral'
};
- setChatMessages(prev => [...prev, aiMessage]);
+ const newMessages = [...chatMessages, userMessage, aiMessage];
+ setChatMessages(newMessages);
+
+ // Save chat to history (only save first message or periodically)
+ if (chatMessages.length === 0) {
+ saveHistoryItem({
+ type: 'chat',
+ topic: message.length > 50 ? message.substring(0, 50) + '...' : message,
+ messages: newMessages,
+ messageCount: newMessages.length
+ });
+ setHistoryRefreshKey(k => k + 1);
+ } else {
+ // Update existing chat history
+ try {
+ const existing = JSON.parse(localStorage.getItem('debateHistory') || '[]');
+ const lastChat = existing.filter(item => item.type === 'chat').pop();
+ if (lastChat) {
+ lastChat.messages = newMessages;
+ lastChat.messageCount = newMessages.length;
+ lastChat.timestamp = Date.now();
+ localStorage.setItem('debateHistory', JSON.stringify(existing));
+ }
+ } catch (e) {
+ // Ignore update errors
+ }
+ }
} catch (err) {
const errorMessage = {
type: 'ai',
@@ -123,6 +187,30 @@ function App() {
setTopic(selectedTopic.topic);
};
+ // Handle restoring history item
+ const handleRestoreHistory = useCallback((item) => {
+ setTopic(item.topic);
+
+ switch (item.type) {
+ case 'generate':
+ if (item.side) setSide(item.side);
+ if (item.complexity) setComplexity(item.complexity);
+ if (item.result) setDebateResult(item.result);
+ setActiveTab('generate');
+ break;
+ case 'analyze':
+ if (item.result) setAnalysisResult(item.result);
+ setActiveTab('analyze');
+ break;
+ case 'chat':
+ if (item.messages) setChatMessages(item.messages);
+ setActiveTab('chat');
+ break;
+ default:
+ break;
+ }
+ }, []);
+
// Render API status indicator
const renderApiStatus = () => {
const statusConfig = {
@@ -137,15 +225,15 @@ function App() {
return (
- {config.text}
+ {config.text}
);
};
return (
-
+
{/* Header */}
-
+
@@ -157,11 +245,33 @@ function App() {
-
AI 辩论助手
-
智能辩论分析与对话平台
+
AI 辩论助手
+
智能辩论分析与对话平台
- {renderApiStatus()}
+
+ {renderApiStatus()}
+ {/* Dark mode toggle */}
+
+
@@ -169,17 +279,17 @@ function App() {
{/* Error display */}
{error && (
-
+
-
{error}
+
{error}