Spaces:
Running
Running
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 +11 -0
- backend/static/assets/index-BSotcTj4.js +0 -0
- backend/static/assets/{index-CN_FzJfm.css → index-DAnNOmJI.css} +1 -1
- backend/static/assets/index-qbWtJ-39.js +0 -0
- backend/static/index.html +2 -2
- backend/tests/test_api.py +2 -2
- frontend/src/App.jsx +27 -0
- frontend/src/WelcomePage.jsx +83 -0
- frontend/src/styles.css +22 -0
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-
|
| 8 |
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
| 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
|
| 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 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 & 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 & 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 |
+
}
|