diff --git a/site/public/native-professor-pages.css b/site/public/native-professor-pages.css
index f45b9e5b..b9bef9b6 100644
--- a/site/public/native-professor-pages.css
+++ b/site/public/native-professor-pages.css
@@ -164,6 +164,6 @@ body:has([data-professor-profile])>.site-header.is-scrolled{position:relative;to
.prof-public__connect>div{flex:1;min-width:0}
.prof-public__connect h2{margin:0 0 4px;font-size:.98rem}
.prof-public__connect p{margin:0;font-size:.82rem;line-height:1.6;color:var(--muted)}
-.prof-public__connect .button{flex:none;white-space:nowrap}
+.prof-public__connect .button{flex:none;white-space:nowrap}.prof-public__ask{background:#fff;border-color:#23656b}.prof-public__ask+.prof-public__connect{padding:12px 20px}
@media(max-width:640px){.prof-public__connect{flex-direction:column;align-items:stretch;text-align:left}.prof-public__connect-icon{display:none}.prof-public__connect .button{text-align:center;justify-content:center}}
-.prof-public__header-actions .button.prof-public__button-secondary{background:#fff;color:#1b3a46;border:1px solid #b9cfd1;box-shadow:none}.prof-public__header-actions .button.prof-public__button-secondary:hover{background:#f1f8f8}
+.prof-public__connect .button.prof-public__button-secondary,.prof-public__header-actions .button.prof-public__button-secondary{background:#fff;color:#1b3a46;border:1px solid #b9cfd1;box-shadow:none}.prof-public__header-actions .button.prof-public__button-secondary:hover{background:#f1f8f8}
diff --git a/site/scripts/professor-pages.test.mjs b/site/scripts/professor-pages.test.mjs
index 47d07fdc..ab0c3a89 100644
--- a/site/scripts/professor-pages.test.mjs
+++ b/site/scripts/professor-pages.test.mjs
@@ -348,9 +348,15 @@ test('la ficha indexable lleva ProfilePage y la que no, solo migas de pan',async
assert.match(con,/
Ana Prueba \(UPC\): 7\/10 · 3 opiniones \| Campus<\/title>/);
});
-test('profile invites visitors to connect their AI, and only on the profile page',()=>{
+test('profile invites visitors to ask Campus (primary) and to connect their AI (secondary), only on the profile page',()=>{
const body=profileBody(data);
- assert.equal(body.match(/href="\/app\/copilot\/"/g)?.length,1);assert.match(body,/Conectar mi IA/);assert.match(body,/class="button prof-public__button-secondary"[^>]*>[^<]*]*href="\/app\/asistente\/"|href="\/app\/asistente\/"[^>]*class="button"/);
+ assert.equal(body.match(/href="\/app\/copilot\/"/g)?.length,1);assert.match(body,/Conectar mi IA/);
+ assert.match(body,/class="button prof-public__button-secondary" href="\/app\/copilot\/"|href="\/app\/copilot\/"[^>]*prof-public__button-secondary/,'connect becomes secondary so ask is the only primary action');
+ assert.match(body,/class="button prof-public__button-secondary"[^>]*>[^<]*/)[0],/ - |—/,'no dash separators in copy');
+ for(const other of [formBody(data),directoryBody({professors:[],query:'',total:0,page:1,pages:1})]){assert.doesNotMatch(other,/\/app\/copilot\/|\/app\/asistente\/|prof-public__ask/);}
});
+
diff --git a/site/scripts/verify-analytics.mjs b/site/scripts/verify-analytics.mjs
index 18696e96..ff8fb8fc 100644
--- a/site/scripts/verify-analytics.mjs
+++ b/site/scripts/verify-analytics.mjs
@@ -75,6 +75,7 @@ assert.deepEqual(safeAnalyticsProperties({ current_plan: 'plus', selected_plan:
});
assert.deepEqual(safeAnalyticsProperties({failure_reason:'not_found',callback_requested:true,whatsappPhone:'+51999999999',phone:'+51999999999',details:'private complaint',whatsappConsent:true}),{failure_reason:'not_found',callback_requested:true});
assert.deepEqual(safeAnalyticsProperties({failure_reason:'private complaint'}),{});
+assert.deepEqual(safeAnalyticsProperties({location:'professor_profile',target:'assistant',intent:'ask',slug:'ana-prueba'}),{location:'professor_profile',target:'assistant',intent:'ask'});
assert.equal(analyticsPage('/app/asistente/'), 'assistant');
assert.equal(analyticsPage('/app/asistente/c/private-id/'), 'assistant');
assert.equal(analyticsPage('/app/profes/c/private-id/'), 'assistant');
@@ -259,6 +260,16 @@ try {
listeners.get('click')({ target: new AppBrand('/app/plus/') });
assert.equal(JSON.parse(storage.get('campus-analytics-debug')).at(-1).properties.intent, 'plus', 'A Free upgrade remains an acquisition');
assert.equal(dwell.selectedPlan(), 'plus');
+ class ProfileAsk extends AppBrand {
+ closest(selector) { return selector === 'a[href]' ? this : selector === '.prof-public__ask' ? {} : null; }
+ }
+ location.pathname = '/profes/upc/ana-prueba/';
+ listeners.get('click')({ target: new ProfileAsk('/app/asistente/') });
+ assert.deepEqual(
+ (({ location, target, intent }) => ({ location, target, intent }))(JSON.parse(storage.get('campus-analytics-debug')).at(-1).properties),
+ { location: 'professor_profile', target: 'assistant', intent: 'ask' }, 'The professor profile CTA reaches PostHog with allowlisted location, target and intent');
+ assert.equal(dwell.selectedPlan(), 'plus', 'Asking Campus from a profile is not a plan selection');
+ location.pathname = '/app/asistente/';
dwell.track('campus_question_retried', { step: 'question', message: 'private' });
assert.equal(JSON.parse(storage.get('campus-analytics-debug')).at(-1).event, 'campus_question_retried');
assert.equal('message' in JSON.parse(storage.get('campus-analytics-debug')).at(-1).properties, false);
diff --git a/site/src/lib/professor-pages.mjs b/site/src/lib/professor-pages.mjs
index 8dc02ab2..fe740222 100644
--- a/site/src/lib/professor-pages.mjs
+++ b/site/src/lib/professor-pages.mjs
@@ -294,13 +294,19 @@ export function profileSEO(data){
// Invitación a instalar el MCP desde las fichas: el tráfico de Google llega con una pregunta
// sobre un profesor y hoy se va sin un siguiente paso. El clic lo mide analytics.ts como
// campus_cta_clicked con target "connect" (ruta /app/copilot/) en la familia professor_profile.
-const connectPanel=()=>`${icon('message-circle')} Que tu IA te cuente cómo es este profesor Conecta Campus a ChatGPT, Claude o Gemini y pregúntale por sus opiniones y cursos, o por tus propias notas, sin salir del chat.
${link('/app/copilot/',`Conectar mi IA ${icon('arrow-right')}`,'button')} `;
+const connectPanel=()=>`${icon('message-circle')} Que tu IA te cuente cómo es este profesor Conecta Campus a ChatGPT, Claude o Gemini y pregúntale por sus opiniones y cursos, o por tus propias notas, sin salir del chat.
${link('/app/copilot/',`Conectar mi IA ${icon('arrow-right')}`,'button prof-public__button-secondary')} `;
+
+// Camino a una cuenta desde la ficha: /app/asistente/ ofrece la pregunta de muestra al invitado cuando el
+// backend la tiene (brazo sample) y, si no, la pantalla de acceso. El clic lo mide analytics.ts como
+// campus_cta_clicked con location "professor_profile", target "assistant" e intent "ask".
+const askPanel=name=>``;
export function profileBody(data){
const p=data.professor,ratings=withProfeMatchSnapshots(data.externalRatings,p),score=combinedRating(data,ratings);
return `
${link('/profes/upc/',`${icon('arrow-left')} Profesores UPC`)}/ ${e(p.name)}
+ ${askPanel(p.name)}
${connectPanel()}
${link('#resenas','Opiniones y puntuaciones')}${link('#cursos','Cursos')}
${profileReviewsSection(data,ratings)}
diff --git a/site/src/scripts/analytics-contract.ts b/site/src/scripts/analytics-contract.ts
index 2c69edc5..81b42f4f 100644
--- a/site/src/scripts/analytics-contract.ts
+++ b/site/src/scripts/analytics-contract.ts
@@ -31,9 +31,9 @@ const categories: Record
= {
feedback_kind:['agent','plus'],reason:['resolved','not_resolved','price','unclear_value','payment_difficult','not_needed','technical_problem','other'],
page: ['home', 'ucal', 'app', 'assistant', 'connect', 'profile', 'turnitin_app', 'plus', 'turnitin', 'professors', 'blackboard_upc', 'chatgpt', 'terminal', 'cli', 'mcp', 'blog', 'privacy', 'professor_directory', 'professor_profile', 'professor_review_form', 'professor_reviews', 'library', 'library_item', 'library_app', 'campus_reviews', 'about', 'contact', 'terms', 'independence', 'other'],
target: ['ucal', 'app', 'assistant', 'connect', 'profile', 'turnitin_app', 'plus', 'turnitin', 'professors', 'blackboard_upc', 'chatgpt', 'terminal', 'cli', 'mcp', 'blog', 'privacy', 'professor_directory', 'professor_profile', 'professor_review_form', 'professor_reviews', 'library', 'library_item', 'library_app', 'campus_reviews', 'about', 'contact', 'terms', 'independence', 'other', 'plans', 'features', 'whatsapp', 'auth'],
- location: ['header', 'hero', 'trial', 'free', 'plus', 'features', 'research', 'onboarding', 'faq', 'closing', 'footer', 'app', 'other'],
+ location: ['header', 'hero', 'trial', 'free', 'plus', 'features', 'research', 'onboarding', 'faq', 'closing', 'footer', 'app', 'professor_profile', 'other'],
section: ['plans', 'features', 'research', 'onboarding', 'faq', 'closing'],
- intent: ['trial', 'free', 'plus', 'connect', 'turnitin', 'signup', 'support', 'navigate'],
+ intent: ['trial', 'free', 'plus', 'connect', 'turnitin', 'signup', 'support', 'navigate', 'ask'],
selected_plan: ['trial', 'free', 'plus'], current_plan: ['guest', 'free', 'plus', 'unavailable'],
plus_status: ['payment_review', 'active', 'rejected'], offer: ['founder', 'standard'],
step: ['plus', 'session', 'university', 'question', 'connector', 'upload', 'turnitin', 'document', 'payment', 'report', 'history'],
diff --git a/site/src/scripts/analytics.ts b/site/src/scripts/analytics.ts
index 0eb7c653..144a98b0 100644
--- a/site/src/scripts/analytics.ts
+++ b/site/src/scripts/analytics.ts
@@ -218,6 +218,7 @@ function locationFor(element: Element): string {
if (element.closest('.cp-hero')) return 'hero';
if (element.closest('#investigacion')) return 'research';
if (element.closest('.cp-final')) return 'closing';
+ if (element.closest('.prof-public__ask')) return 'professor_profile';
return location.pathname.startsWith('/app/') ? 'app' : 'other';
}
let webView: { properties: Record; started: number; scroll: ScrollDepth } | undefined;
@@ -298,7 +299,7 @@ export function setupAnalytics(): void {
const target = support ? 'whatsapp' : provider ? 'connect' : url.pathname === '/api/app/auth/start' ? 'auth'
: url.hash === '#planes' ? 'plans' : url.hash === '#incluye' ? 'features' : analyticsPage(url.pathname);
const managingPlus = anchor.hasAttribute('data-profile-manage') || currentPlan === 'plus';
- const intent = support ? 'support' : target === 'plus' ? managingPlus ? 'navigate' : 'plus' : plan ?? (target === 'auth' ? 'signup' : target === 'connect' ? 'connect' : ['turnitin_app', 'turnitin'].includes(target) ? 'turnitin' : target === 'app' && loc !== 'app' ? 'trial' : 'navigate');
+ const intent = support ? 'support' : loc === 'professor_profile' && target === 'assistant' ? 'ask' : target === 'plus' ? managingPlus ? 'navigate' : 'plus' : plan ?? (target === 'auth' ? 'signup' : target === 'connect' ? 'connect' : ['turnitin_app', 'turnitin'].includes(target) ? 'turnitin' : target === 'app' && loc !== 'app' ? 'trial' : 'navigate');
if (['trial', 'free', 'plus'].includes(intent)) {
try { sessionStorage.setItem('campus-selected-plan', intent); } catch { /* Optional. */ }
}