Docs & Roadmap updates
This commit is contained in:
631
Roadmap TBDs/ONBOARDING_IMPROVEMENTS.md
Normal file
631
Roadmap TBDs/ONBOARDING_IMPROVEMENTS.md
Normal file
@@ -0,0 +1,631 @@
|
||||
# Onboarding Process Improvements
|
||||
|
||||
This document outlines a comprehensive plan to improve the user onboarding experience in AI-Writer, focusing on the API key management and initial setup process.
|
||||
|
||||
## Current Issues
|
||||
|
||||
After analyzing the current onboarding process in `utils.api_key_manager`, several issues were identified:
|
||||
|
||||
### User Experience Issues
|
||||
|
||||
- **Complex Multi-step Process**: The onboarding is split across multiple steps without clear indication of progress or purpose
|
||||
- **Confusing Navigation**: Users can get lost between steps with no clear path forward
|
||||
- **Required vs. Optional**: No clear distinction between required and optional API keys
|
||||
- **No Skip Option**: Users must go through all steps even if some are not relevant to them
|
||||
- **Limited Guidance**: Insufficient contextual help for users unfamiliar with API keys
|
||||
|
||||
### Technical Issues
|
||||
|
||||
- **Inconsistent State Management**: Wizard state is initialized in multiple places
|
||||
- **Basic Validation**: API keys are only checked for non-emptiness, not actual validity
|
||||
- **Environment Variable Handling**: Not robust across different environments
|
||||
- **Error Handling**: Inconsistent error handling and user feedback
|
||||
- **No Testing Mechanism**: No way to test API keys during setup
|
||||
|
||||
### UI/Design Issues
|
||||
|
||||
- **Inconsistent Styling**: Visual inconsistency across different components
|
||||
- **Poor Mobile Experience**: Limited responsiveness for mobile users
|
||||
- **Visual Hierarchy**: Lack of clear visual distinction for important elements
|
||||
- **Help Text Visibility**: Instructions and help text are not prominent enough
|
||||
|
||||
## Proposed Improvements
|
||||
|
||||
### 1. Redesigned Onboarding Flow
|
||||
|
||||
#### Welcome Screen
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ Welcome to AI-Writer! 👋 │
|
||||
│ │
|
||||
│ Let's get you set up in just a few minutes. │
|
||||
│ │
|
||||
│ What would you like to do? │
|
||||
│ │
|
||||
│ ○ Quick Start (minimal setup) │
|
||||
│ ○ Complete Setup (all features) │
|
||||
│ ○ Import Configuration │
|
||||
│ │
|
||||
│ [Start Setup] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### API Key Setup Screen
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ Step 1 of 4: Connect AI Models │
|
||||
│ ━━━━━━━━━━○○○○ │
|
||||
│ │
|
||||
│ Required (choose at least one): │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ ⭐ OpenAI API Key │ │
|
||||
│ │ [••••••••••••••••••] │ │
|
||||
│ │ ✓ Validated successfully! │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ ⭐ Google Gemini API Key │ │
|
||||
│ │ [ ] │ │
|
||||
│ │ ℹ️ Not configured (optional) │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Optional: │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ Anthropic API Key (Coming Soon) │ │
|
||||
│ │ [ ] │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Skip Optional] [Test Keys] [Continue →] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Progress Summary Screen
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ Setup Complete! 🎉 │
|
||||
│ │
|
||||
│ Here's your configuration: │
|
||||
│ │
|
||||
│ AI Models: │
|
||||
│ ✅ OpenAI API - Connected │
|
||||
│ ❌ Google Gemini - Not configured │
|
||||
│ │
|
||||
│ Research Tools: │
|
||||
│ ✅ Tavily Search - Connected │
|
||||
│ ❌ Serper API - Not configured │
|
||||
│ │
|
||||
│ Publishing: │
|
||||
│ ❌ WordPress - Not configured │
|
||||
│ │
|
||||
│ You can change these settings anytime from │
|
||||
│ the Settings menu. │
|
||||
│ │
|
||||
│ [Edit Configuration] [Start Using AI-Writer] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2. Technical Improvements
|
||||
|
||||
#### Unified State Management
|
||||
|
||||
```python
|
||||
# Create a dedicated state manager class
|
||||
class OnboardingStateManager:
|
||||
"""Manages the state of the onboarding process."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the onboarding state."""
|
||||
if 'onboarding_state' not in st.session_state:
|
||||
st.session_state.onboarding_state = {
|
||||
'current_step': 1,
|
||||
'total_steps': 4,
|
||||
'completed_steps': set(),
|
||||
'api_keys': {},
|
||||
'validated_keys': {},
|
||||
'setup_mode': 'quick_start', # or 'complete'
|
||||
'setup_complete': False
|
||||
}
|
||||
|
||||
def get_state(self):
|
||||
"""Get the current onboarding state."""
|
||||
return st.session_state.onboarding_state
|
||||
|
||||
def update_state(self, updates):
|
||||
"""Update the onboarding state."""
|
||||
st.session_state.onboarding_state.update(updates)
|
||||
|
||||
def next_step(self):
|
||||
"""Move to the next step."""
|
||||
current = st.session_state.onboarding_state['current_step']
|
||||
total = st.session_state.onboarding_state['total_steps']
|
||||
|
||||
if current < total:
|
||||
st.session_state.onboarding_state['current_step'] += 1
|
||||
st.session_state.onboarding_state['completed_steps'].add(current)
|
||||
|
||||
def previous_step(self):
|
||||
"""Move to the previous step."""
|
||||
if st.session_state.onboarding_state['current_step'] > 1:
|
||||
st.session_state.onboarding_state['current_step'] -= 1
|
||||
|
||||
def skip_to_step(self, step):
|
||||
"""Skip to a specific step."""
|
||||
if 1 <= step <= st.session_state.onboarding_state['total_steps']:
|
||||
st.session_state.onboarding_state['current_step'] = step
|
||||
|
||||
def mark_complete(self):
|
||||
"""Mark the onboarding as complete."""
|
||||
st.session_state.onboarding_state['setup_complete'] = True
|
||||
```
|
||||
|
||||
#### Enhanced API Key Validation
|
||||
|
||||
```python
|
||||
async def validate_api_key(service, key):
|
||||
"""Validate an API key by making a test request."""
|
||||
try:
|
||||
if service == "openai":
|
||||
# Test OpenAI API key with a minimal request
|
||||
import openai
|
||||
client = openai.OpenAI(api_key=key)
|
||||
response = await client.models.list()
|
||||
return {"valid": True, "models": [model.id for model in response.data[:5]]}
|
||||
|
||||
elif service == "gemini":
|
||||
# Test Google Gemini API key
|
||||
import google.generativeai as genai
|
||||
genai.configure(api_key=key)
|
||||
models = genai.list_models()
|
||||
return {"valid": True, "models": [model.name for model in models]}
|
||||
|
||||
elif service == "tavily":
|
||||
# Test Tavily API key
|
||||
import requests
|
||||
response = requests.get(
|
||||
"https://api.tavily.com/health",
|
||||
headers={"x-api-key": key}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return {"valid": True, "status": "healthy"}
|
||||
else:
|
||||
return {"valid": False, "error": f"Status code: {response.status_code}"}
|
||||
|
||||
# Add more services as needed
|
||||
|
||||
except Exception as e:
|
||||
return {"valid": False, "error": str(e)}
|
||||
```
|
||||
|
||||
#### Secure API Key Storage
|
||||
|
||||
```python
|
||||
def save_api_keys(keys_dict):
|
||||
"""Save API keys securely."""
|
||||
try:
|
||||
# 1. Save to .env file
|
||||
env_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
|
||||
|
||||
# Read existing .env file
|
||||
env_contents = {}
|
||||
if os.path.exists(env_path):
|
||||
with open(env_path, 'r') as f:
|
||||
for line in f:
|
||||
if '=' in line:
|
||||
key, value = line.strip().split('=', 1)
|
||||
env_contents[key] = value
|
||||
|
||||
# Update with new keys
|
||||
for key_name, key_value in keys_dict.items():
|
||||
if key_value: # Only save non-empty keys
|
||||
env_key = f"{key_name.upper()}_API_KEY"
|
||||
env_contents[env_key] = key_value
|
||||
|
||||
# Write back to .env file
|
||||
with open(env_path, 'w') as f:
|
||||
for key, value in env_contents.items():
|
||||
f.write(f"{key}={value}\n")
|
||||
|
||||
# 2. Also store in session state for immediate use
|
||||
for key_name, key_value in keys_dict.items():
|
||||
if key_value:
|
||||
st.session_state[f"{key_name}_api_key"] = key_value
|
||||
|
||||
# 3. Set environment variables for current session
|
||||
for key_name, key_value in keys_dict.items():
|
||||
if key_value:
|
||||
os.environ[f"{key_name.upper()}_API_KEY"] = key_value
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving API keys: {str(e)}")
|
||||
return False
|
||||
```
|
||||
|
||||
### 3. UI/UX Improvements
|
||||
|
||||
#### Responsive Design
|
||||
|
||||
```python
|
||||
def render_responsive_layout():
|
||||
"""Render a responsive layout that works on mobile and desktop."""
|
||||
# Check viewport width
|
||||
st.markdown("""
|
||||
<script>
|
||||
var width = window.innerWidth;
|
||||
if (width < 768) {
|
||||
document.documentElement.style.setProperty('--layout', 'mobile');
|
||||
} else {
|
||||
document.documentElement.style.setProperty('--layout', 'desktop');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Mobile-first styles */
|
||||
.container {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Desktop adjustments */
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
```
|
||||
|
||||
#### Visual Hierarchy for Required vs Optional
|
||||
|
||||
```python
|
||||
def render_api_key_input(label, key_name, required=False, help_text=""):
|
||||
"""Render an API key input with clear visual hierarchy."""
|
||||
|
||||
# Add required indicator if needed
|
||||
display_label = f"{label} {'*' if required else '(optional)'}"
|
||||
|
||||
# Get existing value from session state or environment
|
||||
existing_value = st.session_state.get(f"{key_name}_api_key", "") or os.getenv(f"{key_name.upper()}_API_KEY", "")
|
||||
|
||||
# Render the input with appropriate styling
|
||||
st.markdown(f"""
|
||||
<div class="api-key-input {'required' if required else 'optional'}">
|
||||
<label>{display_label}</label>
|
||||
<div class="input-help-text">{help_text}</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# The actual input field
|
||||
value = st.text_input(
|
||||
label="", # Empty because we use custom label above
|
||||
value=existing_value,
|
||||
type="password",
|
||||
key=f"input_{key_name}",
|
||||
label_visibility="collapsed"
|
||||
)
|
||||
|
||||
# Validation status
|
||||
if value:
|
||||
is_valid = key_name in st.session_state.get("validated_keys", {})
|
||||
if is_valid:
|
||||
st.success(f"✓ {label} validated successfully")
|
||||
else:
|
||||
st.info(f"⚠️ {label} not validated yet")
|
||||
|
||||
return value
|
||||
```
|
||||
|
||||
#### Interactive Help and Tooltips
|
||||
|
||||
```python
|
||||
def render_help_section(service):
|
||||
"""Render an interactive help section for getting API keys."""
|
||||
|
||||
help_content = {
|
||||
"openai": {
|
||||
"title": "How to get your OpenAI API key",
|
||||
"steps": [
|
||||
"Go to [OpenAI's website](https://platform.openai.com)",
|
||||
"Sign up or log in to your account",
|
||||
"Navigate to the API section",
|
||||
"Click 'Create new secret key'",
|
||||
"Copy the generated key and paste it here"
|
||||
],
|
||||
"note": "Keep your API key secure and never share it publicly.",
|
||||
"pricing": "$0.002 per 1K tokens for GPT-3.5, $0.06 per 1K tokens for GPT-4",
|
||||
"link": "https://platform.openai.com/account/api-keys"
|
||||
},
|
||||
"gemini": {
|
||||
"title": "How to get your Google Gemini API key",
|
||||
"steps": [
|
||||
"Visit [Google AI Studio](https://makersuite.google.com/app/apikey)",
|
||||
"Sign in with your Google account",
|
||||
"Click 'Create API key'",
|
||||
"Copy the generated key and paste it here"
|
||||
],
|
||||
"note": "Make sure to enable the Gemini API in your Google Cloud Console.",
|
||||
"pricing": "Free tier available, then $0.0025 per 1K tokens",
|
||||
"link": "https://makersuite.google.com/app/apikey"
|
||||
}
|
||||
# Add more services as needed
|
||||
}
|
||||
|
||||
if service in help_content:
|
||||
content = help_content[service]
|
||||
|
||||
with st.expander(f"📋 {content['title']}", expanded=False):
|
||||
st.markdown("**Step-by-step guide:**")
|
||||
for i, step in enumerate(content["steps"], 1):
|
||||
st.markdown(f"{i}. {step}")
|
||||
|
||||
st.markdown(f"**Note:** {content['note']}")
|
||||
st.markdown(f"**Pricing:** {content['pricing']}")
|
||||
st.markdown(f"[Get your API key here]({content['link']})")
|
||||
```
|
||||
|
||||
### 4. Implementation Plan
|
||||
|
||||
#### Phase 1: Core Improvements
|
||||
|
||||
1. **Create Unified State Manager**
|
||||
- Implement the `OnboardingStateManager` class
|
||||
- Refactor existing code to use the new state manager
|
||||
- Add proper state persistence
|
||||
|
||||
2. **Enhance API Key Validation**
|
||||
- Implement real validation for each service
|
||||
- Add visual feedback for validation status
|
||||
- Create a "Test All Keys" function
|
||||
|
||||
3. **Improve Navigation**
|
||||
- Redesign step indicator with clear labels
|
||||
- Add skip options for optional steps
|
||||
- Implement a "Quick Start" mode
|
||||
|
||||
#### Phase 2: UI/UX Enhancements
|
||||
|
||||
1. **Redesign Input Components**
|
||||
- Create clear visual hierarchy
|
||||
- Add responsive design for mobile
|
||||
- Implement interactive help sections
|
||||
|
||||
2. **Create Summary Screens**
|
||||
- Add welcome screen with setup options
|
||||
- Implement completion summary screen
|
||||
- Add configuration export/import
|
||||
|
||||
3. **Enhance Visual Design**
|
||||
- Update color scheme for better accessibility
|
||||
- Add animations for transitions
|
||||
- Implement progress indicators
|
||||
|
||||
#### Phase 3: Advanced Features
|
||||
|
||||
1. **Guided Tours**
|
||||
- Add interactive tutorials
|
||||
- Create contextual help popups
|
||||
- Implement feature discovery
|
||||
|
||||
2. **Smart Defaults**
|
||||
- Suggest configurations based on user needs
|
||||
- Implement templates for common use cases
|
||||
- Add recommended settings
|
||||
|
||||
3. **Troubleshooting Assistance**
|
||||
- Add automatic error detection
|
||||
- Create guided troubleshooting flows
|
||||
- Implement self-healing for common issues
|
||||
|
||||
## Code Implementation Examples
|
||||
|
||||
### Welcome Screen Component
|
||||
|
||||
```python
|
||||
def render_welcome_screen():
|
||||
"""Render the welcome screen for onboarding."""
|
||||
st.markdown("""
|
||||
<div class="welcome-container">
|
||||
<h1>Welcome to AI-Writer! 👋</h1>
|
||||
<p class="welcome-subtitle">Let's get you set up in just a few minutes.</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Setup mode selection
|
||||
setup_mode = st.radio(
|
||||
"What would you like to do?",
|
||||
options=["Quick Start (minimal setup)",
|
||||
"Complete Setup (all features)",
|
||||
"Import Configuration"],
|
||||
index=0,
|
||||
key="setup_mode_selection"
|
||||
)
|
||||
|
||||
# Store the selection in state
|
||||
if "onboarding_state" in st.session_state:
|
||||
st.session_state.onboarding_state["setup_mode"] = setup_mode.split(" ")[0].lower()
|
||||
|
||||
# Start button
|
||||
if st.button("Start Setup", use_container_width=True, type="primary"):
|
||||
if "onboarding_state" in st.session_state:
|
||||
st.session_state.onboarding_state["current_step"] = 1
|
||||
st.rerun()
|
||||
```
|
||||
|
||||
### API Key Manager Component
|
||||
|
||||
```python
|
||||
def render_api_key_manager():
|
||||
"""Render the improved API key manager."""
|
||||
# Get state manager
|
||||
state_manager = OnboardingStateManager()
|
||||
state = state_manager.get_state()
|
||||
|
||||
# Render step indicator
|
||||
render_step_indicator(state["current_step"], state["total_steps"])
|
||||
|
||||
# Render appropriate step based on current_step
|
||||
if state["current_step"] == 1:
|
||||
render_ai_providers_step(state_manager)
|
||||
elif state["current_step"] == 2:
|
||||
render_research_tools_step(state_manager)
|
||||
elif state["current_step"] == 3:
|
||||
render_publishing_step(state_manager)
|
||||
elif state["current_step"] == 4:
|
||||
render_summary_step(state_manager)
|
||||
|
||||
# Render navigation buttons
|
||||
render_navigation_buttons(state_manager)
|
||||
```
|
||||
|
||||
### Improved AI Providers Step
|
||||
|
||||
```python
|
||||
def render_ai_providers_step(state_manager):
|
||||
"""Render the improved AI providers setup step."""
|
||||
st.markdown("## Step 1: Connect AI Models")
|
||||
st.markdown("Configure the AI models you want to use for content generation.")
|
||||
|
||||
# Create tabs for required vs optional
|
||||
tab1, tab2 = st.tabs(["Required (at least one)", "Optional Models"])
|
||||
|
||||
with tab1:
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
# OpenAI
|
||||
openai_key = render_api_key_input(
|
||||
"OpenAI API Key",
|
||||
"openai",
|
||||
required=True,
|
||||
help_text="Powers GPT-3.5 and GPT-4 models"
|
||||
)
|
||||
render_help_section("openai")
|
||||
|
||||
with col2:
|
||||
# Google Gemini
|
||||
gemini_key = render_api_key_input(
|
||||
"Google Gemini API Key",
|
||||
"gemini",
|
||||
required=False,
|
||||
help_text="Powers Gemini Pro models"
|
||||
)
|
||||
render_help_section("gemini")
|
||||
|
||||
with tab2:
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
# Anthropic
|
||||
anthropic_key = render_api_key_input(
|
||||
"Anthropic API Key",
|
||||
"anthropic",
|
||||
required=False,
|
||||
help_text="Powers Claude models"
|
||||
)
|
||||
render_help_section("anthropic")
|
||||
|
||||
with col2:
|
||||
# Mistral
|
||||
mistral_key = render_api_key_input(
|
||||
"Mistral API Key",
|
||||
"mistral",
|
||||
required=False,
|
||||
help_text="Powers Mistral models"
|
||||
)
|
||||
render_help_section("mistral")
|
||||
|
||||
# Test keys button
|
||||
if st.button("Test API Keys", use_container_width=True):
|
||||
with st.spinner("Testing API keys..."):
|
||||
# Test each provided key
|
||||
results = {}
|
||||
if openai_key:
|
||||
results["openai"] = asyncio.run(validate_api_key("openai", openai_key))
|
||||
if gemini_key:
|
||||
results["gemini"] = asyncio.run(validate_api_key("gemini", gemini_key))
|
||||
if anthropic_key:
|
||||
results["anthropic"] = asyncio.run(validate_api_key("anthropic", anthropic_key))
|
||||
if mistral_key:
|
||||
results["mistral"] = asyncio.run(validate_api_key("mistral", mistral_key))
|
||||
|
||||
# Store validation results
|
||||
state_manager.update_state({"validated_keys": results})
|
||||
|
||||
# Display results
|
||||
for service, result in results.items():
|
||||
if result.get("valid", False):
|
||||
st.success(f"✅ {service.title()} API key is valid")
|
||||
else:
|
||||
st.error(f"❌ {service.title()} API key is invalid: {result.get('error', 'Unknown error')}")
|
||||
|
||||
# Save keys to state
|
||||
api_keys = {
|
||||
"openai": openai_key,
|
||||
"gemini": gemini_key,
|
||||
"anthropic": anthropic_key,
|
||||
"mistral": mistral_key
|
||||
}
|
||||
state_manager.update_state({"api_keys": api_keys})
|
||||
|
||||
# Check if we have at least one valid key
|
||||
has_valid_key = any([
|
||||
openai_key and state_manager.get_state().get("validated_keys", {}).get("openai", {}).get("valid", False),
|
||||
gemini_key and state_manager.get_state().get("validated_keys", {}).get("gemini", {}).get("valid", False)
|
||||
])
|
||||
|
||||
if not has_valid_key and (openai_key or gemini_key):
|
||||
st.warning("Please test your API keys before continuing")
|
||||
```
|
||||
|
||||
## Benefits of Improved Onboarding
|
||||
|
||||
1. **Increased User Retention**
|
||||
- Smoother onboarding leads to higher completion rates
|
||||
- Clear guidance reduces frustration and abandonment
|
||||
- Faster time-to-value improves user satisfaction
|
||||
|
||||
2. **Reduced Support Burden**
|
||||
- Better self-service options decrease support tickets
|
||||
- Clearer instructions prevent common setup issues
|
||||
- Automated validation catches problems early
|
||||
|
||||
3. **Higher Feature Adoption**
|
||||
- Users understand available features better
|
||||
- Guided setup encourages exploration of capabilities
|
||||
- Contextual help improves feature discovery
|
||||
|
||||
4. **Improved User Experience**
|
||||
- Consistent design creates a professional impression
|
||||
- Responsive layout works across all devices
|
||||
- Intuitive navigation reduces cognitive load
|
||||
|
||||
5. **Better Data Quality**
|
||||
- Proper validation ensures working API keys
|
||||
- Clear requirements improve data completeness
|
||||
- Structured setup leads to better configuration
|
||||
|
||||
## Implementation Timeline
|
||||
|
||||
- **Week 1**: Design and prototype core improvements
|
||||
- **Week 2**: Implement unified state management and API validation
|
||||
- **Week 3**: Develop UI components and responsive design
|
||||
- **Week 4**: Create welcome and summary screens
|
||||
- **Week 5**: Add help content and contextual assistance
|
||||
- **Week 6**: Testing, refinement, and documentation
|
||||
|
||||
## Conclusion
|
||||
|
||||
The proposed improvements to the onboarding process will significantly enhance the user experience for new AI-Writer users. By implementing a more intuitive, guided, and responsive setup flow, we can increase user retention, reduce support needs, and help users get value from the platform faster.
|
||||
|
||||
These changes represent a comprehensive overhaul of the current system, addressing both technical and user experience issues while maintaining compatibility with the existing codebase.
|
||||
Reference in New Issue
Block a user