AI Welcome scan experience — backend + web profile
When a card is scanned, the public profile can open with a full-bleed, AI-generated welcome hero before the contact info — experience #10 from the scan-experiences brief, on the share-token experience selector foundation. Backend (cardclaws-backend): - migration 0005_welcome.sql: add cards.welcome JSONB; flows through PublicProfile via serde flatten. - POST /v1/cards/:id/welcome (owner-gated): refine + generate via the existing AiClient (nano-banana), store {kind, message, imageDataUrl}. - card_service::set_welcome + queries::update_welcome; CardRow gains `welcome`. - 2 tests (generate→public-profile, ownership); workspace gate green (116 tests). - Verified live with real Gemini end-to-end. Web (cardclaws-profile): - WelcomeHero.astro: full-bleed AI hero + greeting + "scroll to connect" cue. - [handle].astro renders it when the card has a welcome; experience selector (?x=profile suppresses) as the lever for the other experiences. - PublicProfile gains an optional `welcome`; astro check clean. MVP inlines the image as a data URL; production swaps to an R2 URL (isolated in the welcome column). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a66978910d
commit
5aba3ef1a1
@@ -21,6 +21,42 @@ pub async fn get_owned(state: &AppState, id: Uuid, user_id: Uuid) -> Result<Card
|
||||
Ok(card)
|
||||
}
|
||||
|
||||
/// Generate an AI welcome image (nano-banana) and attach it to the card the
|
||||
/// caller owns — shown on the public profile when the QR is scanned.
|
||||
pub async fn set_welcome(
|
||||
state: &AppState,
|
||||
id: Uuid,
|
||||
user_id: Uuid,
|
||||
prompt: &str,
|
||||
message: Option<&str>,
|
||||
style: Option<&str>,
|
||||
mood: Option<&str>,
|
||||
) -> Result<CardRow, AppError> {
|
||||
get_owned(state, id, user_id).await?;
|
||||
if prompt.trim().is_empty() {
|
||||
return Err(AppError::Validation("prompt is required".into()));
|
||||
}
|
||||
let brief = crate::ai::SceneBrief {
|
||||
scene: prompt.to_string(),
|
||||
style: style.map(str::to_string),
|
||||
mood: mood.map(str::to_string),
|
||||
};
|
||||
let refined = state.ai.refine_prompt(&brief).await.map_err(ai_to_app)?;
|
||||
let img = state.ai.generate_image(&refined).await.map_err(ai_to_app)?;
|
||||
let welcome = serde_json::json!({
|
||||
"kind": "image",
|
||||
"message": message.unwrap_or("Great to meet you"),
|
||||
"imageDataUrl": format!("data:{};base64,{}", img.mime_type, img.base64),
|
||||
});
|
||||
cards::update_welcome(&state.db, id, &welcome)
|
||||
.await
|
||||
.map_db()
|
||||
}
|
||||
|
||||
fn ai_to_app(e: crate::ai::AiError) -> AppError {
|
||||
AppError::Internal(e.to_string())
|
||||
}
|
||||
|
||||
pub async fn list(state: &AppState, user_id: Uuid) -> Result<Vec<CardRow>, AppError> {
|
||||
cards::list_by_owner(&state.db, user_id).await.map_db()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user