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
@@ -223,3 +223,61 @@ async fn duplicate_creates_independent_draft() {
|
||||
assert_ne!(body["handle"].as_str().unwrap(), handle);
|
||||
assert_eq!(body["status"], "draft");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn welcome_generates_and_appears_on_public_profile() {
|
||||
let app = require_app!();
|
||||
let token = app.register_and_token().await;
|
||||
let (id, handle) = create_card(&app, &token).await;
|
||||
|
||||
// Generate the AI welcome (FakeAiClient returns a 1x1 png).
|
||||
let (status, body) = app
|
||||
.request(
|
||||
"POST",
|
||||
&format!("/v1/cards/{id}/welcome"),
|
||||
Some(&token),
|
||||
Some(json!({ "prompt": "a calm forest at dawn", "message": "Great to meet you, I'm Omar" })),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::OK, "set welcome failed: {body}");
|
||||
assert_eq!(body["welcome"]["kind"], "image");
|
||||
assert!(body["welcome"]["imageDataUrl"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with("data:image/png;base64,"));
|
||||
|
||||
// Publish -> the public profile exposes the welcome.
|
||||
app.request(
|
||||
"POST",
|
||||
&format!("/v1/cards/{id}/publish"),
|
||||
Some(&token),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let (status, profile) = app
|
||||
.request("GET", &format!("/v1/cards/handle/{handle}"), None, None)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(profile["welcome"]["message"], "Great to meet you, I'm Omar");
|
||||
assert!(profile["welcome"]["imageDataUrl"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with("data:image/png"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn welcome_requires_ownership() {
|
||||
let app = require_app!();
|
||||
let owner = app.register_and_token().await;
|
||||
let (id, _handle) = create_card(&app, &owner).await;
|
||||
let other = app.register_and_token().await;
|
||||
let (status, _) = app
|
||||
.request(
|
||||
"POST",
|
||||
&format!("/v1/cards/{id}/welcome"),
|
||||
Some(&other),
|
||||
Some(json!({ "prompt": "x" })),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user