diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..d6ee5839580381734e622dae8f5d44a94b763412
--- /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/README.md b/README.md
index 47ded3e6238dcc784f3ff4458ba8ad760733aeec..2c066e08aa4081b13edfce7f29c2dcacd0ca93ee 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/__init__.py b/debate-hackathon/backend/app/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed65cdc9815bf19fe86d807d322eaa8d0149bf7a
--- /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 0000000000000000000000000000000000000000..6f0d6adf8b6654835ec938068ab05be8bf7b3715
--- /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 0000000000000000000000000000000000000000..652c88be301ffd7e1f3117846c96f122390972a4
--- /dev/null
+++ b/debate-hackathon/backend/app/api/debate.py
@@ -0,0 +1,260 @@
+"""
+Debate API routes
+"""
+
+import base64
+from fastapi import APIRouter, HTTPException, Depends, File, UploadFile, Form
+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.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"""
+ 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}
diff --git a/debate-hackathon/backend/app/core/__init__.py b/debate-hackathon/backend/app/core/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3a25fa0564779f61fcdf3d334a6025defe3c073e
--- /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 0000000000000000000000000000000000000000..ff7659f53f4b53161e341afdfca8586b2d70fb85
--- /dev/null
+++ b/debate-hackathon/backend/app/core/llm_client.py
@@ -0,0 +1,312 @@
+"""
+LLM Client for generating debate arguments
+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
+
+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")
+
+ # 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,
+ 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)}"
+
+ 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/backend/app/main.py b/debate-hackathon/backend/app/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..d0f7b1d9691be6ae920ae0ae161be65e67c7319c
--- /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 0000000000000000000000000000000000000000..c3659befb814afa0f2145ae93dc4e674fe401c11
--- /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 0000000000000000000000000000000000000000..e059d1e1d876e3eb65d5dcebb5c374ad4c265118
--- /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 0000000000000000000000000000000000000000..6b31b6fd6ce4538f9e8813d3471fe9f3876e67e4
--- /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 0000000000000000000000000000000000000000..3e13d9e7cd76d639376a8cfb1d86e4a8475f53cc
--- /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 0000000000000000000000000000000000000000..33ad091d26d8a9dc95ebdf616e217d985ec215b8
--- /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 0000000000000000000000000000000000000000..dda74dacd1355d2fcc5dda91397d246d7d2e0088
--- /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 0000000000000000000000000000000000000000..89d5fb7a3a5ce2e22e5c6330dc31e0c6a1d5b2ef
--- /dev/null
+++ b/debate-hackathon/frontend/src/App.jsx
@@ -0,0 +1,655 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import ChatBox from './components/ChatBox';
+import AnalysisPanel from './components/AnalysisPanel';
+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
+ */
+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);
+ 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(() => {
+ 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);
+
+ // 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 {
+ 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);
+
+ // Save to history
+ saveHistoryItem({
+ type: 'analyze',
+ topic: topic.trim(),
+ result: result
+ });
+ setHistoryRefreshKey(k => k + 1);
+ } 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'
+ };
+
+ 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',
+ 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);
+ };
+
+ // 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 = {
+ 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()}
+ {/* Dark mode toggle */}
+
+
+
+
+
+
+
+ {/* Error display */}
+ {error && (
+
+
+
+
{error}
+
+
+
+ )}
+
+ {/* Input section */}
+
+
+ {/* Topic input */}
+
+
+ setTopic(e.target.value)}
+ placeholder="输入你想辩论的话题,例如:人工智能对人类有益吗?"
+ className="w-full px-3 py-2 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 text-sm placeholder-gray-400 dark:placeholder-gray-500 transition-colors"
+ 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' && (
+
+
+
+ )}
+
+ {activeTab === 'image' && (
+
+
+
+
🖼️
+
+
AI 图片识别
+
上传图片,AI 将自动识别并描述图片内容
+
+
+
+
{
+ setImageAnalysisResult(result);
+ setError(null);
+ // 自动将识别出的文字内容填入话题输入框,并切换到辩论生成Tab
+ setTopic(result.analysis);
+ setActiveTab('generate');
+ }}
+ onError={(msg) => setError(msg)}
+ />
+ {imageAnalysisResult && (
+
+
+
+ 📄
+ {imageAnalysisResult.filename}
+
+ ({(imageAnalysisResult.size / 1024).toFixed(1)} KB)
+
+
+
+
+
+ 🤖 AI 识别结果
+
+
+ {imageAnalysisResult.analysis}
+
+
+
+ )}
+
+ )}
+
+ {activeTab === 'history' && (
+
+
+
+ )}
+
+
+
+
+ {/* 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 0000000000000000000000000000000000000000..e5bb5471ce05ef32864bcc1f7f3f4107e21984c4
--- /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 0000000000000000000000000000000000000000..8d07f50c5ff0a705fa8ce482814e98d20dc4e53f
--- /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/HistoryPanel.jsx b/debate-hackathon/frontend/src/components/HistoryPanel.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e185803dfd48a9a52043c2b4f401595a59be5e66
--- /dev/null
+++ b/debate-hackathon/frontend/src/components/HistoryPanel.jsx
@@ -0,0 +1,244 @@
+import React, { useState, useEffect } from 'react';
+
+/**
+ * HistoryPanel component for viewing debate history
+ */
+const HistoryPanel = ({ onRestore, onClear }) => {
+ const [history, setHistory] = useState([]);
+ const [filter, setFilter] = useState('all'); // 'all', 'generate', 'analyze', 'chat'
+ const [searchTerm, setSearchTerm] = useState('');
+
+ // Load history from localStorage
+ useEffect(() => {
+ loadHistory();
+ }, []);
+
+ const loadHistory = () => {
+ try {
+ const saved = localStorage.getItem('debateHistory');
+ if (saved) {
+ setHistory(JSON.parse(saved));
+ }
+ } catch (e) {
+ console.error('Failed to load history:', e);
+ }
+ };
+
+ // Listen for storage changes from other tabs
+ useEffect(() => {
+ const handleStorageChange = (e) => {
+ if (e.key === 'debateHistory') {
+ loadHistory();
+ }
+ };
+ window.addEventListener('storage', handleStorageChange);
+ return () => window.removeEventListener('storage', handleStorageChange);
+ }, []);
+
+ const handleClearAll = () => {
+ if (window.confirm('确定要清除所有讨论历史吗?此操作不可恢复。')) {
+ localStorage.removeItem('debateHistory');
+ setHistory([]);
+ if (onClear) onClear();
+ }
+ };
+
+ const handleDeleteItem = (id) => {
+ const newHistory = history.filter(item => item.id !== id);
+ localStorage.setItem('debateHistory', JSON.stringify(newHistory));
+ setHistory(newHistory);
+ };
+
+ const getTypeIcon = (type) => {
+ switch (type) {
+ case 'generate': return '📝';
+ case 'analyze': return '📊';
+ case 'chat': return '💬';
+ default: return '📌';
+ }
+ };
+
+ const getTypeLabel = (type) => {
+ switch (type) {
+ case 'generate': return '辩论生成';
+ case 'analyze': return '辩论分析';
+ case 'chat': return 'AI 对话';
+ default: return '未知';
+ }
+ };
+
+ const getTypeColor = (type) => {
+ switch (type) {
+ case 'generate': return 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300';
+ case 'analyze': return 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300';
+ case 'chat': return 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300';
+ default: return 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300';
+ }
+ };
+
+ const formatTime = (timestamp) => {
+ const date = new Date(timestamp);
+ const now = new Date();
+ const diff = now - date;
+
+ if (diff < 60000) return '刚刚';
+ if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`;
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`;
+ if (diff < 604800000) return `${Math.floor(diff / 86400000)} 天前`;
+
+ return date.toLocaleDateString('zh-CN', {
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ };
+
+ const getPreview = (item) => {
+ switch (item.type) {
+ case 'generate':
+ return `立场: ${item.side === 'pro' ? '正方' : '反方'} | 深度: ${item.complexity === 'easy' ? '简单' : item.complexity === 'medium' ? '中等' : '深入'}`;
+ case 'analyze':
+ return '正反双方分析';
+ case 'chat':
+ const msgCount = item.messageCount || 0;
+ return `${msgCount} 条消息`;
+ default:
+ return '';
+ }
+ };
+
+ // Filter and search
+ const filteredHistory = history.filter(item => {
+ if (filter !== 'all' && item.type !== filter) return false;
+ if (searchTerm && !item.topic.toLowerCase().includes(searchTerm.toLowerCase())) return false;
+ return true;
+ }).sort((a, b) => b.timestamp - a.timestamp);
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
讨论历史
+
共 {history.length} 条记录
+
+
+ {history.length > 0 && (
+
+ )}
+
+
+
+ {/* 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 0000000000000000000000000000000000000000..d630ddbac7b2d274e66f79e77796fbd7859264f4
--- /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 */}
+
+

+
+
+ {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
new file mode 100644
index 0000000000000000000000000000000000000000..1aecac96c74bfa513372600dc372519b978cb906
--- /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 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 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl'
+ : side === 'con'
+ ? '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 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 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-xl';
+ default:
+ return 'bg-white dark:bg-slate-800 border border-gray-200 dark:border-dark-border 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 0000000000000000000000000000000000000000..17d8d5ecd41143ad1cef6bad4444bb5035921031
--- /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 0000000000000000000000000000000000000000..2cb1087e76eb7b33e49ace3622c9e108cd1ed64c
--- /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 0000000000000000000000000000000000000000..a4a6d63e1d9edf32ac7119421feedb0bd91b2a06
--- /dev/null
+++ b/debate-hackathon/frontend/src/services/debateApi.js
@@ -0,0 +1,199 @@
+/**
+ * Debate API service
+ * Handles all communication with the backend API and local history management
+ */
+
+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;
+ }
+};
+
+/**
+ * Analyze an image using AI vision
+ * @param {File} file - The image file to analyze
+ * @param {string} prompt - Optional custom prompt
+ */
+export const analyzeImage = async (file, prompt = '请详细描述这张图片的内容,包括其中的文字、物体、场景等') => {
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('prompt', prompt);
+
+ const response = await api.post('/api/debate/analyze-image', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ timeout: 60000, // 60 seconds for image analysis
+ });
+ return response.data;
+ } catch (error) {
+ console.error('Failed to analyze image:', error);
+ throw error;
+ }
+};
+
+/**
+ * ========== History Management (localStorage) ==========
+ */
+
+const HISTORY_KEY = 'debateHistory';
+
+/**
+ * Generate a unique ID for history items
+ */
+const generateId = () => {
+ return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
+};
+
+/**
+ * Get all history items from localStorage
+ */
+export const getHistory = () => {
+ try {
+ const saved = localStorage.getItem(HISTORY_KEY);
+ return saved ? JSON.parse(saved) : [];
+ } catch (e) {
+ console.error('Failed to load history:', e);
+ return [];
+ }
+};
+
+/**
+ * Save a history item to localStorage
+ * @param {Object} item - The history item to save
+ */
+export const saveHistoryItem = (item) => {
+ try {
+ const history = getHistory();
+ const newItem = {
+ id: generateId(),
+ timestamp: Date.now(),
+ ...item,
+ };
+ history.push(newItem);
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
+ return newItem;
+ } catch (e) {
+ console.error('Failed to save history:', e);
+ return null;
+ }
+};
+
+/**
+ * Delete a history item by id
+ * @param {string} id - The id of the item to delete
+ */
+export const deleteHistoryItem = (id) => {
+ try {
+ const history = getHistory();
+ const filtered = history.filter(item => item.id !== id);
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(filtered));
+ return filtered;
+ } catch (e) {
+ console.error('Failed to delete history item:', e);
+ return [];
+ }
+};
+
+/**
+ * Clear all history
+ */
+export const clearHistory = () => {
+ try {
+ localStorage.removeItem(HISTORY_KEY);
+ return [];
+ } catch (e) {
+ console.error('Failed to clear history:', e);
+ return [];
+ }
+};
+
+export default api;
diff --git a/debate-hackathon/frontend/tailwind.config.js b/debate-hackathon/frontend/tailwind.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..d6c72a9c7c9400e0e05e2f9c9810488867974cb2
--- /dev/null
+++ b/debate-hackathon/frontend/tailwind.config.js
@@ -0,0 +1,39 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ darkMode: 'class',
+ 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',
+ },
+ dark: {
+ bg: '#0f172a',
+ card: '#1e293b',
+ border: '#334155',
+ text: '#e2e8f0',
+ muted: '#94a3b8',
+ }
+ }
+ },
+ },
+ plugins: [],
+}
diff --git a/image-1.png b/image-1.png
new file mode 100644
index 0000000000000000000000000000000000000000..b1c8f40772bf9d0774f6007cbc09cd81449afe18
Binary files /dev/null and b/image-1.png differ
diff --git a/image-2.png b/image-2.png
new file mode 100644
index 0000000000000000000000000000000000000000..835a0db94d04cd1b996d5c7ac1a6637ab1830609
Binary files /dev/null and b/image-2.png differ
diff --git a/image-3.png b/image-3.png
new file mode 100644
index 0000000000000000000000000000000000000000..4621b81e4db81991b6aff9675ca1ab31ef1a8d87
Binary files /dev/null and b/image-3.png differ
diff --git a/image-4.png b/image-4.png
new file mode 100644
index 0000000000000000000000000000000000000000..a3dcff9d371a28ec22093ff5b8ca9ce829f0a007
Binary files /dev/null and b/image-4.png differ
diff --git a/image.png b/image.png
new file mode 100644
index 0000000000000000000000000000000000000000..43a52746040c977b00dc548e0088020fccbd8523
Binary files /dev/null and b/image.png differ