Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions site/public/native-professor-pages.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 10 additions & 4 deletions site/scripts/professor-pages.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -348,9 +348,15 @@ test('la ficha indexable lleva ProfilePage y la que no, solo migas de pan',async
assert.match(con,/<title>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"[^>]*>[^<]*<svg[\s\S]*?Escribir una reseña/,'review button must be secondary so connect is the only primary action');
assert.ok(body.indexOf('prof-public__connect')<body.indexOf('prof-public__columns'),'CTA must appear above the reviews');
assert.doesNotMatch(formBody(data),/\/app\/copilot\//);assert.doesNotMatch(directoryBody({professors:[],query:'',total:0,page:1,pages:1}),/\/app\/copilot\//);
assert.equal(body.match(/href="\/app\/asistente\/"/g)?.length,1);assert.match(body,/Pregúntale a Campus por Juan Carlos Morales Arevalo/);assert.match(body,/class="button"[^>]*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"[^>]*>[^<]*<svg[\s\S]*?Escribir una reseña/,'review button stays secondary');
assert.ok(body.indexOf('prof-public__ask')<body.indexOf('prof-public__columns'),'CTA must appear above the reviews');
assert.ok(body.indexOf('prof-public__ask')<body.indexOf('Conectar mi IA'),'ask comes before connect');
assert.doesNotMatch(body.match(/prof-public__ask[\s\S]*?<\/aside>/)[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/);}
});

11 changes: 11 additions & 0 deletions site/scripts/verify-analytics.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 7 additions & 1 deletion site/src/lib/professor-pages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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=()=>`<aside class="prof-public__connect" aria-label="Conectar Campus a tu IA"><span class="prof-public__connect-icon">${icon('message-circle')}</span><div><h2>Que tu IA te cuente cómo es este profesor</h2><p>Conecta Campus a ChatGPT, Claude o Gemini y pregúntale por sus opiniones y cursos, o por tus propias notas, sin salir del chat.</p></div>${link('/app/copilot/',`Conectar mi IA ${icon('arrow-right')}`,'button')}</aside>`;
const connectPanel=()=>`<aside class="prof-public__connect" aria-label="Conectar Campus a tu IA"><span class="prof-public__connect-icon">${icon('message-circle')}</span><div><h2>Que tu IA te cuente cómo es este profesor</h2><p>Conecta Campus a ChatGPT, Claude o Gemini y pregúntale por sus opiniones y cursos, o por tus propias notas, sin salir del chat.</p></div>${link('/app/copilot/',`Conectar mi IA ${icon('arrow-right')}`,'button prof-public__button-secondary')}</aside>`;

// 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=>`<aside class="prof-public__connect prof-public__ask" aria-label="Preguntar a Campus por este profesor"><span class="prof-public__connect-icon">${icon('sparkles')}</span><div><h2>Pregúntale a Campus por ${e(name)}</h2><p>Dinos qué quieres saber, por ejemplo cómo evalúa o si conviene con tu horario, y Campus responde con lo que dicen los estudiantes.</p></div>${link('/app/asistente/',`Preguntar a Campus ${icon('arrow-right')}`,'button')}</aside>`;

export function profileBody(data){
const p=data.professor,ratings=withProfeMatchSnapshots(data.externalRatings,p),score=combinedRating(data,ratings);
return `<div class="prof-public__profile" data-professor-profile>
<nav class="prof-public__crumbs" aria-label="Ruta de navegación">${link('/profes/upc/',`${icon('arrow-left')} Profesores UPC`)}<span>/</span><span>${e(p.name)}</span></nav>
<header class="prof-public__profile-header"><div><p class="prof-public__eyebrow">Profesor · UPC</p><h1>${e(p.name)}</h1><p class="prof-public__muted">Experiencias de estudiantes para elegir mejor.</p>${(()=>{const programs=teachingPrograms(data.teachingCourses??data.bannerCourses);return programs.length?`<ul class="prof-public__programs" aria-label="Programas en los que enseña">${programs.map(name=>`<li>${e(name)}</li>`).join('')}</ul>`:'';})()}<div class="prof-public__summary"><span>${score.average===null?icon('star'):`${starRow(score.average)} <strong>${e(displayScore(score.average))}/10</strong>`}${score.average===null?'Sin puntuaciones públicas':'promedio conjunto de fuentes'}</span><span>${score.sourceCount} ${score.sourceCount===1?'fuente':'fuentes'} con puntuaciones</span></div></div><div class="prof-public__header-actions">${link(writingURL(p.slug),`${icon('file-text')} Escribir una reseña`,'button prof-public__button-secondary')}<span class="prof-public__help">Tu nombre no se muestra.</span></div></header>
${askPanel(p.name)}
${connectPanel()}
<nav class="prof-public__section-nav" aria-label="En esta ficha">${link('#resenas','Opiniones y puntuaciones')}${link('#cursos','Cursos')}</nav>
<div class="prof-public__columns"><div class="prof-public__feed">${profileReviewsSection(data,ratings)}</div>
Expand Down
4 changes: 2 additions & 2 deletions site/src/scripts/analytics-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ const categories: Record<string, readonly string[]> = {
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'],
Expand Down
3 changes: 2 additions & 1 deletion site/src/scripts/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>; started: number; scroll: ScrollDepth } | undefined;
Expand Down Expand Up @@ -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. */ }
}
Expand Down
Loading