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 */} +
+
+ setInput(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="输入你的论点或问题..." + disabled={isLoading} + className="flex-1 px-4 py-2.5 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent text-sm disabled:bg-gray-100 disabled:cursor-not-allowed" + /> + +
+
+
+ ); +}; + +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 ( +
+
+ {content} +
+
+ ); + } + + // 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 自动生成结构化论点 | +![alt text](image-1.png) +| 📊 **辩论分析** | AI 从正反双方角度全面分析话题,给出综合建议和置信度评分 | +![alt text](image-2.png) +| 💬 **AI 对话** | 与 AI 辩论助手实时对话,探讨辩论技巧、论点构建等 | +![alt text](image-3.png) +| 📋 **历史记录** | 自动保存所有讨论记录,支持搜索、筛选、恢复查看 | +![alt text](image-4.png) +| 🌙 **深色模式** | 一键切换深色/亮色主题,自动跟随系统偏好 | +![alt text](image.png) -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}

+ )} +
+
+ + {/* Search and filter */} +
+
+
+ + + + setSearchTerm(e.target.value)} + placeholder="搜索话题..." + className="w-full pl-8 pr-3 py-1.5 text-xs border border-gray-200 dark:border-dark-border bg-white dark:bg-slate-800 text-gray-900 dark:text-dark-text rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent placeholder-gray-400 dark:placeholder-gray-500" + /> +
+
+
+ {[ + { key: 'all', label: '全部' }, + { key: 'generate', label: '📝 辩论' }, + { key: 'analyze', label: '📊 分析' }, + { key: 'chat', label: '💬 对话' }, + ].map(({ key, label }) => ( + + ))} +
+
+ + {/* History list */} +
+ {filteredHistory.length === 0 ? ( +
+
+
📋
+

+ {searchTerm ? '没有找到匹配的记录' : '暂无讨论历史'} +

+

+ {searchTerm ? '试试其他关键词' : '生成辩论或进行对话后,记录将显示在这里'} +

+
+
+ ) : ( + filteredHistory.map((item) => ( +
onRestore && onRestore(item)} + > +
+
+
+ + {getTypeIcon(item.type)} {getTypeLabel(item.type)} + + + {formatTime(item.timestamp)} + +
+

+ {item.topic} +

+

+ {getPreview(item)} +

+
+ +
+
+ )) + )} +
+
+ ); +}; + +export default HistoryPanel; diff --git a/debate-hackathon/frontend/src/components/ImageUploader.jsx b/debate-hackathon/frontend/src/components/ImageUploader.jsx new file mode 100644 index 0000000..d630ddb --- /dev/null +++ b/debate-hackathon/frontend/src/components/ImageUploader.jsx @@ -0,0 +1,190 @@ +import React, { useState, useRef } from 'react'; +import { analyzeImage } from '../services/debateApi'; + +/** + * ImageUploader component for uploading and analyzing images + */ +const ImageUploader = ({ onAnalysisResult, onError }) => { + const [selectedFile, setSelectedFile] = useState(null); + const [preview, setPreview] = useState(null); + const [isAnalyzing, setIsAnalyzing] = useState(false); + const [dragOver, setDragOver] = useState(false); + const [customPrompt, setCustomPrompt] = useState(''); + const fileInputRef = useRef(null); + + const handleFileSelect = (file) => { + if (!file) return; + + // Validate file type + const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + if (!allowedTypes.includes(file.type)) { + if (onError) onError('不支持的文件格式,请上传 JPG、PNG、GIF 或 WebP 格式的图片'); + return; + } + + // Validate file size (10MB) + if (file.size > 10 * 1024 * 1024) { + if (onError) onError('图片太大,请上传小于 10MB 的图片'); + return; + } + + setSelectedFile(file); + + // Create preview + const reader = new FileReader(); + reader.onload = (e) => { + setPreview(e.target.result); + }; + reader.readAsDataURL(file); + }; + + const handleInputChange = (e) => { + handleFileSelect(e.target.files[0]); + }; + + const handleDrop = (e) => { + e.preventDefault(); + setDragOver(false); + const file = e.dataTransfer.files[0]; + handleFileSelect(file); + }; + + const handleDragOver = (e) => { + e.preventDefault(); + setDragOver(true); + }; + + const handleDragLeave = () => { + setDragOver(false); + }; + + const handleAnalyze = async () => { + if (!selectedFile) return; + + setIsAnalyzing(true); + try { + const prompt = customPrompt.trim() || '请详细描述这张图片的内容,包括其中的文字、物体、场景等'; + const result = await analyzeImage(selectedFile, prompt); + if (onAnalysisResult) onAnalysisResult(result); + } catch (err) { + if (onError) onError(err.response?.data?.detail || '图片分析失败,请检查API配置'); + } finally { + setIsAnalyzing(false); + } + }; + + const handleReset = () => { + setSelectedFile(null); + setPreview(null); + setCustomPrompt(''); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + return ( +
+ {!preview ? ( + /* Upload area */ +
fileInputRef.current?.click()} + className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all ${ + dragOver + ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' + : 'border-gray-300 dark:border-dark-border hover:border-blue-400 dark:hover:border-blue-500 bg-gray-50 dark:bg-slate-800/50' + }`} + > + +
+ + + +

+ 点击或拖拽图片到此处 +

+

+ 支持 JPG、PNG、GIF、WebP 格式,最大 10MB +

+
+
+ ) : ( + /* Preview area */ +
+ {/* Image preview */} +
+ Preview + +
+ {selectedFile?.name} +
+
+ + {/* Custom prompt */} +
+ + setCustomPrompt(e.target.value)} + placeholder="例如:请提取图片中的文字内容" + className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-dark-border bg-white dark:bg-slate-800 text-gray-900 dark:text-dark-text rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent placeholder-gray-400 dark:placeholder-gray-500" + /> +
+ + {/* Action buttons */} +
+ + +
+
+ )} +
+ ); +}; + +export default ImageUploader; diff --git a/debate-hackathon/frontend/src/components/Message.jsx b/debate-hackathon/frontend/src/components/Message.jsx index 7d3b719..1aecac9 100644 --- a/debate-hackathon/frontend/src/components/Message.jsx +++ b/debate-hackathon/frontend/src/components/Message.jsx @@ -9,19 +9,19 @@ const Message = ({ message, type = 'argument', side = 'neutral' }) => { 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'; + return 'bg-white dark:bg-slate-800 border border-gray-200 dark:border-dark-border rounded-2xl rounded-bl-sm shadow-sm'; case 'argument': return side === 'pro' - ? 'bg-green-50 border border-green-200 rounded-xl' + ? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl' : side === 'con' - ? 'bg-red-50 border border-red-200 rounded-xl' - : 'bg-gray-50 border border-gray-200 rounded-xl'; + ? 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl' + : 'bg-gray-50 dark:bg-slate-800 border border-gray-200 dark:border-dark-border rounded-xl'; case 'summary': - return 'bg-gradient-to-r from-indigo-50 to-purple-50 border border-indigo-200 rounded-xl'; + return 'bg-gradient-to-r from-indigo-50 to-purple-50 dark:from-indigo-900/20 dark:to-purple-900/20 border border-indigo-200 dark:border-indigo-800 rounded-xl'; case 'analysis': - return 'bg-purple-50 border border-purple-200 rounded-xl'; + return 'bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-xl'; default: - return 'bg-white border border-gray-200 rounded-xl'; + return 'bg-white dark:bg-slate-800 border border-gray-200 dark:border-dark-border rounded-xl'; } }; @@ -29,13 +29,13 @@ const Message = ({ message, type = 'argument', side = 'neutral' }) => { switch (side) { case 'pro': return ( - + 👍 正方 ); case 'con': return ( - + 👎 反方 ); @@ -56,11 +56,11 @@ const Message = ({ message, type = 'argument', side = 'neutral' }) => { return (
-
+
论证强度 {percentage}%
-
+
{ return (
{content}
@@ -92,28 +92,28 @@ const Message = ({ message, type = 'argument', side = 'neutral' }) => {
{getSideLabel()} {type === 'summary' && ( - + 📝 总结 )} {type === 'analysis' && ( - + 📊 分析 )}
-
+
{content}
{message.supporting_evidence && message.supporting_evidence.length > 0 && ( -
-

📎 支持证据:

+
+

📎 支持证据: