devaanand commited on
Commit
8ed8f32
·
1 Parent(s): 8cc4299

feat: add welcome page for first-time users and update routing

Browse files

- Introduced a new WelcomePage component to guide first-time visitors through the platform's features and usage.
- Updated App component to conditionally render the WelcomePage based on user navigation and localStorage state.
- Modified test cases to include the new /welcome route.
- Updated styles to accommodate the new welcome page layout and design.
- Changed asset references in index.html for updated frontend build.

backend/app/main.py CHANGED
@@ -927,6 +927,17 @@ def admin_page():
927
  return FileResponse(index, media_type="text/html")
928
 
929
 
 
 
 
 
 
 
 
 
 
 
 
930
  if SAMPLES_DIR.exists():
931
  app.mount("/samples", StaticFiles(directory=SAMPLES_DIR), name="samples")
932
 
 
927
  return FileResponse(index, media_type="text/html")
928
 
929
 
930
+ @app.get("/welcome", include_in_schema=False)
931
+ def welcome_page():
932
+ """Serve the SPA at /welcome; the frontend renders the landing page there
933
+ (what CCR is, who runs the platform, links to /guide and /product).
934
+ First-time visitors to / are shown it automatically."""
935
+ index = Path(__file__).resolve().parent.parent / "static" / "index.html"
936
+ if not index.exists():
937
+ raise HTTPException(404, "UI not built.")
938
+ return FileResponse(index, media_type="text/html")
939
+
940
+
941
  if SAMPLES_DIR.exists():
942
  app.mount("/samples", StaticFiles(directory=SAMPLES_DIR), name="samples")
943
 
backend/static/assets/index-BSotcTj4.js DELETED
The diff for this file is too large to render. See raw diff
 
backend/static/assets/{index-CN_FzJfm.css → index-DAnNOmJI.css} RENAMED
@@ -1 +1 @@
1
- :root{--maroon: #7a1f3d;--maroon-dark: #5e1730;--ink: #1d2129;--muted: #667085;--line: #e5e7eb;--bg: #f7f7f8;--card: #ffffff;--ok: #157f3d;--err: #b42318;--accent-soft: #f6ebef;--control-height: 40px}*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;color:var(--ink);background:var(--bg);font-size:14.5px;line-height:1.5}.app{display:flex;flex-direction:column;min-height:100vh}.header{background:var(--maroon);color:#fff;padding:14px 28px;display:flex;align-items:baseline;flex-wrap:wrap;gap:14px}.header h1{flex:0 0 auto;font-size:17px;margin:0;font-weight:650;letter-spacing:.2px;white-space:nowrap}.header .sub{flex:1 1 280px;min-width:0;font-size:12.5px;opacity:.85}.layout{display:flex;flex:1;min-width:0;min-height:0}.sidebar{width:250px;background:var(--card);border-right:1px solid var(--line);padding:18px 14px;flex-shrink:0;display:flex;flex-direction:column;min-height:0}.sidebar h2{font-size:11.5px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);margin:0 0 10px 4px;display:flex;align-items:center;gap:8px}.sidebar h2 .count{background:var(--bg);border:1px solid var(--line);border-radius:999px;padding:0 8px;font-size:10.5px;letter-spacing:0;color:var(--muted)}.sidebar-filter{width:100%;padding:7px 10px;margin-bottom:10px;border:1px solid var(--line);border-radius:8px;font:inherit;font-size:13px;background:#fff;color:var(--ink)}.sidebar-filter:focus{outline:none;border-color:var(--maroon)}.project-list{flex:1;min-height:0;overflow-y:auto;margin:0 -4px;padding:0 4px 4px}.group-label{font-size:10.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);margin:10px 4px 5px}div:first-child>.group-label{margin-top:2px}.project-item{display:block;width:100%;text-align:left;padding:9px 12px;margin-bottom:5px;border:1px solid transparent;border-radius:8px;background:none;cursor:pointer;font:inherit;color:var(--ink)}.project-item:hover{background:var(--bg)}.project-item.active{background:var(--accent-soft);border-color:var(--maroon);font-weight:600}.project-item .project-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.project-item .date{display:block;font-size:11.5px;color:var(--muted);font-weight:400}.project-create{margin-top:12px;padding-top:12px;border-top:1px solid var(--line)}.project-create>button{width:100%}.main{flex:1;padding:22px 28px;overflow-y:auto;min-width:0}.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:18px 20px;margin-bottom:16px}.card h3{margin:0 0 4px;font-size:15px}.card .hint{color:var(--muted);font-size:12.5px;margin:0 0 12px}.step-badge{display:inline-flex;align-items:center;justify-content:center;width:21px;height:21px;border-radius:50%;background:var(--maroon);color:#fff;font-size:12px;font-weight:700;margin-right:8px;vertical-align:-3px}button{white-space:nowrap}button.primary{background:var(--maroon);color:#fff;border:none;min-height:var(--control-height);display:inline-flex;align-items:center;justify-content:center;padding:0 18px;border-radius:8px;font:inherit;font-weight:600;cursor:pointer;line-height:1.2}button.primary:hover{background:var(--maroon-dark)}button.primary:disabled{background:#c9ccd1;cursor:not-allowed}a.google-btn{background:var(--maroon);color:#fff;border:none;min-height:var(--control-height);display:flex;align-items:center;justify-content:center;padding:0 18px;border-radius:8px;font:inherit;font-weight:600;cursor:pointer;line-height:1.2;text-decoration:none;width:100%;box-sizing:border-box}a.google-btn:hover{background:var(--maroon-dark)}button.ghost{background:none;border:1px solid var(--line);color:var(--ink);min-height:var(--control-height);display:inline-flex;align-items:center;justify-content:center;padding:0 14px;border-radius:8px;font:inherit;cursor:pointer;line-height:1.2}button.ghost:hover{border-color:var(--maroon);color:var(--maroon)}button.linkish{background:none;border:none;color:var(--maroon);font:inherit;cursor:pointer;padding:0;text-decoration:underline}input[type=text],input[type=email],input[type=password],textarea,select{width:100%;padding:8px 10px;border:1px solid var(--line);border-radius:8px;font:inherit;background:#fff;color:var(--ink)}input[type=text],input[type=email],input[type=password],select{height:var(--control-height)}button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid rgba(122,31,61,.38);outline-offset:2px}input[type=file]{display:block;max-width:100%;margin-top:6px;font-size:13px;color:var(--muted)}input[type=file]::file-selector-button{background:#fff;border:1px solid var(--line);color:var(--ink);padding:7px 14px;border-radius:8px;font:inherit;font-size:13px;cursor:pointer;margin-right:10px}input[type=file]::file-selector-button:hover{border-color:var(--maroon);color:var(--maroon)}.row>button{align-self:flex-end;margin-bottom:1px}textarea{resize:vertical}label.field{display:block;margin-bottom:10px;font-size:13px;font-weight:600}label.field>*{margin-top:4px;font-weight:400}.field-hint{margin-top:0;font-weight:400;color:var(--muted);font-size:12px}.row{display:flex;gap:14px;flex-wrap:wrap}.row>*{min-width:0}.row>.grow{flex:1;min-width:min(220px,100%)}.language-control{min-width:170px}.model-control{min-width:260px}.run-settings{display:grid;grid-template-columns:minmax(160px,230px) minmax(320px,720px) max-content;justify-content:start;gap:14px;align-items:end}.run-settings .field{margin-bottom:0}.run-button{min-width:180px;height:var(--control-height)}.construct-row{display:grid;grid-template-columns:minmax(320px,1120px) max-content;justify-content:start;align-items:end;gap:14px}.results-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:14px;flex-wrap:wrap}.result-actions{justify-content:flex-end}.result-actions a{display:inline-flex;text-decoration:none}.pill{display:inline-block;padding:2px 10px;border-radius:999px;font-size:11.5px;font-weight:600}.pill.completed{background:#e6f4ea;color:var(--ok)}.pill.running{background:#fff3e0;color:#b45309}.pill.queued{background:#eef2f7;color:var(--muted)}.pill.failed{background:#fdecea;color:var(--err)}.progress-track{background:var(--line);border-radius:999px;height:7px;overflow:hidden}.progress-fill{background:var(--maroon);height:100%;transition:width .4s ease}.warnings{background:#fff8e6;border:1px solid #f2dfa8;color:#7a5b00;border-radius:8px;padding:10px 14px}.error-banner{background:#fdecea;color:var(--err);border:1px solid #f5c6c0;padding:10px 14px;border-radius:8px;margin-bottom:14px;font-size:13px}.table-wrap{width:100%;overflow-x:auto}table.docs{width:100%;border-collapse:collapse;font-size:13px}table.docs th{text-align:left;color:var(--muted);font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;padding:6px 8px;border-bottom:1px solid var(--line)}table.docs td{padding:7px 8px;border-bottom:1px solid var(--bg);vertical-align:top;overflow-wrap:anywhere}table.docs td.score{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap}.stat-grid{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:4px}.stat{flex:1;min-width:110px;background:var(--bg);border-radius:8px;padding:10px 14px}.stat .v{font-size:20px;font-weight:700;font-variant-numeric:tabular-nums}.stat .k{font-size:11.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}.item-bar-row{display:flex;align-items:center;gap:10px;margin-bottom:7px}.item-bar-label{flex:1;font-size:12.5px;min-width:0;overflow-wrap:anywhere}.item-bar-track{flex:1.2;background:var(--bg);border-radius:999px;height:10px}.item-bar-fill{background:var(--maroon);opacity:.85;height:100%;border-radius:999px}.item-bar-val{width:52px;text-align:right;font-variant-numeric:tabular-nums;font-size:12.5px;font-weight:600}.construct-items{margin:8px 0 0;padding-left:20px;color:var(--muted);font-size:12.5px}.construct-items li{margin-bottom:2px}.meta-footer{font-size:12px;color:var(--muted);background:var(--bg);border-radius:8px;padding:10px 14px;margin-top:14px;font-variant-numeric:tabular-nums}.meta-footer code{font-size:11.5px}.muted{color:var(--muted)}.small{font-size:12.5px}.mt{margin-top:12px}.header-auth{margin-left:auto;display:flex;align-items:center;gap:10px;color:#fff}.header-btn{background:#ffffff1f;color:#fff;border:1px solid rgba(255,255,255,.45);padding:5px 14px;border-radius:7px;font:inherit;font-size:13px;cursor:pointer}.header-btn:hover{background:#ffffff38}.project-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px;flex-wrap:wrap}.project-title{font-size:17px;font-weight:650;margin-right:10px}button.danger{color:var(--err);border-color:#f0c4be}button.danger:hover{color:var(--err);border-color:var(--err)}button.danger-solid{background:var(--err)}button.danger-solid:hover{background:#93261b}button.danger-solid:disabled{background:#c9ccd1}.picker{position:relative}.picker-display{width:100%;display:flex;align-items:center;gap:10px;padding:8px 12px;border:1px solid var(--line);border-radius:8px;background:#fff;font:inherit;color:var(--ink);cursor:pointer;text-align:left}.picker-display:hover{border-color:var(--maroon)}.picker-display .picker-caret{margin-left:auto;color:var(--muted);font-size:11px}.picker-search{width:100%;padding:8px 12px;border:1px solid var(--maroon);border-radius:8px;font:inherit;background:#fff}.picker-search:focus{outline:none;box-shadow:0 0 0 3px #7a1f3d1f}.picker-panel{position:absolute;top:calc(100% + 6px);left:0;z-index:50;width:100%;max-width:640px;background:var(--card);border:1px solid var(--line);border-radius:10px;box-shadow:0 14px 40px #0000002e;max-height:340px;overflow-y:auto;padding:4px 0 6px}.picker-group{font-size:10.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);padding:8px 12px 3px;position:sticky;top:0;background:var(--card)}.picker-option{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:6px 12px;cursor:pointer;font-size:13.5px}.picker-option:hover,.picker-option.active{background:var(--accent-soft)}.picker-option.selected .picker-name{font-weight:650;color:var(--maroon)}.picker-name{min-width:0}.picker-meta{flex-shrink:0;font-size:11.5px;color:var(--muted);white-space:nowrap}.picker-empty{padding:12px;margin:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#14161a73;display:flex;align-items:center;justify-content:center;z-index:40;padding:16px}.modal{background:var(--card);border-radius:12px;padding:26px 28px 24px;width:100%;max-width:460px;box-shadow:0 12px 40px #0000002e}.modal h3{margin:0 0 8px;font-size:18px}.modal .hint{color:var(--muted);font-size:13px;line-height:1.5;margin:0 0 4px}.modal form.mt{margin-top:18px}.modal label.field{margin-bottom:16px}.modal label.field:last-of-type{margin-bottom:20px}.modal .row{gap:10px}.modal .row>.primary{flex:1}.modal p.small.muted.mt{margin-top:18px;padding-top:16px;border-top:1px solid var(--line);font-size:12.5px;line-height:1.6}@media (max-width: 820px){body{font-size:14px}.header{padding:12px 16px;align-items:flex-start;gap:2px 12px}.header h1{font-size:16px}.header .sub{flex:1 1 210px;font-size:12px;line-height:1.35}.layout{display:block}.sidebar{width:100%;border-right:0;border-bottom:1px solid var(--line);padding:14px}.project-list{max-height:220px;margin-right:0;flex:none}.project-create{border-top:0}.main{width:100%;padding:16px 14px 28px;overflow:visible}.card{padding:16px;margin-bottom:14px}.row,.run-settings,.construct-row,.results-toolbar,.result-actions{gap:10px}.row,.results-toolbar,.result-actions{flex-direction:column;align-items:stretch}.run-settings,.construct-row{grid-template-columns:1fr}.row>.grow,.construct-row>.grow,.language-control,.model-control{width:100%;min-width:0}.main .row>button,.main .row>a,.main .row>a>button,.results-toolbar>button{align-self:stretch;margin-bottom:0;width:100%}.run-button{width:100%;min-width:0}.stat-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.stat{min-width:0}.item-bar-row{display:grid;grid-template-columns:minmax(0,1fr) 56px;gap:6px 10px}.item-bar-track{grid-column:1 / -1;width:100%}.item-bar-val{width:auto}table.docs{min-width:520px}}@media (max-width: 460px){.header .sub{flex-basis:100%}.stat-grid{grid-template-columns:1fr}}
 
1
+ :root{--maroon: #7a1f3d;--maroon-dark: #5e1730;--ink: #1d2129;--muted: #667085;--line: #e5e7eb;--bg: #f7f7f8;--card: #ffffff;--ok: #157f3d;--err: #b42318;--accent-soft: #f6ebef;--control-height: 40px}*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;color:var(--ink);background:var(--bg);font-size:14.5px;line-height:1.5}.app{display:flex;flex-direction:column;min-height:100vh}.header{background:var(--maroon);color:#fff;padding:14px 28px;display:flex;align-items:baseline;flex-wrap:wrap;gap:14px}.header h1{flex:0 0 auto;font-size:17px;margin:0;font-weight:650;letter-spacing:.2px;white-space:nowrap}.header .sub{flex:1 1 280px;min-width:0;font-size:12.5px;opacity:.85}.layout{display:flex;flex:1;min-width:0;min-height:0}.sidebar{width:250px;background:var(--card);border-right:1px solid var(--line);padding:18px 14px;flex-shrink:0;display:flex;flex-direction:column;min-height:0}.sidebar h2{font-size:11.5px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);margin:0 0 10px 4px;display:flex;align-items:center;gap:8px}.sidebar h2 .count{background:var(--bg);border:1px solid var(--line);border-radius:999px;padding:0 8px;font-size:10.5px;letter-spacing:0;color:var(--muted)}.sidebar-filter{width:100%;padding:7px 10px;margin-bottom:10px;border:1px solid var(--line);border-radius:8px;font:inherit;font-size:13px;background:#fff;color:var(--ink)}.sidebar-filter:focus{outline:none;border-color:var(--maroon)}.project-list{flex:1;min-height:0;overflow-y:auto;margin:0 -4px;padding:0 4px 4px}.group-label{font-size:10.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);margin:10px 4px 5px}div:first-child>.group-label{margin-top:2px}.project-item{display:block;width:100%;text-align:left;padding:9px 12px;margin-bottom:5px;border:1px solid transparent;border-radius:8px;background:none;cursor:pointer;font:inherit;color:var(--ink)}.project-item:hover{background:var(--bg)}.project-item.active{background:var(--accent-soft);border-color:var(--maroon);font-weight:600}.project-item .project-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.project-item .date{display:block;font-size:11.5px;color:var(--muted);font-weight:400}.project-create{margin-top:12px;padding-top:12px;border-top:1px solid var(--line)}.project-create>button{width:100%}.main{flex:1;padding:22px 28px;overflow-y:auto;min-width:0}.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:18px 20px;margin-bottom:16px}.card h3{margin:0 0 4px;font-size:15px}.card .hint{color:var(--muted);font-size:12.5px;margin:0 0 12px}.step-badge{display:inline-flex;align-items:center;justify-content:center;width:21px;height:21px;border-radius:50%;background:var(--maroon);color:#fff;font-size:12px;font-weight:700;margin-right:8px;vertical-align:-3px}button{white-space:nowrap}button.primary{background:var(--maroon);color:#fff;border:none;min-height:var(--control-height);display:inline-flex;align-items:center;justify-content:center;padding:0 18px;border-radius:8px;font:inherit;font-weight:600;cursor:pointer;line-height:1.2}button.primary:hover{background:var(--maroon-dark)}button.primary:disabled{background:#c9ccd1;cursor:not-allowed}a.google-btn{background:var(--maroon);color:#fff;border:none;min-height:var(--control-height);display:flex;align-items:center;justify-content:center;padding:0 18px;border-radius:8px;font:inherit;font-weight:600;cursor:pointer;line-height:1.2;text-decoration:none;width:100%;box-sizing:border-box}a.google-btn:hover{background:var(--maroon-dark)}button.ghost{background:none;border:1px solid var(--line);color:var(--ink);min-height:var(--control-height);display:inline-flex;align-items:center;justify-content:center;padding:0 14px;border-radius:8px;font:inherit;cursor:pointer;line-height:1.2}button.ghost:hover{border-color:var(--maroon);color:var(--maroon)}button.linkish{background:none;border:none;color:var(--maroon);font:inherit;cursor:pointer;padding:0;text-decoration:underline}input[type=text],input[type=email],input[type=password],textarea,select{width:100%;padding:8px 10px;border:1px solid var(--line);border-radius:8px;font:inherit;background:#fff;color:var(--ink)}input[type=text],input[type=email],input[type=password],select{height:var(--control-height)}button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid rgba(122,31,61,.38);outline-offset:2px}input[type=file]{display:block;max-width:100%;margin-top:6px;font-size:13px;color:var(--muted)}input[type=file]::file-selector-button{background:#fff;border:1px solid var(--line);color:var(--ink);padding:7px 14px;border-radius:8px;font:inherit;font-size:13px;cursor:pointer;margin-right:10px}input[type=file]::file-selector-button:hover{border-color:var(--maroon);color:var(--maroon)}.row>button{align-self:flex-end;margin-bottom:1px}textarea{resize:vertical}label.field{display:block;margin-bottom:10px;font-size:13px;font-weight:600}label.field>*{margin-top:4px;font-weight:400}.field-hint{margin-top:0;font-weight:400;color:var(--muted);font-size:12px}.row{display:flex;gap:14px;flex-wrap:wrap}.row>*{min-width:0}.row>.grow{flex:1;min-width:min(220px,100%)}.language-control{min-width:170px}.model-control{min-width:260px}.run-settings{display:grid;grid-template-columns:minmax(160px,230px) minmax(320px,720px) max-content;justify-content:start;gap:14px;align-items:end}.run-settings .field{margin-bottom:0}.run-button{min-width:180px;height:var(--control-height)}.construct-row{display:grid;grid-template-columns:minmax(320px,1120px) max-content;justify-content:start;align-items:end;gap:14px}.results-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:14px;flex-wrap:wrap}.result-actions{justify-content:flex-end}.result-actions a{display:inline-flex;text-decoration:none}.pill{display:inline-block;padding:2px 10px;border-radius:999px;font-size:11.5px;font-weight:600}.pill.completed{background:#e6f4ea;color:var(--ok)}.pill.running{background:#fff3e0;color:#b45309}.pill.queued{background:#eef2f7;color:var(--muted)}.pill.failed{background:#fdecea;color:var(--err)}.progress-track{background:var(--line);border-radius:999px;height:7px;overflow:hidden}.progress-fill{background:var(--maroon);height:100%;transition:width .4s ease}.warnings{background:#fff8e6;border:1px solid #f2dfa8;color:#7a5b00;border-radius:8px;padding:10px 14px}.error-banner{background:#fdecea;color:var(--err);border:1px solid #f5c6c0;padding:10px 14px;border-radius:8px;margin-bottom:14px;font-size:13px}.table-wrap{width:100%;overflow-x:auto}table.docs{width:100%;border-collapse:collapse;font-size:13px}table.docs th{text-align:left;color:var(--muted);font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;padding:6px 8px;border-bottom:1px solid var(--line)}table.docs td{padding:7px 8px;border-bottom:1px solid var(--bg);vertical-align:top;overflow-wrap:anywhere}table.docs td.score{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap}.stat-grid{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:4px}.stat{flex:1;min-width:110px;background:var(--bg);border-radius:8px;padding:10px 14px}.stat .v{font-size:20px;font-weight:700;font-variant-numeric:tabular-nums}.stat .k{font-size:11.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}.item-bar-row{display:flex;align-items:center;gap:10px;margin-bottom:7px}.item-bar-label{flex:1;font-size:12.5px;min-width:0;overflow-wrap:anywhere}.item-bar-track{flex:1.2;background:var(--bg);border-radius:999px;height:10px}.item-bar-fill{background:var(--maroon);opacity:.85;height:100%;border-radius:999px}.item-bar-val{width:52px;text-align:right;font-variant-numeric:tabular-nums;font-size:12.5px;font-weight:600}.construct-items{margin:8px 0 0;padding-left:20px;color:var(--muted);font-size:12.5px}.construct-items li{margin-bottom:2px}.meta-footer{font-size:12px;color:var(--muted);background:var(--bg);border-radius:8px;padding:10px 14px;margin-top:14px;font-variant-numeric:tabular-nums}.meta-footer code{font-size:11.5px}.muted{color:var(--muted)}.small{font-size:12.5px}.mt{margin-top:12px}.header-auth{margin-left:auto;display:flex;align-items:center;gap:10px;color:#fff}.header-btn{background:#ffffff1f;color:#fff;border:1px solid rgba(255,255,255,.45);padding:5px 14px;border-radius:7px;font:inherit;font-size:13px;cursor:pointer}.header-btn:hover{background:#ffffff38}.project-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px;flex-wrap:wrap}.project-title{font-size:17px;font-weight:650;margin-right:10px}button.danger{color:var(--err);border-color:#f0c4be}button.danger:hover{color:var(--err);border-color:var(--err)}button.danger-solid{background:var(--err)}button.danger-solid:hover{background:#93261b}button.danger-solid:disabled{background:#c9ccd1}.picker{position:relative}.picker-display{width:100%;display:flex;align-items:center;gap:10px;padding:8px 12px;border:1px solid var(--line);border-radius:8px;background:#fff;font:inherit;color:var(--ink);cursor:pointer;text-align:left}.picker-display:hover{border-color:var(--maroon)}.picker-display .picker-caret{margin-left:auto;color:var(--muted);font-size:11px}.picker-search{width:100%;padding:8px 12px;border:1px solid var(--maroon);border-radius:8px;font:inherit;background:#fff}.picker-search:focus{outline:none;box-shadow:0 0 0 3px #7a1f3d1f}.picker-panel{position:absolute;top:calc(100% + 6px);left:0;z-index:50;width:100%;max-width:640px;background:var(--card);border:1px solid var(--line);border-radius:10px;box-shadow:0 14px 40px #0000002e;max-height:340px;overflow-y:auto;padding:4px 0 6px}.picker-group{font-size:10.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);padding:8px 12px 3px;position:sticky;top:0;background:var(--card)}.picker-option{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:6px 12px;cursor:pointer;font-size:13.5px}.picker-option:hover,.picker-option.active{background:var(--accent-soft)}.picker-option.selected .picker-name{font-weight:650;color:var(--maroon)}.picker-name{min-width:0}.picker-meta{flex-shrink:0;font-size:11.5px;color:var(--muted);white-space:nowrap}.picker-empty{padding:12px;margin:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#14161a73;display:flex;align-items:center;justify-content:center;z-index:40;padding:16px}.modal{background:var(--card);border-radius:12px;padding:26px 28px 24px;width:100%;max-width:460px;box-shadow:0 12px 40px #0000002e}.modal h3{margin:0 0 8px;font-size:18px}.modal .hint{color:var(--muted);font-size:13px;line-height:1.5;margin:0 0 4px}.modal form.mt{margin-top:18px}.modal label.field{margin-bottom:16px}.modal label.field:last-of-type{margin-bottom:20px}.modal .row{gap:10px}.modal .row>.primary{flex:1}.modal p.small.muted.mt{margin-top:18px;padding-top:16px;border-top:1px solid var(--line);font-size:12.5px;line-height:1.6}@media (max-width: 820px){body{font-size:14px}.header{padding:12px 16px;align-items:flex-start;gap:2px 12px}.header h1{font-size:16px}.header .sub{flex:1 1 210px;font-size:12px;line-height:1.35}.layout{display:block}.sidebar{width:100%;border-right:0;border-bottom:1px solid var(--line);padding:14px}.project-list{max-height:220px;margin-right:0;flex:none}.project-create{border-top:0}.main{width:100%;padding:16px 14px 28px;overflow:visible}.card{padding:16px;margin-bottom:14px}.row,.run-settings,.construct-row,.results-toolbar,.result-actions{gap:10px}.row,.results-toolbar,.result-actions{flex-direction:column;align-items:stretch}.run-settings,.construct-row{grid-template-columns:1fr}.row>.grow,.construct-row>.grow,.language-control,.model-control{width:100%;min-width:0}.main .row>button,.main .row>a,.main .row>a>button,.results-toolbar>button{align-self:stretch;margin-bottom:0;width:100%}.run-button{width:100%;min-width:0}.stat-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.stat{min-width:0}.item-bar-row{display:grid;grid-template-columns:minmax(0,1fr) 56px;gap:6px 10px}.item-bar-track{grid-column:1 / -1;width:100%}.item-bar-val{width:auto}table.docs{min-width:520px}}@media (max-width: 460px){.header .sub{flex-basis:100%}.stat-grid{grid-template-columns:1fr}}.welcome{padding-top:1.2rem}.welcome-hero h2{margin:0 0 .6rem;font-size:1.45rem}.welcome-hero p{max-width:62ch}.welcome-cta{display:flex;gap:.9rem;align-items:center;flex-wrap:wrap;margin:1.1rem 0 .6rem}.welcome-cta .ghost-link{font-size:.95rem}.welcome-steps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem}.welcome-step h3{margin:.2rem 0 .3rem}.step-n{width:1.9rem;height:1.9rem;border-radius:50%;background:var(--maroon);color:#fff;font-weight:700;display:flex;align-items:center;justify-content:center}@media (max-width: 700px){.welcome-steps{grid-template-columns:1fr}}
backend/static/assets/index-qbWtJ-39.js ADDED
The diff for this file is too large to render. See raw diff
 
backend/static/index.html CHANGED
@@ -4,8 +4,8 @@
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>CCR Platform - Contextualized Construct Representations</title>
7
- <script type="module" crossorigin src="/assets/index-BSotcTj4.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-CN_FzJfm.css">
9
  </head>
10
  <body>
11
  <div id="root"></div>
 
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>CCR Platform - Contextualized Construct Representations</title>
7
+ <script type="module" crossorigin src="/assets/index-qbWtJ-39.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-DAnNOmJI.css">
9
  </head>
10
  <body>
11
  <div id="root"></div>
backend/tests/test_api.py CHANGED
@@ -78,8 +78,8 @@ def test_health(client):
78
  assert client.get("/api/health").json()["status"] == "ok"
79
 
80
 
81
- def test_guide_and_product_pages_served(client):
82
- for path in ("/guide", "/product"):
83
  resp = client.get(path)
84
  assert resp.status_code == 200, path
85
  assert "text/html" in resp.headers["content-type"]
 
78
  assert client.get("/api/health").json()["status"] == "ok"
79
 
80
 
81
+ def test_guide_product_welcome_pages_served(client):
82
+ for path in ("/guide", "/product", "/welcome"):
83
  resp = client.get(path)
84
  assert resp.status_code == 200, path
85
  assert "text/html" in resp.headers["content-type"]
frontend/src/App.jsx CHANGED
@@ -1,9 +1,14 @@
1
  import { useEffect, useState } from "react";
2
  import { api } from "./api.js";
3
  import AdminPage from "./AdminPage.jsx";
 
4
  import Workspace from "./Workspace.jsx";
5
 
6
  const IS_ADMIN_PATH = window.location.pathname === "/admin";
 
 
 
 
7
 
8
  function relativeTime(iso) {
9
  if (!iso) return "";
@@ -56,6 +61,18 @@ export default function App() {
56
  const [authError, setAuthError] = useState("");
57
  const [authBusy, setAuthBusy] = useState(false);
58
  const [inviteToken, setInviteToken] = useState("");
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  // Invite links carry their role in the signed payload; decode it for the
61
  // banner only - the server re-verifies the signature on registration.
@@ -91,6 +108,7 @@ export default function App() {
91
  setInviteToken(invite);
92
  setAuthMode("register");
93
  setShowLogin(true);
 
94
  }
95
  }, []);
96
 
@@ -170,6 +188,11 @@ export default function App() {
170
  Contextualized Construct Representations · theory-driven psychological text analysis
171
  </span>
172
  <span className="header-auth">
 
 
 
 
 
173
  {auth?.signed_in ? (
174
  <>
175
  <span className="small">Hi, {auth.name}</span>
@@ -296,6 +319,10 @@ export default function App() {
296
  <main className="main" style={{ maxWidth: 1100, margin: "0 auto", padding: "0 1rem" }}>
297
  <AdminPage auth={auth} />
298
  </main>
 
 
 
 
299
  ) : (
300
  <div className="layout">
301
  <aside className="sidebar">
 
1
  import { useEffect, useState } from "react";
2
  import { api } from "./api.js";
3
  import AdminPage from "./AdminPage.jsx";
4
+ import WelcomePage from "./WelcomePage.jsx";
5
  import Workspace from "./Workspace.jsx";
6
 
7
  const IS_ADMIN_PATH = window.location.pathname === "/admin";
8
+ // First visit lands on the welcome page; after "Open the dashboard" (or any
9
+ // return visit) the root goes straight to work. /welcome always shows it.
10
+ const WELCOME_SEEN_KEY = "ccr_welcome_seen";
11
+ const IS_WELCOME_PATH = window.location.pathname === "/welcome";
12
 
13
  function relativeTime(iso) {
14
  if (!iso) return "";
 
61
  const [authError, setAuthError] = useState("");
62
  const [authBusy, setAuthBusy] = useState(false);
63
  const [inviteToken, setInviteToken] = useState("");
64
+ const [showWelcome, setShowWelcome] = useState(
65
+ IS_WELCOME_PATH ||
66
+ (!IS_ADMIN_PATH && !localStorage.getItem(WELCOME_SEEN_KEY))
67
+ );
68
+
69
+ function enterDashboard() {
70
+ localStorage.setItem(WELCOME_SEEN_KEY, "1");
71
+ if (window.location.pathname !== "/") {
72
+ window.history.replaceState({}, "", "/");
73
+ }
74
+ setShowWelcome(false);
75
+ }
76
 
77
  // Invite links carry their role in the signed payload; decode it for the
78
  // banner only - the server re-verifies the signature on registration.
 
108
  setInviteToken(invite);
109
  setAuthMode("register");
110
  setShowLogin(true);
111
+ setShowWelcome(false); // invited people go straight to the signup form
112
  }
113
  }, []);
114
 
 
188
  Contextualized Construct Representations · theory-driven psychological text analysis
189
  </span>
190
  <span className="header-auth">
191
+ {!showWelcome && (
192
+ <button className="header-btn" onClick={() => setShowWelcome(true)}>
193
+ About
194
+ </button>
195
+ )}
196
  {auth?.signed_in ? (
197
  <>
198
  <span className="small">Hi, {auth.name}</span>
 
319
  <main className="main" style={{ maxWidth: 1100, margin: "0 auto", padding: "0 1rem" }}>
320
  <AdminPage auth={auth} />
321
  </main>
322
+ ) : showWelcome ? (
323
+ <main className="main" style={{ maxWidth: 920, margin: "0 auto", padding: "0 1rem" }}>
324
+ <WelcomePage onEnter={enterDashboard} />
325
+ </main>
326
  ) : (
327
  <div className="layout">
328
  <aside className="sidebar">
frontend/src/WelcomePage.jsx ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Landing page: what CCR is, who runs the platform, and the doors in -
2
+ // dashboard, guide, product/architecture. First-time visitors land here
3
+ // (localStorage flag); the header "About" link brings anyone back.
4
+
5
+ const STEPS = [
6
+ {
7
+ n: "1",
8
+ title: "Upload your texts",
9
+ body: "A CSV or Excel file with one text per row - tweets, essays, open-ended survey answers, transcripts.",
10
+ },
11
+ {
12
+ n: "2",
13
+ title: "Pick a validated scale",
14
+ body: "Choose from the construct library (90+ published psychological scales) or paste your own questionnaire items.",
15
+ },
16
+ {
17
+ n: "3",
18
+ title: "Get scores you can defend",
19
+ body: "Every text is scored against every item, with distributions, per-item loadings, warnings, and a downloadable script that reproduces the numbers on any machine.",
20
+ },
21
+ ];
22
+
23
+ export default function WelcomePage({ onEnter }) {
24
+ return (
25
+ <div className="welcome">
26
+ <section className="welcome-hero card">
27
+ <h2>Psychological text analysis, grounded in theory</h2>
28
+ <p>
29
+ The CCR Platform measures psychological constructs in text using{" "}
30
+ <b>Contextualized Construct Representations</b> (CCR; Atari, Omrani,
31
+ et&nbsp;al.) - instead of counting words or prompting a chatbot, it
32
+ embeds <i>validated questionnaire items</i> and <i>your texts</i> with
33
+ the same language model and scores each text by its similarity to the
34
+ scale. Transparent, deterministic, and reproducible: the same input
35
+ gives the same numbers, every time, and you can take the script home
36
+ to prove it.
37
+ </p>
38
+ <div className="welcome-cta">
39
+ <button className="primary" onClick={onEnter}>
40
+ Open the dashboard →
41
+ </button>
42
+ <a className="ghost-link" href="/guide">How to use &amp; test it</a>
43
+ <a className="ghost-link" href="/product">How it works under the hood</a>
44
+ </div>
45
+ <p className="hint">
46
+ No account needed to try it - anonymous visitors get a few runs a day,
47
+ and uploads are deleted right after their analysis. Accounts are free
48
+ and keep your projects.
49
+ </p>
50
+ </section>
51
+
52
+ <section className="welcome-steps">
53
+ {STEPS.map((s) => (
54
+ <div className="card welcome-step" key={s.n}>
55
+ <div className="step-n">{s.n}</div>
56
+ <h3>{s.title}</h3>
57
+ <p className="hint">{s.body}</p>
58
+ </div>
59
+ ))}
60
+ </section>
61
+
62
+ <section className="card">
63
+ <h3>Your data stays here</h3>
64
+ <p className="hint">
65
+ The embedding models run on this server - no text is ever sent to
66
+ third-party AI APIs. Every run records the exact model version, scale
67
+ wording, and package versions, so results are auditable and
68
+ reproducible outside the platform.
69
+ </p>
70
+ </section>
71
+
72
+ <section className="card">
73
+ <h3>Who runs this</h3>
74
+ <p className="hint">
75
+ Built and maintained by the Culture &amp; Morality Lab (PI: Mohammad
76
+ Atari) at the University of Massachusetts Amherst, building on the
77
+ published CCR method and the lab's PsyEmbedding models. Questions,
78
+ bugs, ideas: <a href="mailto:devaanand@umass.edu">devaanand@umass.edu</a>.
79
+ </p>
80
+ </section>
81
+ </div>
82
+ );
83
+ }
frontend/src/styles.css CHANGED
@@ -469,3 +469,25 @@ button.danger-solid:disabled { background: #c9ccd1; }
469
  .header .sub { flex-basis: 100%; }
470
  .stat-grid { grid-template-columns: 1fr; }
471
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  .header .sub { flex-basis: 100%; }
470
  .stat-grid { grid-template-columns: 1fr; }
471
  }
472
+
473
+ /* ------------------------------------------------------------- welcome */
474
+ .welcome { padding-top: 1.2rem; }
475
+ .welcome-hero h2 { margin: 0 0 .6rem; font-size: 1.45rem; }
476
+ .welcome-hero p { max-width: 62ch; }
477
+ .welcome-cta {
478
+ display: flex; gap: .9rem; align-items: center; flex-wrap: wrap;
479
+ margin: 1.1rem 0 .6rem;
480
+ }
481
+ .welcome-cta .ghost-link { font-size: .95rem; }
482
+ .welcome-steps {
483
+ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem;
484
+ }
485
+ .welcome-step h3 { margin: .2rem 0 .3rem; }
486
+ .step-n {
487
+ width: 1.9rem; height: 1.9rem; border-radius: 50%;
488
+ background: var(--maroon); color: #fff; font-weight: 700;
489
+ display: flex; align-items: center; justify-content: center;
490
+ }
491
+ @media (max-width: 700px) {
492
+ .welcome-steps { grid-template-columns: 1fr; }
493
+ }