diff --git a/README.md b/README.md index ad60a40..a1ab069 100644 --- a/README.md +++ b/README.md @@ -1,205 +1,242 @@ -# TimeWhisper - -Client Milestone 1 Meeting Note: https://github.com/kumamonlove/TimeWhisper/issues/6#issue-3037033222 - - - -TimeWhisper is a time management web application built with React and Python FastAPI, integrated with DeepSeek AI features to help users better manage their time and tasks. - -## Features - -1. **Task Management**: Add, view, complete, and delete tasks -2. **AI Assistant**: Offers time management advice and assistance - - Supports multiple DeepSeek model options - - **DeepSeek-V3 (deepseek-chat)**: General-purpose conversational model - - **DeepSeek-R1 (deepseek-reasoner)**: Optimized for complex reasoning - - Markdown rendering supported, including code highlighting - -## Tech Stack - -- **Frontend**: React, Axios, React Icons, React Markdown -- **Backend**: Python, FastAPI, DeepSeek API - -## Project Structure - -``` -time-management/ -├── backend/ -│ ├── main_openai_sdk.py # FastAPI main app (using OpenAI SDK) -│ └── run_sdk.py # Startup script (using OpenAI SDK) -├── frontend/ -│ ├── public/ -│ └── src/ -│ ├── components/ -│ │ ├── TaskList.js -│ │ └── ChatAssistant.js -│ ├── App.js -│ ├── App.css -│ ├── index.js -│ └── index.css -├── .env # Environment variable example file -└── requirements.txt # Python dependencies -``` - -## Installation & Run - -### Backend - -1. Install Python dependencies: - - ``` - pip install -r requirements.txt - ``` - -2. Create a `.env` file and set your DeepSeek API key: - - ``` - DEEPSEEK_API_KEY=your_deepseek_api_key_here - ``` - - - Note: The API key typically starts with `sk-` - - You can obtain it at https://platform.deepseek.com/api_keys - -3. Start the backend: - - Using OpenAI SDK (recommended): - - ``` - cd backend - python run_sdk.py - ``` - -### Frontend - -1. Install Node.js dependencies: - - ``` - cd frontend - npm install - npm install react-markdown react-syntax-highlighter --save - npm install remark-gfm - npm install react-datepicker - ``` - -2. Start the frontend development server: - - ``` - npm start - npm run build - npm install -g serve - serve -s build - ``` - -## DeepSeek API Usage - -This project supports two ways to call the DeepSeek API: - -1. **Using `requests` (direct HTTP)**: Manually sending HTTP requests to the API endpoints -2. **Using OpenAI SDK**: Leverages OpenAI-compatible endpoints, which is the recommended approach by DeepSeek - -Since DeepSeek provides OpenAI-compatible APIs, you can use the OpenAI SDK directly with minimal configuration. - -### Supported Models - -1. **DeepSeek-V3** (`deepseek-chat`): General-purpose model for everyday conversations and basic inquiries -2. **DeepSeek-R1** (`deepseek-reasoner`): Reasoning-optimized model for complex logic and analytical responses - -Users can choose the desired model from a dropdown menu in the chat interface. Each model may respond differently in style and content. - -### Markdown Support - -The chat interface supports rendering Markdown-formatted responses, including: - -- Headings (H1-H6) -- Lists (ordered and unordered) -- Links and images -- Code blocks with syntax highlighting -- Tables -- Blockquotes -- Bold, italic, and other text styles - -This allows the AI assistant to present rich, structured content. Try prompting the assistant with something like: - -``` -Please provide a time management plan for today in Markdown, including a table and some code examples. -``` - -### Troubleshooting - -1. **API Errors**: - - Ensure your API key is valid (starts with `sk-`) - - Verify the selected model name (`deepseek-chat` or `deepseek-reasoner`) - - Check that request parameters are correctly formatted -2. **Timeouts**: - - Might be due to network issues or slow API response — try increasing the timeout value -3. **Markdown Rendering Issues**: - - Ensure `react-markdown` and `react-syntax-highlighter` are installed - - Check that the AI's response conforms to proper Markdown syntax - -## How to Use - -1. Visit `http://localhost:3000` to open the app -2. Use the **"Task Management"** tab to add and manage tasks -3. Use the **"Chat Assistant"** tab to interact with the AI assistant for time management suggestions - - Select your preferred model from the dropdown - - Try prompting the AI to use Markdown (e.g., tables, code blocks) for better output formatting - ------- - -## Update – April 25 - -1. **Added Conversation History**: - The assistant now retains up to 5 rounds of context for better continuity, eliminating the issue of single-turn conversations. -2. **Improved Table Display**: - Integrated the `remark-gfm` plugin to correctly render Markdown tables. No more garbled outputs when displaying time schedules. -3. **Fixed Chat Tab State Loss**: - Resolved an issue where switching from Chat Assistant to Task Management would clear previous conversation history. -4. **Chat Output Stream Control**: - You can now stop and interrupt the assistant’s response mid-generation if you realize your input was incorrect. -5. **Enhanced Date-Time Selection in Tasks**: - Tasks now support selecting specific time (e.g., 10:00 AM or 3:00 PM), not just the date. -6. **Fixed Timezone Offset Bug**: - Task date displays now correctly align with local time without unwanted timezone shifts. - ------- - -## Update – May 02 - -**Swap pages and task pages**: -Adjusted the positions of the conversation page and task page. - -Of course! Here's the English version of your content: - -**Modernized Color Scheme:** - -- Adopted a blue-based primary color (#3a6ea5) for a more professional and composed appearance -- Defined a complete color system and variables for easier maintenance and consistent visual style -- Introduced more refined grayscale gradients to enhance the sense of depth in the interface - -**Sophisticated Visual Effects:** - -- Added detailed shadow effects to enhance depth and dimensionality -- Optimized border radii to make the interface more modern and softer -- Introduced transition animations to make interactions smoother and more natural - -**Improved Layout and Typography:** - -- Expanded the overall container width for better space utilization -- Optimized tab styles by using an underline to indicate the active tab -- Enhanced the layout of task lists and chat interfaces to improve readability - -**Advanced Detail Enhancements:** - -- Added decorative underlines to headings -- Introduced hover animations and elevation effects for task items -- Improved the state feedback of buttons and input fields -- Added fade-in animations for messages - -**Refined Form Elements:** - -- Beautified the styles of input fields and buttons -- Enhanced the focus state of form elements -- Optimized the styling of the date picker - ------- - +# TimeWhisper + +TimeWhisper is a time management web application built with React and Python FastAPI, integrated with DeepSeek AI features to help users better manage their time and tasks. + +## Features + +1. **Task Management**: Add, view, complete, and delete tasks +2. **AI Assistant**: Offers time management advice and assistance + - Supports multiple DeepSeek model options + - **DeepSeek-V3 (deepseek-chat)**: General-purpose conversational model + - **DeepSeek-R1 (deepseek-reasoner)**: Optimized for complex reasoning + - Markdown rendering supported, including code highlighting + +## Tech Stack + +- **Frontend**: React, Axios, React Icons, React Markdown +- **Backend**: Python, FastAPI, DeepSeek API + +## Project Structure + +``` +time-management/ +├── backend/ +│ ├── main_openai_sdk.py # FastAPI main app (using OpenAI SDK) +│ └── run_sdk.py # Startup script (using OpenAI SDK) +├── frontend/ +│ ├── public/ +│ └── src/ +│ ├── components/ +│ │ ├── TaskList.js +│ │ └── ChatAssistant.js +│ ├── App.js +│ ├── App.css +│ ├── index.js +│ └── index.css +├── .env # Environment variable example file +└── requirements.txt # Python dependencies +``` + +## Installation & Run + +### Backend + +1. Install Python dependencies: + + ``` + pip install -r requirements.txt + ``` + +2. Create a `.env` file and set your DeepSeek API key: + + ``` + DEEPSEEK_API_KEY=your_deepseek_api_key_here + ``` + + - Note: The API key typically starts with `sk-` + - You can obtain it at https://platform.deepseek.com/api_keys + +3. Start the backend: + + Using OpenAI SDK (recommended): + + ``` + cd backend + python run_sdk.py + ``` + +### Frontend + +1. Install Node.js dependencies: + + ``` + cd frontend + npm install + npm install react-markdown react-syntax-highlighter --save + npm install remark-gfm + npm install react-datepicker + npm install recharts + ``` + +2. Start the frontend development server: + + ``` + npm start + npm run build + npm install -g serve + serve -s build + ``` + +## DeepSeek API Usage + +This project supports two ways to call the DeepSeek API: + +1. **Using `requests` (direct HTTP)**: Manually sending HTTP requests to the API endpoints +2. **Using OpenAI SDK**: Leverages OpenAI-compatible endpoints, which is the recommended approach by DeepSeek + +Since DeepSeek provides OpenAI-compatible APIs, you can use the OpenAI SDK directly with minimal configuration. + +### Supported Models + +1. **DeepSeek-V3** (`deepseek-chat`): General-purpose model for everyday conversations and basic inquiries +2. **DeepSeek-R1** (`deepseek-reasoner`): Reasoning-optimized model for complex logic and analytical responses + +Users can choose the desired model from a dropdown menu in the chat interface. Each model may respond differently in style and content. + +### Markdown Support + +The chat interface supports rendering Markdown-formatted responses, including: + +- Headings (H1-H6) +- Lists (ordered and unordered) +- Links and images +- Code blocks with syntax highlighting +- Tables +- Blockquotes +- Bold, italic, and other text styles + +This allows the AI assistant to present rich, structured content. Try prompting the assistant with something like: + +``` +Please provide a time management plan for today in Markdown, including a table and some code examples. +``` + +### Troubleshooting + +1. **API Errors**: + - Ensure your API key is valid (starts with `sk-`) + - Verify the selected model name (`deepseek-chat` or `deepseek-reasoner`) + - Check that request parameters are correctly formatted +2. **Timeouts**: + - Might be due to network issues or slow API response — try increasing the timeout value +3. **Markdown Rendering Issues**: + - Ensure `react-markdown` and `react-syntax-highlighter` are installed + - Check that the AI's response conforms to proper Markdown syntax + +## How to Use + +1. Visit `http://localhost:3000` to open the app +2. Use the **"Task Management"** tab to add and manage tasks +3. Use the **"Chat Assistant"** tab to interact with the AI assistant for time management suggestions + - Select your preferred model from the dropdown + - Try prompting the AI to use Markdown (e.g., tables, code blocks) for better output formatting + +------ + +## Update – April 25 + +1. **Added Conversation History**: + The assistant now retains up to 5 rounds of context for better continuity, eliminating the issue of single-turn conversations. +2. **Improved Table Display**: + Integrated the `remark-gfm` plugin to correctly render Markdown tables. No more garbled outputs when displaying time schedules. +3. **Fixed Chat Tab State Loss**: + Resolved an issue where switching from Chat Assistant to Task Management would clear previous conversation history. +4. **Chat Output Stream Control**: + You can now stop and interrupt the assistant’s response mid-generation if you realize your input was incorrect. +5. **Enhanced Date-Time Selection in Tasks**: + Tasks now support selecting specific time (e.g., 10:00 AM or 3:00 PM), not just the date. +6. **Fixed Timezone Offset Bug**: + Task date displays now correctly align with local time without unwanted timezone shifts. + +------ + +## Update – April 30 + +**Swap pages and task pages**: +Adjusted the positions of the conversation page and task page. + +Of course! Here's the English version of your content: + +**Modernized Color Scheme:** + +- Adopted a blue-based primary color (#3a6ea5) for a more professional and composed appearance +- Defined a complete color system and variables for easier maintenance and consistent visual style +- Introduced more refined grayscale gradients to enhance the sense of depth in the interface + +**Sophisticated Visual Effects:** + +- Added detailed shadow effects to enhance depth and dimensionality +- Optimized border radii to make the interface more modern and softer +- Introduced transition animations to make interactions smoother and more natural + +**Improved Layout and Typography:** + +- Expanded the overall container width for better space utilization +- Optimized tab styles by using an underline to indicate the active tab +- Enhanced the layout of task lists and chat interfaces to improve readability + +**Advanced Detail Enhancements:** + +- Added decorative underlines to headings +- Introduced hover animations and elevation effects for task items +- Improved the state feedback of buttons and input fields +- Added fade-in animations for messages + +**Refined Form Elements:** + +- Beautified the styles of input fields and buttons +- Enhanced the focus state of form elements +- Optimized the styling of the date picker + +------ + +## Update – May 9th + +Created the **TaskStats** component: + +- Includes four different types of charts (pie chart, bar chart, line chart). +- Displays task completion status, task category distribution, weekly task trends, and yearly trends. +- Utilizes the **Recharts** library to implement chart functionalities. + +Updated **App.js**: + +- Added a **"Task Statistics"** tab. +- Enabled automatic display of the statistics popup when a task is marked as completed. +- Implemented a popup window to show statistical information, with the ability to close it. + +Added corresponding **CSS styles**: + +- Designed a visually appealing layout and animation effects for the statistics cards. +- Styled the popup window, including a semi-transparent background and animations. +- Ensured responsive design of the statistical charts across different screen sizes. + +Now, users can view task statistics in two ways: + +- Click the **"Task Statistics"** tab to view all statistics at any time. +- Automatically see the statistics popup after marking a task as completed. + +Each chart is dynamic and updates in real time based on the task data, providing intuitive data visualization, including: + +- The ratio of completed to incomplete tasks. +- Distribution of tasks across different categories. +- Weekly task completion trends. +- Annual task completion trends. + +Created login interfacet: + +- The login interface uses a landscape map. +- The login username and password used are admin and admin. +- Set up in a simple and elegant form. + +------ + diff --git a/backend/main_openai_sdk.py b/backend/main_openai_sdk.py new file mode 100644 index 0000000..117d64e --- /dev/null +++ b/backend/main_openai_sdk.py @@ -0,0 +1,248 @@ +from fastapi import FastAPI, HTTPException, Depends, Body, Request, Query +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from typing import List, Optional, Dict +import os +from datetime import datetime +import json +from openai import OpenAI +from fastapi.responses import StreamingResponse +from dotenv import load_dotenv + +load_dotenv() + +app = FastAPI() + +# Configure CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Should be set to the actual frontend URL in production + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Configure OpenAI client for DeepSeek API - updated to the officially recommended initialization +client = OpenAI( + api_key=os.environ.get("DEEPSEEK_API_KEY"), + base_url="https://api.deepseek.com" # Removed "/v1" according to official documentation +) + +# Available DeepSeek models +DEEPSEEK_MODELS = { + "deepseek-chat": "DeepSeek-V3 model, general conversation model", + "deepseek-reasoner": "DeepSeek-R1 reasoning model, excels at complex reasoning" +} + +# Data models +class Task(BaseModel): + id: Optional[int] = None + title: str + description: Optional[str] = None + due_date: Optional[str] = None + completed: bool = False + +class Message(BaseModel): + role: str # "user" or "assistant" + content: str + +class ChatRequest(BaseModel): + message: str + model: Optional[str] = "deepseek-chat" + history: Optional[List[Message]] = [] # Added history message list + +# Simple data storage +tasks = [] +task_id_counter = 1 + +# API routes +@app.get("/") +def read_root(): + return {"message": "Welcome to Time Management App"} + +@app.get("/models") +def get_models(): + """Get list of available DeepSeek models""" + return DEEPSEEK_MODELS + +@app.get("/tasks", response_model=List[Task]) +def get_tasks(): + return tasks + +@app.post("/tasks", response_model=Task) +def create_task(task: Task): + global task_id_counter + task.id = task_id_counter + tasks.append(task) + task_id_counter += 1 + return task + +@app.put("/tasks/{task_id}", response_model=Task) +def update_task(task_id: int, task: Task): + for i, t in enumerate(tasks): + if t.id == task_id: + task.id = task_id + tasks[i] = task + return task + raise HTTPException(status_code=404, detail="Task not found") + +@app.delete("/tasks/{task_id}") +def delete_task(task_id: int): + for i, task in enumerate(tasks): + if task.id == task_id: + del tasks[i] + return {"message": "Task deleted"} + raise HTTPException(status_code=404, detail="Task not found") + +def get_completion_stream(message: str, model: str = "deepseek-chat", history: List[Message] = None): + """Generate DeepSeek streaming response generator function""" + try: + # Validate if the model exists + if model not in DEEPSEEK_MODELS: + yield f"data: {{\"error\": \"Invalid model: {model}, Available models: {list(DEEPSEEK_MODELS.keys())}\" }}\n\n" + return + + # Build message list + messages = [{"role": "system", "content": "You are a time management expert assistant, please help the user manage time and tasks."}] + + # For DeepSeek Reasoner, ensure the first non-system message is a user message + # Check if there's history and it starts with an assistant message + if model == "deepseek-reasoner" and history and history[0].role == "assistant": + # Filter out assistant messages until we find a user message + filtered_history = [] + user_message_found = False + + for msg in history: + if msg.role == "user": + user_message_found = True + filtered_history.append(msg) + elif user_message_found: # Only include assistant messages after a user message + filtered_history.append(msg) + + # Use the filtered history + for msg in filtered_history: + messages.append({"role": msg.role, "content": msg.content}) + else: + # For other models, use the regular history + if history: + for msg in history: + messages.append({"role": msg.role, "content": msg.content}) + + # Add current message + messages.append({"role": "user", "content": message}) + + # Use OpenAI SDK to call DeepSeek API for streaming response + print(f"Sending message to DeepSeek (streaming): {message}") + print(f"Using model: {model}") + print(f"History message count: {len(history) if history else 0}") + print(f"Final message structure: {json.dumps(messages, indent=2)}") + + # Create streaming response + response_stream = client.chat.completions.create( + model=model, + messages=messages, + temperature=0.7, + max_tokens=2000, + stream=True # Enable streaming output + ) + + # Send model info as the first event + yield f"data: {{\"model\": \"{model}\"}}\n\n" + + # Send response tokens one by one + for chunk in response_stream: + if chunk.choices and chunk.choices[0].delta.content: + content = chunk.choices[0].delta.content + # Format as SSE event + yield f"data: {json.dumps({'content': content})}\n\n" + + # Send completion event + yield f"data: {{\"done\": true}}\n\n" + + except Exception as e: + error_message = str(e) + print(f"Streaming DeepSeek API error: {error_message}") + yield f"data: {{\"error\": \"{error_message}\" }}\n\n" + +@app.post("/chat") +def chat_with_deepseek(chat_request: ChatRequest): + try: + # Validate if the model exists + if chat_request.model not in DEEPSEEK_MODELS: + raise HTTPException(status_code=400, detail=f"Invalid model: {chat_request.model}, Available models: {list(DEEPSEEK_MODELS.keys())}") + + # Build message list + messages = [{"role": "system", "content": "You are a time management expert assistant, please help the user manage time and tasks."}] + + # For DeepSeek Reasoner, ensure the first non-system message is a user message + if chat_request.model == "deepseek-reasoner" and chat_request.history and chat_request.history[0].role == "assistant": + # Filter out assistant messages until we find a user message + filtered_history = [] + user_message_found = False + + for msg in chat_request.history: + if msg.role == "user": + user_message_found = True + filtered_history.append(msg) + elif user_message_found: # Only include assistant messages after a user message + filtered_history.append(msg) + + # Use the filtered history + for msg in filtered_history: + messages.append({"role": msg.role, "content": msg.content}) + else: + # For other models, use the regular history + if chat_request.history: + for msg in chat_request.history: + messages.append({"role": msg.role, "content": msg.content}) + + # Add current message + messages.append({"role": "user", "content": chat_request.message}) + + # Log request details + print(f"Sending message to DeepSeek: {chat_request.message}") + print(f"Using model: {chat_request.model}") + print(f"History message count: {len(chat_request.history) if chat_request.history else 0}") + print(f"Final message structure: {json.dumps(messages, indent=2)}") + + # Use format from official documentation + response = client.chat.completions.create( + model=chat_request.model, # Use the user selected model + messages=messages, + temperature=0.7, + max_tokens=2000, + stream=False + ) + + # Print complete response for debugging + print(f"DeepSeek response: {response}") + + # Return generated message content + return {"response": response.choices[0].message.content, "model": chat_request.model} + + except Exception as e: + print(f"DeepSeek API error: {str(e)}") + raise HTTPException(status_code=500, detail=f"Communication error with DeepSeek: {str(e)}") + +@app.post("/chat_stream") +async def chat_stream_post(chat_request: ChatRequest): + """Streaming chat interface - POST method""" + return StreamingResponse( + get_completion_stream(chat_request.message, chat_request.model, chat_request.history), + media_type="text/event-stream" + ) + +@app.get("/chat_stream") +async def chat_stream_get(message: str = Query(None), model: str = Query("deepseek-chat")): + """Streaming chat interface - GET method""" + if not message: + return {"error": "message parameter is required"} + + return StreamingResponse( + get_completion_stream(message, model), + media_type="text/event-stream" + ) + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/backend/run_sdk.py b/backend/run_sdk.py new file mode 100644 index 0000000..424a308 --- /dev/null +++ b/backend/run_sdk.py @@ -0,0 +1,5 @@ +import uvicorn + +if __name__ == "__main__": + print("使用OpenAI SDK调用DeepSeek API启动服务...") + uvicorn.run("main_openai_sdk:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d305d99..07b98cc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,8 +17,10 @@ "react-dom": "^18.2.0", "react-icons": "^4.11.0", "react-markdown": "^10.1.0", + "react-router-dom": "^7.5.3", "react-scripts": "5.0.1", "react-syntax-highlighter": "^15.6.1", + "recharts": "^2.15.3", "remark-gfm": "^4.0.1", "web-vitals": "^2.1.4" } @@ -4114,6 +4116,69 @@ "@types/node": "*" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -6326,6 +6391,15 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -7070,8 +7144,128 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -7183,6 +7377,12 @@ "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", "license": "MIT" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/decode-named-character-reference": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", @@ -7473,6 +7673,16 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -8750,6 +8960,15 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -10264,6 +10483,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -17110,6 +17338,54 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.3.tgz", + "integrity": "sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.3.tgz", + "integrity": "sha512-cK0jSaTyW4jV9SRKAItMIQfWZ/D6WEZafgHuuCb9g+SjhLolY78qc+De4w/Cz9ybjvLzShAmaIMEXt8iF1Cm+A==", + "license": "MIT", + "dependencies": { + "react-router": "7.5.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -17183,6 +17459,21 @@ } } }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-syntax-highlighter": { "version": "15.6.1", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", @@ -17200,6 +17491,22 @@ "react": ">= 0.14.0" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -17235,6 +17542,44 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz", + "integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -18227,6 +18572,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -19525,6 +19876,12 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "license": "MIT" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -19680,6 +20037,12 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -20189,6 +20552,28 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7cf5b7f..e20e5db 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,8 +12,10 @@ "react-dom": "^18.2.0", "react-icons": "^4.11.0", "react-markdown": "^10.1.0", + "react-router-dom": "^7.5.3", "react-scripts": "5.0.1", "react-syntax-highlighter": "^15.6.1", + "recharts": "^2.15.3", "remark-gfm": "^4.0.1", "web-vitals": "^2.1.4" }, diff --git a/frontend/src/App.css b/frontend/src/App.css index 4eb8631..dd19bdb 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -718,4 +718,199 @@ h1 { padding: 2rem; color: var(--gray-500); font-style: italic; +} + +/* 统计图表样式 */ +.stats-container { + background-color: white; + border-radius: var(--radius-lg); + padding: 2rem; + box-shadow: var(--shadow-lg); + transition: all var(--transition-speed) ease; + border: 1px solid var(--gray-200); +} + +.stats-container h2 { + color: var(--primary-color); + margin-bottom: 1.5rem; + font-size: 1.75rem; + position: relative; + padding-bottom: 0.5rem; +} + +.stats-container h2:after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 60px; + height: 3px; + background-color: var(--primary-color); +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.stats-card { + background-color: var(--gray-100); + border-radius: var(--radius-md); + padding: 1.5rem; + box-shadow: var(--shadow-sm); + transition: all var(--transition-speed) ease; + border: 1px solid var(--gray-200); +} + +.stats-card:hover { + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} + +.stats-card h3 { + color: var(--primary-color); + font-size: 1.25rem; + margin-bottom: 1rem; + text-align: center; +} + +.stats-card.full-width { + grid-column: 1 / -1; +} + +.chart-container { + margin-top: 1rem; + height: 100%; +} + +.stats-summary { + display: flex; + justify-content: space-around; + margin-bottom: 1.5rem; +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; +} + +.stat-value { + font-size: 2rem; + font-weight: 700; + color: var(--primary-color); +} + +.stat-label { + font-size: 0.875rem; + color: var(--gray-600); + margin-top: 0.25rem; +} + +/* 任务完成后的弹出统计图表 */ +.task-stats-popup { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + animation: fadeIn 0.3s ease; +} + +.task-stats-popup-content { + background-color: white; + width: 90%; + max-width: 1000px; + max-height: 90vh; + overflow-y: auto; + border-radius: var(--radius-lg); + position: relative; + padding: 2rem; + box-shadow: var(--shadow-lg); + animation: slideIn 0.4s ease; +} + +@keyframes slideIn { + from { + transform: translateY(50px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.close-stats-button { + position: absolute; + top: 1rem; + right: 1rem; + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: var(--gray-600); + transition: color var(--transition-speed) ease; + width: 2rem; + height: 2rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.close-stats-button:hover { + color: var(--danger-color); + background-color: rgba(229, 62, 62, 0.1); +} + +/* 响应式布局调整 */ +@media (max-width: 768px) { + .stats-grid { + grid-template-columns: 1fr; + } + + .task-stats-popup-content { + width: 95%; + padding: 1.5rem; + } + + .stat-value { + font-size: 1.5rem; + } +} + +.header-content { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 1rem; + margin-bottom: 1rem; + position: relative; + width: 100%; +} + +.logout-button { + position: absolute; + top: 0; + right: 1rem; + padding: 8px 16px; + background-color: #dc3545; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.3s ease; + z-index: 10; +} + +.logout-button:hover { + background-color: #c82333; } \ No newline at end of file diff --git a/frontend/src/App.js b/frontend/src/App.js index 8bf2f63..22d542d 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,6 +1,10 @@ import React, { useState, useEffect } from 'react'; +import { BrowserRouter as Router, Routes, Route, Navigate, useNavigate } from 'react-router-dom'; import TaskList from './components/TaskList'; import ChatAssistant from './components/ChatAssistant'; +import TaskStats from './components/TaskStats'; +import Login from './components/Login'; +import ProtectedRoute from './components/ProtectedRoute'; import axios from 'axios'; import DatePicker from 'react-datepicker'; import "react-datepicker/dist/react-datepicker.css"; @@ -8,7 +12,7 @@ import './App.css'; const API_URL = 'http://localhost:8000'; -function App() { +function MainApp() { const [activeTab, setActiveTab] = useState('chat'); const [tasks, setTasks] = useState([]); const [newTask, setNewTask] = useState({ @@ -18,6 +22,8 @@ function App() { completed: false }); const [dueDateTime, setDueDateTime] = useState(null); + const [showStats, setShowStats] = useState(false); + const navigate = useNavigate(); useEffect(() => { fetchTasks(); @@ -61,6 +67,11 @@ function App() { try { await axios.put(`${API_URL}/tasks/${updatedTask.id}`, updatedTask); fetchTasks(); + + // Show statistics when a task is marked as completed + if (updatedTask.completed) { + setShowStats(true); + } } catch (error) { console.error('Failed to update task:', error); } @@ -99,10 +110,20 @@ function App() { } }; + const handleLogout = () => { + localStorage.removeItem('isAuthenticated'); + navigate('/login'); + }; + return (