Spaces:
Runtime error
Runtime error
Commit Β·
5c44de2
1
Parent(s): 23dabb1
UX: add guided tour, pipeline timing note
Browse files- tour.js: 6-step spotlight tour (upload β model β run β reimport β roulette β pipeline diagram); simulates tab navigation and temporarily reveals sub-tab-bar in demo mode; triggered by βΆ Take the tour button in FAQ
- Progress card: static timing note ("usually 5β10 min β may appear stuck") so users don't abandon a running job
- app.css: tour overlay/spotlight/tooltip styles + .progress-timing-note + .tour-cta-card/.btn-tour
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CLAUDE.md +6 -3
- README.md +2 -1
- web/static/app.css +140 -0
- web/static/tour.js +213 -0
- web/templates/index.html +11 -1
CLAUDE.md
CHANGED
|
@@ -32,9 +32,11 @@ pipeline/ shared core (used by CLI, Docker, web)
|
|
| 32 |
web/app.py FastAPI: pipeline, SSE progress, results browser, resolve_provider()
|
| 33 |
POST /import β KML or CSV re-import (no extraction, skip to tabs)
|
| 34 |
web/static/ served at /static/ via FastAPI StaticFiles mount
|
| 35 |
-
app.css all styles (~
|
| 36 |
app.js all client JS (~1280 lines, no Jinja β reads config from body data-* attrs)
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
<body data-ollama-enabled data-client-geocode data-default-model
|
| 39 |
data-default-ollama-model>
|
| 40 |
tests/ runnable scripts (NOT pytest) β see Conventions
|
|
@@ -58,7 +60,7 @@ The app has a **permanent two-level nav** visible from first page load:
|
|
| 58 |
βββ [π² Roulette] [π Browse] β #sub-tab-bar, after data loads
|
| 59 |
```
|
| 60 |
|
| 61 |
-
- **Extract tab** (`#top-panel-extract`): upload card, model picker, run button, progress card.
|
| 62 |
- **Explore tab** (`#top-panel-explore`): import card at top; once data loads the import card hides
|
| 63 |
and `#sub-tab-bar` appears with two sub-panels.
|
| 64 |
- After extraction completes β `showTopTab('explore')` β `showSubTabBar(jobId)` β `showSubTab('roulette')` (auto-spins) + nudge toast.
|
|
@@ -83,6 +85,7 @@ Key JS functions:
|
|
| 83 |
- `showSubTabBar(jobId)` β hides import card, reveals `#sub-tab-bar` + `#download-bar` inside Explore
|
| 84 |
- `showSubTab(name)` β switches sub-panels (roulette / browse); auto-spins roulette on first visit
|
| 85 |
- `updateTabBadges()` β sets badge counts and stat strip
|
|
|
|
| 86 |
|
| 87 |
KML parser convention: folder nesting = country (outer) β city (inner); `<coordinates>` order is
|
| 88 |
**lng,lat,alt**; description CDATA uses `" Β· "` separator for category Β· cuisine Β· price.
|
|
|
|
| 32 |
web/app.py FastAPI: pipeline, SSE progress, results browser, resolve_provider()
|
| 33 |
POST /import β KML or CSV re-import (no extraction, skip to tabs)
|
| 34 |
web/static/ served at /static/ via FastAPI StaticFiles mount
|
| 35 |
+
app.css all styles (~1150 lines, no Jinja)
|
| 36 |
app.js all client JS (~1280 lines, no Jinja β reads config from body data-* attrs)
|
| 37 |
+
tour.js guided tour (IIFE, ~210 lines); exposes window.startTour / window.endTour;
|
| 38 |
+
requires app.js globals showTopTab, showSubTab, currentJobId
|
| 39 |
+
web/templates/index.html HTML skeleton only (~500 lines); Jinja vars passed to JS via
|
| 40 |
<body data-ollama-enabled data-client-geocode data-default-model
|
| 41 |
data-default-ollama-model>
|
| 42 |
tests/ runnable scripts (NOT pytest) β see Conventions
|
|
|
|
| 60 |
βββ [π² Roulette] [π Browse] β #sub-tab-bar, after data loads
|
| 61 |
```
|
| 62 |
|
| 63 |
+
- **Extract tab** (`#top-panel-extract`): upload card, model picker, run button, progress card. The progress card includes a static `.progress-timing-note` ("usually 5β10 min β may appear stuck") so users don't abandon a running job.
|
| 64 |
- **Explore tab** (`#top-panel-explore`): import card at top; once data loads the import card hides
|
| 65 |
and `#sub-tab-bar` appears with two sub-panels.
|
| 66 |
- After extraction completes β `showTopTab('explore')` β `showSubTabBar(jobId)` β `showSubTab('roulette')` (auto-spins) + nudge toast.
|
|
|
|
| 85 |
- `showSubTabBar(jobId)` β hides import card, reveals `#sub-tab-bar` + `#download-bar` inside Explore
|
| 86 |
- `showSubTab(name)` β switches sub-panels (roulette / browse); auto-spins roulette on first visit
|
| 87 |
- `updateTabBadges()` β sets badge counts and stat strip
|
| 88 |
+
- `startTour()` / `endTour()` β guided tour (defined in `tour.js`); works without data loaded by temporarily revealing the sub-tab-bar in demo mode and restoring state on exit. The FAQ tab has a `.tour-cta-card` with a `βΆ Take the tour` button (`onclick="startTour()"`).
|
| 89 |
|
| 90 |
KML parser convention: folder nesting = country (outer) β city (inner); `<coordinates>` order is
|
| 91 |
**lng,lat,alt**; description CDATA uses `" Β· "` separator for category Β· cuisine Β· price.
|
README.md
CHANGED
|
@@ -307,8 +307,9 @@ The UI lets you:
|
|
| 307 |
- **πΊ Explore** β for returning users; drop your `places_map.kml` or `places_full.csv` here to skip re-extraction entirely. Once data loads, two sub-tabs appear:
|
| 308 |
- **π² Roulette** β auto-spins the moment you land on it; filter by category or location (accepts city name or country name); badge shows how many unvisited spots remain. Default after extraction
|
| 309 |
- **π Browse** β stats bar, interactive Leaflet map, full-text search, filters, card grid, and KML / GeoJSON / CSV download buttons. Default after import
|
| 310 |
-
- **β FAQ** β system pipeline diagram, About The Belagurian, and answers to common questions
|
| 311 |
- After extraction completes, the app automatically jumps to Explore β Roulette and spins immediately
|
|
|
|
| 312 |
- **Preview pins on a map** β an in-app Leaflet/OpenStreetMap map renders every geocoded place so you can sanity-check for misplaced pins before exporting. Follows your active filters; pins link back to the original reel
|
| 313 |
- **Filter and search** β text search by name/city, country, category, and cuisine dropdowns, multi-select price range chips, "missing pins" toggle, and a **π΄ Food only** toggle (on by default) that limits Browse and Roulette to food/drink places (Restaurant, Bar, Cafe, Bakery, Market) β all client-side with instant results
|
| 314 |
- **Fix extractions inline** β click any field (name, city, state, category, cuisine, occasion) directly on the card to edit it; changes are saved to the CSV immediately
|
|
|
|
| 307 |
- **πΊ Explore** β for returning users; drop your `places_map.kml` or `places_full.csv` here to skip re-extraction entirely. Once data loads, two sub-tabs appear:
|
| 308 |
- **π² Roulette** β auto-spins the moment you land on it; filter by category or location (accepts city name or country name); badge shows how many unvisited spots remain. Default after extraction
|
| 309 |
- **π Browse** β stats bar, interactive Leaflet map, full-text search, filters, card grid, and KML / GeoJSON / CSV download buttons. Default after import
|
| 310 |
+
- **β FAQ** β system pipeline diagram, About The Belagurian, and answers to common questions. Includes a **βΆ Take the tour** button that runs a 6-step guided walkthrough of the full workflow (upload β model β run β re-import β roulette β pipeline diagram), with spotlight highlighting and simulated tab navigation
|
| 311 |
- After extraction completes, the app automatically jumps to Explore β Roulette and spins immediately
|
| 312 |
+
- **Pipeline timing note** β the progress card shows a persistent note ("usually 5β10 min β the bar may appear stuck during AI extraction") so users don't think the app is broken
|
| 313 |
- **Preview pins on a map** β an in-app Leaflet/OpenStreetMap map renders every geocoded place so you can sanity-check for misplaced pins before exporting. Follows your active filters; pins link back to the original reel
|
| 314 |
- **Filter and search** β text search by name/city, country, category, and cuisine dropdowns, multi-select price range chips, "missing pins" toggle, and a **π΄ Food only** toggle (on by default) that limits Browse and Roulette to food/drink places (Restaurant, Bar, Cafe, Bakery, Market) β all client-side with instant results
|
| 315 |
- **Fix extractions inline** β click any field (name, city, state, category, cuisine, occasion) directly on the card to edit it; changes are saved to the CSV immediately
|
web/static/app.css
CHANGED
|
@@ -1010,3 +1010,143 @@
|
|
| 1010 |
border-radius: 4px;
|
| 1011 |
padding: 1px 5px;
|
| 1012 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1010 |
border-radius: 4px;
|
| 1011 |
padding: 1px 5px;
|
| 1012 |
}
|
| 1013 |
+
|
| 1014 |
+
/* ββ Pipeline timing note βββββββββββββββββββββββββββββββββββ */
|
| 1015 |
+
.progress-timing-note {
|
| 1016 |
+
margin-top: 14px;
|
| 1017 |
+
padding: 10px 13px;
|
| 1018 |
+
background: var(--surface2);
|
| 1019 |
+
border: 1px solid var(--border);
|
| 1020 |
+
border-radius: 8px;
|
| 1021 |
+
font-size: 0.78rem;
|
| 1022 |
+
color: var(--muted);
|
| 1023 |
+
line-height: 1.55;
|
| 1024 |
+
}
|
| 1025 |
+
.progress-timing-note strong { color: var(--text); }
|
| 1026 |
+
|
| 1027 |
+
/* ββ Tour overlay & spotlight βββββββββββββββββββββββββββββββ */
|
| 1028 |
+
#tour-overlay {
|
| 1029 |
+
display: none;
|
| 1030 |
+
position: fixed;
|
| 1031 |
+
inset: 0;
|
| 1032 |
+
z-index: 10000;
|
| 1033 |
+
background: transparent;
|
| 1034 |
+
pointer-events: all;
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
.tour-spotlight {
|
| 1038 |
+
position: relative !important;
|
| 1039 |
+
z-index: 10001 !important;
|
| 1040 |
+
box-shadow: 0 0 0 4px var(--ig1), 0 0 0 9999px rgba(0,0,0,0.62) !important;
|
| 1041 |
+
border-radius: 10px;
|
| 1042 |
+
}
|
| 1043 |
+
|
| 1044 |
+
/* ββ Tour tooltip βββββββββββββββββββββββββββββββββββββββββββ */
|
| 1045 |
+
#tour-tooltip {
|
| 1046 |
+
display: none;
|
| 1047 |
+
position: fixed;
|
| 1048 |
+
z-index: 10002;
|
| 1049 |
+
flex-direction: column;
|
| 1050 |
+
gap: 10px;
|
| 1051 |
+
background: var(--surface);
|
| 1052 |
+
border: 1px solid var(--border);
|
| 1053 |
+
border-radius: 14px;
|
| 1054 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.45);
|
| 1055 |
+
padding: 18px 20px 14px;
|
| 1056 |
+
max-width: 320px;
|
| 1057 |
+
min-width: 260px;
|
| 1058 |
+
font-size: 0.88rem;
|
| 1059 |
+
line-height: 1.5;
|
| 1060 |
+
}
|
| 1061 |
+
|
| 1062 |
+
#tour-close {
|
| 1063 |
+
position: absolute;
|
| 1064 |
+
top: 10px; right: 12px;
|
| 1065 |
+
background: none;
|
| 1066 |
+
border: none;
|
| 1067 |
+
cursor: pointer;
|
| 1068 |
+
font-size: 1rem;
|
| 1069 |
+
color: var(--muted);
|
| 1070 |
+
padding: 2px 6px;
|
| 1071 |
+
border-radius: 4px;
|
| 1072 |
+
line-height: 1;
|
| 1073 |
+
}
|
| 1074 |
+
#tour-close:hover { background: var(--surface2); }
|
| 1075 |
+
|
| 1076 |
+
#tour-title {
|
| 1077 |
+
font-weight: 700;
|
| 1078 |
+
font-size: 0.95rem;
|
| 1079 |
+
padding-right: 24px;
|
| 1080 |
+
color: var(--text);
|
| 1081 |
+
}
|
| 1082 |
+
|
| 1083 |
+
#tour-body { color: var(--muted); }
|
| 1084 |
+
#tour-body code {
|
| 1085 |
+
font-family: monospace;
|
| 1086 |
+
font-size: 0.8rem;
|
| 1087 |
+
color: var(--text);
|
| 1088 |
+
background: var(--surface2);
|
| 1089 |
+
padding: 1px 5px;
|
| 1090 |
+
border-radius: 3px;
|
| 1091 |
+
}
|
| 1092 |
+
#tour-body strong, #tour-body em { color: var(--text); font-style: normal; }
|
| 1093 |
+
|
| 1094 |
+
#tour-footer {
|
| 1095 |
+
display: flex;
|
| 1096 |
+
align-items: center;
|
| 1097 |
+
justify-content: space-between;
|
| 1098 |
+
margin-top: 4px;
|
| 1099 |
+
}
|
| 1100 |
+
#tour-counter { font-size: 0.75rem; color: var(--muted); }
|
| 1101 |
+
#tour-btns { display: flex; gap: 8px; }
|
| 1102 |
+
|
| 1103 |
+
#tour-back, #tour-next, #tour-finish {
|
| 1104 |
+
font-size: 0.82rem;
|
| 1105 |
+
padding: 5px 13px;
|
| 1106 |
+
border-radius: 6px;
|
| 1107 |
+
border: 1px solid var(--border);
|
| 1108 |
+
cursor: pointer;
|
| 1109 |
+
background: var(--surface2);
|
| 1110 |
+
color: var(--text);
|
| 1111 |
+
transition: background 0.15s;
|
| 1112 |
+
}
|
| 1113 |
+
#tour-back:hover { background: var(--border); }
|
| 1114 |
+
#tour-next, #tour-finish {
|
| 1115 |
+
background: var(--ig1);
|
| 1116 |
+
color: #fff;
|
| 1117 |
+
border-color: var(--ig1);
|
| 1118 |
+
}
|
| 1119 |
+
#tour-next:hover, #tour-finish:hover { filter: brightness(1.12); }
|
| 1120 |
+
|
| 1121 |
+
/* ββ Take the tour button in FAQ ββββββββββββββββββββββββββββ */
|
| 1122 |
+
.tour-cta-card {
|
| 1123 |
+
max-width: 640px;
|
| 1124 |
+
display: flex;
|
| 1125 |
+
align-items: center;
|
| 1126 |
+
justify-content: space-between;
|
| 1127 |
+
gap: 16px;
|
| 1128 |
+
flex-wrap: wrap;
|
| 1129 |
+
}
|
| 1130 |
+
.tour-cta-card p {
|
| 1131 |
+
font-size: 0.88rem;
|
| 1132 |
+
color: var(--muted);
|
| 1133 |
+
flex: 1;
|
| 1134 |
+
min-width: 0;
|
| 1135 |
+
}
|
| 1136 |
+
.btn-tour {
|
| 1137 |
+
flex: 0 0 auto;
|
| 1138 |
+
display: inline-flex;
|
| 1139 |
+
align-items: center;
|
| 1140 |
+
gap: 7px;
|
| 1141 |
+
padding: 9px 18px;
|
| 1142 |
+
background: var(--ig1);
|
| 1143 |
+
color: #fff;
|
| 1144 |
+
border: none;
|
| 1145 |
+
border-radius: 8px;
|
| 1146 |
+
font-size: 0.88rem;
|
| 1147 |
+
font-weight: 600;
|
| 1148 |
+
cursor: pointer;
|
| 1149 |
+
white-space: nowrap;
|
| 1150 |
+
transition: filter 0.15s;
|
| 1151 |
+
}
|
| 1152 |
+
.btn-tour:hover { filter: brightness(1.12); }
|
web/static/tour.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Guided tour β triggered by "Take the tour" button in FAQ tab.
|
| 2 |
+
// Requires app.js globals: showTopTab, showSubTab, currentJobId.
|
| 3 |
+
|
| 4 |
+
(function () {
|
| 5 |
+
let _step = -1;
|
| 6 |
+
let _overlay = null;
|
| 7 |
+
let _tooltip = null;
|
| 8 |
+
let _spotlit = null;
|
| 9 |
+
|
| 10 |
+
// Explore state before demo mode (we temporarily show sub-tab-bar)
|
| 11 |
+
let _savedState = null;
|
| 12 |
+
|
| 13 |
+
function _saveExploreState() {
|
| 14 |
+
return {
|
| 15 |
+
subTabBar: document.getElementById('sub-tab-bar').style.display,
|
| 16 |
+
importCard: document.getElementById('import-card').style.display,
|
| 17 |
+
};
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
function _restoreExploreState() {
|
| 21 |
+
if (!_savedState) return;
|
| 22 |
+
document.getElementById('sub-tab-bar').style.display = _savedState.subTabBar;
|
| 23 |
+
document.getElementById('import-card').style.display = _savedState.importCard;
|
| 24 |
+
_savedState = null;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// ββ Step definitions βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 28 |
+
const STEPS = [
|
| 29 |
+
{
|
| 30 |
+
id: 'upload-card',
|
| 31 |
+
pre() { showTopTab('extract'); },
|
| 32 |
+
title: 'π Upload your export',
|
| 33 |
+
body: 'Drop your Instagram ZIP or <code>saved_posts.json</code> here. No unzipping needed β the ZIP works as-is.',
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
id: 'model-card',
|
| 37 |
+
title: 'π€ Choose your AI',
|
| 38 |
+
body: '<strong>Claude API</strong> is fast (bring your Anthropic key). <strong>Ollama</strong> is free and fully local. <strong>Free chatbot</strong> needs no API key β paste posts into ChatGPT or Claude.ai.',
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
id: 'run-btn-wrap',
|
| 42 |
+
title: 'π Extract & geocode',
|
| 43 |
+
body: 'Hit <em>Extract places</em>. AI reads captions, OpenStreetMap geocodes each place, and a KML file is built β ready to import into Google My Maps.',
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
id: 'import-card',
|
| 47 |
+
pre() {
|
| 48 |
+
if (_savedState) {
|
| 49 |
+
// Restore before we navigate back to import card
|
| 50 |
+
const st = _savedState;
|
| 51 |
+
_savedState = null;
|
| 52 |
+
document.getElementById('sub-tab-bar').style.display = st.subTabBar;
|
| 53 |
+
document.getElementById('import-card').style.display = st.importCard;
|
| 54 |
+
}
|
| 55 |
+
showTopTab('explore');
|
| 56 |
+
},
|
| 57 |
+
title: 'π Re-import a previous run',
|
| 58 |
+
body: 'Already have a <code>places_map.kml</code> or <code>places_full.csv</code>? Drop it here to skip extraction and jump straight to browsing β no re-running needed.',
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
id: 'roulette-section',
|
| 62 |
+
pre() {
|
| 63 |
+
if (!_savedState) _savedState = _saveExploreState();
|
| 64 |
+
document.getElementById('import-card').style.display = 'none';
|
| 65 |
+
document.getElementById('sub-tab-bar').style.display = 'block';
|
| 66 |
+
showSubTab('roulette');
|
| 67 |
+
},
|
| 68 |
+
title: 'π² Place Roulette',
|
| 69 |
+
body: "Can't decide where to go? Spin the wheel for a random pick from your saved places. Filter by city, distance, or category.",
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
id: 'faq-pipeline-card',
|
| 73 |
+
pre() {
|
| 74 |
+
if (_savedState && !currentJobId) _restoreExploreState();
|
| 75 |
+
showTopTab('faq');
|
| 76 |
+
},
|
| 77 |
+
title: 'π Everything runs local-first',
|
| 78 |
+
body: 'Pre-filter β AI extraction β Nominatim geocoding β KML / GeoJSON / CSV export. Your Instagram data never leaves your device (except the AI call you pick).',
|
| 79 |
+
last: true,
|
| 80 |
+
},
|
| 81 |
+
];
|
| 82 |
+
|
| 83 |
+
// ββ DOM setup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 84 |
+
function _ensureDOM() {
|
| 85 |
+
if (_overlay) return;
|
| 86 |
+
|
| 87 |
+
_overlay = document.createElement('div');
|
| 88 |
+
_overlay.id = 'tour-overlay';
|
| 89 |
+
document.body.appendChild(_overlay);
|
| 90 |
+
_overlay.addEventListener('click', endTour);
|
| 91 |
+
|
| 92 |
+
_tooltip = document.createElement('div');
|
| 93 |
+
_tooltip.id = 'tour-tooltip';
|
| 94 |
+
_tooltip.innerHTML =
|
| 95 |
+
'<button id="tour-close" title="Exit tour">β</button>' +
|
| 96 |
+
'<div id="tour-title"></div>' +
|
| 97 |
+
'<div id="tour-body"></div>' +
|
| 98 |
+
'<div id="tour-footer">' +
|
| 99 |
+
'<span id="tour-counter"></span>' +
|
| 100 |
+
'<div id="tour-btns">' +
|
| 101 |
+
'<button id="tour-back">β Back</button>' +
|
| 102 |
+
'<button id="tour-next">Next β</button>' +
|
| 103 |
+
'<button id="tour-finish">Finish β</button>' +
|
| 104 |
+
'</div>' +
|
| 105 |
+
'</div>';
|
| 106 |
+
document.body.appendChild(_tooltip);
|
| 107 |
+
|
| 108 |
+
document.getElementById('tour-close').addEventListener('click', endTour);
|
| 109 |
+
document.getElementById('tour-back').addEventListener('click', () => goToStep(_step - 1));
|
| 110 |
+
document.getElementById('tour-next').addEventListener('click', () => goToStep(_step + 1));
|
| 111 |
+
document.getElementById('tour-finish').addEventListener('click', endTour);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// ββ Spotlight ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 115 |
+
function _applySpotlight(el) {
|
| 116 |
+
_clearSpotlight();
|
| 117 |
+
_spotlit = el;
|
| 118 |
+
el.classList.add('tour-spotlight');
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
function _clearSpotlight() {
|
| 122 |
+
if (_spotlit) { _spotlit.classList.remove('tour-spotlight'); _spotlit = null; }
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// ββ Tooltip positioning ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 126 |
+
function _positionTooltip(el) {
|
| 127 |
+
const tt = _tooltip;
|
| 128 |
+
tt.style.visibility = 'hidden';
|
| 129 |
+
tt.style.display = 'flex';
|
| 130 |
+
|
| 131 |
+
requestAnimationFrame(() => {
|
| 132 |
+
requestAnimationFrame(() => {
|
| 133 |
+
const rect = el.getBoundingClientRect();
|
| 134 |
+
const ttH = tt.offsetHeight;
|
| 135 |
+
const ttW = tt.offsetWidth;
|
| 136 |
+
const vW = window.innerWidth;
|
| 137 |
+
const vH = window.innerHeight;
|
| 138 |
+
const GAP = 14;
|
| 139 |
+
const EDGE = 12;
|
| 140 |
+
|
| 141 |
+
let top, left;
|
| 142 |
+
|
| 143 |
+
if (rect.bottom + GAP + ttH <= vH - EDGE) {
|
| 144 |
+
// Below target
|
| 145 |
+
top = rect.bottom + GAP;
|
| 146 |
+
left = rect.left + rect.width / 2 - ttW / 2;
|
| 147 |
+
} else if (rect.top - GAP - ttH >= EDGE) {
|
| 148 |
+
// Above target
|
| 149 |
+
top = rect.top - ttH - GAP;
|
| 150 |
+
left = rect.left + rect.width / 2 - ttW / 2;
|
| 151 |
+
} else {
|
| 152 |
+
// Centre of viewport
|
| 153 |
+
top = (vH - ttH) / 2;
|
| 154 |
+
left = (vW - ttW) / 2;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
left = Math.max(EDGE, Math.min(left, vW - ttW - EDGE));
|
| 158 |
+
top = Math.max(EDGE, Math.min(top, vH - ttH - EDGE));
|
| 159 |
+
|
| 160 |
+
tt.style.top = top + 'px';
|
| 161 |
+
tt.style.left = left + 'px';
|
| 162 |
+
tt.style.visibility = 'visible';
|
| 163 |
+
});
|
| 164 |
+
});
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
// ββ Navigation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 168 |
+
function goToStep(n) {
|
| 169 |
+
if (n < 0 || n >= STEPS.length) return;
|
| 170 |
+
_step = n;
|
| 171 |
+
const s = STEPS[n];
|
| 172 |
+
|
| 173 |
+
if (s.pre) s.pre();
|
| 174 |
+
|
| 175 |
+
const el = document.getElementById(s.id);
|
| 176 |
+
if (!el) return;
|
| 177 |
+
|
| 178 |
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 179 |
+
|
| 180 |
+
setTimeout(() => {
|
| 181 |
+
_applySpotlight(el);
|
| 182 |
+
_overlay.style.display = 'block';
|
| 183 |
+
|
| 184 |
+
document.getElementById('tour-title').textContent = s.title;
|
| 185 |
+
document.getElementById('tour-body').innerHTML = s.body;
|
| 186 |
+
document.getElementById('tour-counter').textContent = (n + 1) + ' / ' + STEPS.length;
|
| 187 |
+
|
| 188 |
+
document.getElementById('tour-back').style.display = n === 0 ? 'none' : '';
|
| 189 |
+
document.getElementById('tour-next').style.display = s.last ? 'none' : '';
|
| 190 |
+
document.getElementById('tour-finish').style.display = s.last ? '' : 'none';
|
| 191 |
+
|
| 192 |
+
_positionTooltip(el);
|
| 193 |
+
}, 280);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
// ββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 197 |
+
function startTour() {
|
| 198 |
+
_ensureDOM();
|
| 199 |
+
_savedState = null;
|
| 200 |
+
goToStep(0);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
function endTour() {
|
| 204 |
+
_clearSpotlight();
|
| 205 |
+
if (_overlay) _overlay.style.display = 'none';
|
| 206 |
+
if (_tooltip) _tooltip.style.display = 'none';
|
| 207 |
+
if (_savedState && !currentJobId) _restoreExploreState();
|
| 208 |
+
_step = -1;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
window.startTour = startTour;
|
| 212 |
+
window.endTour = endTour;
|
| 213 |
+
})();
|
web/templates/index.html
CHANGED
|
@@ -205,6 +205,9 @@
|
|
| 205 |
</div>
|
| 206 |
</div>
|
| 207 |
<div class="status-msg" id="status-msg"></div>
|
|
|
|
|
|
|
|
|
|
| 208 |
</div>
|
| 209 |
|
| 210 |
</div><!-- /top-panel-extract -->
|
|
@@ -378,8 +381,14 @@
|
|
| 378 |
<!-- ββ FAQ / About panel βββββββββββββββββββββββββββββββββββββββ -->
|
| 379 |
<div class="top-panel" id="top-panel-faq">
|
| 380 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
<!-- System diagram -->
|
| 382 |
-
<div class="card" style="max-width:640px;">
|
| 383 |
<p class="card-title">How it works</p>
|
| 384 |
<div class="pipeline-diagram">
|
| 385 |
<div class="pipe-node">
|
|
@@ -488,5 +497,6 @@
|
|
| 488 |
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
| 489 |
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
| 490 |
<script src="/static/app.js"></script>
|
|
|
|
| 491 |
</body>
|
| 492 |
</html>
|
|
|
|
| 205 |
</div>
|
| 206 |
</div>
|
| 207 |
<div class="status-msg" id="status-msg"></div>
|
| 208 |
+
<p class="progress-timing-note">
|
| 209 |
+
<strong>β± This usually takes 5β10 min.</strong> The progress bar may appear stuck while the AI reads your captions in bulk β it's still running. Keep this tab open.
|
| 210 |
+
</p>
|
| 211 |
</div>
|
| 212 |
|
| 213 |
</div><!-- /top-panel-extract -->
|
|
|
|
| 381 |
<!-- ββ FAQ / About panel βββββββββββββββββββββββββββββββββββββββ -->
|
| 382 |
<div class="top-panel" id="top-panel-faq">
|
| 383 |
|
| 384 |
+
<!-- Tour CTA -->
|
| 385 |
+
<div class="card tour-cta-card">
|
| 386 |
+
<p>New here? Take a 60-second walkthrough of the full workflow β upload, extract, roulette, and browse.</p>
|
| 387 |
+
<button class="btn-tour" onclick="startTour()">βΆ Take the tour</button>
|
| 388 |
+
</div>
|
| 389 |
+
|
| 390 |
<!-- System diagram -->
|
| 391 |
+
<div class="card" id="faq-pipeline-card" style="max-width:640px;">
|
| 392 |
<p class="card-title">How it works</p>
|
| 393 |
<div class="pipeline-diagram">
|
| 394 |
<div class="pipe-node">
|
|
|
|
| 497 |
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
| 498 |
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
| 499 |
<script src="/static/app.js"></script>
|
| 500 |
+
<script src="/static/tour.js"></script>
|
| 501 |
</body>
|
| 502 |
</html>
|