max / makenotwork
- Co-Authored-By
- Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8 files changed,
+137 insertions,
-9 deletions
| @@ -294,6 +294,32 @@ | |||
| 294 | 294 | Ok(()) | |
| 295 | 295 | } | |
| 296 | 296 | ||
| 297 | + | /// Update a project's AI content tier and disclosure. | |
| 298 | + | #[tracing::instrument(skip_all)] | |
| 299 | + | pub async fn update_project_ai_tier( | |
| 300 | + | pool: &PgPool, | |
| 301 | + | id: ProjectId, | |
| 302 | + | user_id: UserId, | |
| 303 | + | ai_tier: super::AiTier, | |
| 304 | + | ai_disclosure: Option<&str>, | |
| 305 | + | ) -> Result<()> { | |
| 306 | + | sqlx::query( | |
| 307 | + | r#" | |
| 308 | + | UPDATE projects | |
| 309 | + | SET ai_tier = $3, ai_disclosure = $4, updated_at = NOW() | |
| 310 | + | WHERE id = $1 AND user_id = $2 | |
| 311 | + | "#, | |
| 312 | + | ) | |
| 313 | + | .bind(id) | |
| 314 | + | .bind(user_id) | |
| 315 | + | .bind(ai_tier) | |
| 316 | + | .bind(ai_disclosure) | |
| 317 | + | .execute(pool) | |
| 318 | + | .await?; | |
| 319 | + | ||
| 320 | + | Ok(()) | |
| 321 | + | } | |
| 322 | + | ||
| 297 | 323 | /// Update a project's pricing model, price, and PWYW minimum. | |
| 298 | 324 | #[tracing::instrument(skip_all)] | |
| 299 | 325 | pub async fn update_project_pricing( |
| @@ -312,6 +312,8 @@ | |||
| 312 | 312 | pub features: Vec<String>, | |
| 313 | 313 | pub description: String, | |
| 314 | 314 | pub category_name: String, | |
| 315 | + | pub ai_tier: String, | |
| 316 | + | pub ai_disclosure: String, | |
| 315 | 317 | } | |
| 316 | 318 | ||
| 317 | 319 | /// Wizard step partial: project appearance (cover image upload). |
| @@ -76,6 +76,31 @@ | |||
| 76 | 76 | placeholder="Describe your project..."></textarea> | |
| 77 | 77 | </div> | |
| 78 | 78 | ||
| 79 | + | <div class="form-group"> | |
| 80 | + | <label>AI Disclosure</label> | |
| 81 | + | <div class="hint" style="margin-bottom: 0.5rem;">How was AI used in creating this project's content?</div> | |
| 82 | + | <div style="display: flex; flex-direction: column; gap: 0.5rem;"> | |
| 83 | + | <label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;"> | |
| 84 | + | <input type="radio" name="ai_tier" value="handmade" checked> | |
| 85 | + | <span><strong>Handmade</strong> — no AI tools used in content creation</span> | |
| 86 | + | </label> | |
| 87 | + | <label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;"> | |
| 88 | + | <input type="radio" name="ai_tier" value="assisted"> | |
| 89 | + | <span><strong>AI-Assisted</strong> — AI tools used as part of the creative process</span> | |
| 90 | + | </label> | |
| 91 | + | <label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;"> | |
| 92 | + | <input type="radio" name="ai_tier" value="generated"> | |
| 93 | + | <span><strong>AI-Generated</strong> — content primarily generated by AI</span> | |
| 94 | + | </label> | |
| 95 | + | </div> | |
| 96 | + | </div> | |
| 97 | + | ||
| 98 | + | <div class="form-group" id="ai-disclosure-group" style="display: none;"> | |
| 99 | + | <label for="wiz-ai-disclosure">Disclosure Details</label> | |
| 100 | + | <textarea id="wiz-ai-disclosure" name="ai_disclosure" rows="2" | |
| 101 | + | placeholder="Describe how AI was used..."></textarea> | |
| 102 | + | </div> | |
| 103 | + | ||
| 79 | 104 | <div class="wizard-actions"> | |
| 80 | 105 | <a href="/dashboard" class="secondary">Cancel</a> | |
| 81 | 106 | <button type="submit" class="primary">Continue</button> | |
| @@ -147,6 +172,14 @@ | |||
| 147 | 172 | input.addEventListener('blur', function() { | |
| 148 | 173 | setTimeout(function() { dropdown.classList.remove('open'); }, 150); | |
| 149 | 174 | }); | |
| 175 | + | ||
| 176 | + | // AI tier toggle | |
| 177 | + | document.querySelectorAll('input[name="ai_tier"]').forEach(function(r) { | |
| 178 | + | r.addEventListener('change', function() { | |
| 179 | + | var group = document.getElementById('ai-disclosure-group'); | |
| 180 | + | group.style.display = this.value === 'assisted' ? '' : 'none'; | |
| 181 | + | }); | |
| 182 | + | }); | |
| 150 | 183 | })(); | |
| 151 | 184 | </script> | |
| 152 | 185 | {% endblock %} |
| @@ -44,6 +44,10 @@ | |||
| 44 | 44 | pub pwyw_min_cents: Option<i32>, | |
| 45 | 45 | /// Whether this project requires license verification (phone-home) on content access. | |
| 46 | 46 | pub license_verification_enabled: bool, | |
| 47 | + | /// AI content tier: handmade, assisted, or generated. | |
| 48 | + | pub ai_tier: super::super::AiTier, | |
| 49 | + | /// Required disclosure text when ai_tier is assisted. | |
| 50 | + | pub ai_disclosure: Option<String>, | |
| 47 | 51 | } | |
| 48 | 52 | ||
| 49 | 53 | /// A git repository tracked on disk, optionally linked to a project. |
| @@ -86,17 +86,14 @@ | |||
| 86 | 86 | ))); | |
| 87 | 87 | } | |
| 88 | 88 | ||
| 89 | - | // Validate AI tier disclosure | |
| 90 | - | let ai_tier = req.ai_tier.unwrap_or(AiTier::Handmade); | |
| 89 | + | // Inherit AI tier from project if not specified on the item | |
| 90 | + | let ai_tier = req.ai_tier.unwrap_or(project.ai_tier); | |
| 91 | 91 | let ai_disclosure = match ai_tier { | |
| 92 | 92 | AiTier::Assisted => { | |
| 93 | - | let text = req.ai_disclosure.as_deref().unwrap_or("").trim(); | |
| 94 | - | if text.is_empty() { | |
| 95 | - | return Err(AppError::Validation( | |
| 96 | - | "AI disclosure is required for Assisted tier items".to_string(), | |
| 97 | - | )); | |
| 98 | - | } | |
| 99 | - | Some(text.to_string()) | |
| 93 | + | let text = req.ai_disclosure.as_deref() | |
| 94 | + | .or(project.ai_disclosure.as_deref()) | |
| 95 | + | .unwrap_or("").trim(); | |
| 96 | + | if text.is_empty() { None } else { Some(text.to_string()) } | |
| 100 | 97 | } | |
| 101 | 98 | _ => None, | |
| 102 | 99 | }; |