Skip to content

Commit b7ac04a

Browse files
committed
Add diversity-aware sampling feedback loop
1 parent 7f8d58e commit b7ac04a

15 files changed

Lines changed: 1533 additions & 144 deletions

File tree

.github/workflows/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ on:
44
push:
55
branches:
66
- main
7+
paths-ignore:
8+
- "telemetry/**"
9+
- "sampler-config/**"
710
workflow_dispatch:
811

912
permissions:

.github/workflows/telemetry.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Aggregate Telemetry
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- "telemetry/events/**"
9+
- "scripts/aggregate_telemetry.py"
10+
- ".github/workflows/telemetry.yml"
11+
workflow_dispatch:
12+
13+
permissions:
14+
contents: write
15+
16+
concurrency:
17+
group: streetview-wander-telemetry
18+
cancel-in-progress: true
19+
20+
jobs:
21+
aggregate:
22+
runs-on: ubuntu-latest
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@v4
26+
27+
- name: Aggregate telemetry
28+
run: python3 scripts/aggregate_telemetry.py
29+
30+
- name: Commit generated telemetry outputs
31+
run: |
32+
if git diff --quiet -- telemetry/summary/latest.json sampler-config/latest.json; then
33+
echo "No telemetry output changes."
34+
exit 0
35+
fi
36+
git config user.name "github-actions[bot]"
37+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
38+
git add telemetry/summary/latest.json sampler-config/latest.json
39+
git commit -m "Update telemetry sampler config"
40+
git push

Sources/StreetViewWander/AppMain.swift

Lines changed: 122 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ struct StreetViewWanderApp: App {
1515
.frame(minWidth: 980, minHeight: 680)
1616
.task {
1717
updates.startAutoChecks()
18+
await model.refreshSamplerConfig()
1819
}
1920
}
2021
.commands {
@@ -168,26 +169,58 @@ struct ContentView: View {
168169
}
169170

170171
private var bottomStatus: some View {
171-
HStack(spacing: 12) {
172-
if !model.hasBrowserAPIKey || !model.hasMetadataAPIKey {
173-
Button("Add API Keys") {
174-
model.isSettingsPresented = true
172+
VStack(alignment: .leading, spacing: 8) {
173+
HStack(spacing: 12) {
174+
if !model.hasBrowserAPIKey || !model.hasMetadataAPIKey {
175+
Button("Add API Keys") {
176+
model.isSettingsPresented = true
177+
}
178+
.buttonStyle(PrimaryOverlayButtonStyle())
175179
}
176-
.buttonStyle(PrimaryOverlayButtonStyle())
180+
181+
Text(model.errorText ?? model.statusText)
182+
.lineLimit(2)
183+
.font(.system(size: 13, weight: .semibold))
184+
.foregroundStyle(model.errorText == nil ? Color.white.opacity(0.86) : Color(red: 1, green: 0.82, blue: 0.76))
185+
.layoutPriority(1)
177186
}
178187

179-
Text(model.errorText ?? model.statusText)
180-
.lineLimit(2)
181-
.font(.system(size: 13, weight: .semibold))
182-
.foregroundStyle(model.errorText == nil ? Color.white.opacity(0.86) : Color(red: 1, green: 0.82, blue: 0.76))
183-
.layoutPriority(1)
188+
if let reason = model.selectionReasonSummary, model.errorText == nil {
189+
Text(reason)
190+
.lineLimit(2)
191+
.font(.system(size: 12, weight: .medium))
192+
.foregroundStyle(.white.opacity(0.70))
193+
}
194+
195+
if model.panorama != nil, !model.isLoading {
196+
feedbackBar
197+
}
184198
}
185199
.padding(.horizontal, 14)
186200
.padding(.vertical, 10)
187201
.background(.black.opacity(0.58), in: RoundedRectangle(cornerRadius: 8))
188202
.frame(maxWidth: 620, alignment: .leading)
189203
}
190204

205+
private var feedbackBar: some View {
206+
HStack(spacing: 6) {
207+
feedbackButton(.liked, icon: "hand.thumbsup")
208+
feedbackButton(.tooSimilar, icon: "square.on.square")
209+
feedbackButton(.tooManyRoads, icon: "road.lanes")
210+
feedbackButton(.moreCity, icon: "building.2")
211+
}
212+
}
213+
214+
private func feedbackButton(_ kind: PlaceFeedbackKind, icon: String) -> some View {
215+
Button {
216+
model.recordFeedback(kind)
217+
} label: {
218+
Label(kind.label, systemImage: icon)
219+
}
220+
.buttonStyle(OverlayButtonStyle(isActive: model.hasFeedback(kind)))
221+
.help(kind.label)
222+
}
223+
191224
private var metadataUsageBadge: some View {
192225
HStack(spacing: 6) {
193226
Image(systemName: "gauge.with.dots.needle.33percent")
@@ -286,36 +319,66 @@ struct DetailsPanel: View {
286319
@EnvironmentObject private var model: WanderModel
287320

288321
var body: some View {
289-
VStack(alignment: .leading, spacing: 14) {
290-
panelHeader("Current Start Point")
322+
ScrollView {
323+
VStack(alignment: .leading, spacing: 14) {
324+
panelHeader("Current Start Point")
325+
326+
if let panorama = model.panorama {
327+
detail("Scope", panorama.scopeLabel)
328+
detail("Area", panorama.areaLabel)
329+
if let country = panorama.countryLabel {
330+
detail("Country", country)
331+
}
332+
if let continent = panorama.continentLabel {
333+
detail("Continent", continent)
334+
}
335+
if let sceneKind = panorama.sceneKind {
336+
detail("Search mix", sceneKind.label)
337+
}
338+
detail("Latitude", formatCoord(panorama.location.lat))
339+
detail("Longitude", formatCoord(panorama.location.lng))
340+
detail("Requested", "\(formatCoord(panorama.requestedLocation.lat)), \(formatCoord(panorama.requestedLocation.lng))")
341+
detail(
342+
"Request distance",
343+
formatDistance(
344+
SearchDensityTier.distanceMeters(
345+
from: panorama.requestedLocation,
346+
to: panorama.location
347+
)
348+
)
349+
)
350+
detail("Metadata checks", "\(panorama.attempts)")
351+
detail("Image date", panorama.date ?? "Unknown")
352+
detail("Sampler config", model.samplerConfigSourceLabel)
353+
354+
if let reason = panorama.selectionReasonSummary {
355+
detail("Selection reason", reason)
356+
}
357+
if !model.selectionReasonDetails.isEmpty {
358+
VStack(alignment: .leading, spacing: 6) {
359+
Text("Reason details")
360+
.font(.system(size: 11, weight: .medium))
361+
.foregroundStyle(.secondary)
362+
ForEach(Array(model.selectionReasonDetails.enumerated()), id: \.offset) { _, item in
363+
Text(item)
364+
.font(.system(size: 12, weight: .medium))
365+
.foregroundStyle(.white.opacity(0.78))
366+
.fixedSize(horizontal: false, vertical: true)
367+
}
368+
}
369+
}
291370

292-
if let panorama = model.panorama {
293-
detail("Scope", panorama.scopeLabel)
294-
detail("Area", panorama.areaLabel)
295-
if let country = panorama.countryLabel {
296-
detail("Country", country)
297-
}
298-
if let continent = panorama.continentLabel {
299-
detail("Continent", continent)
300-
}
301-
if let sceneKind = panorama.sceneKind {
302-
detail("Search mix", sceneKind.label)
371+
Button("Open in Google Maps") {
372+
NSWorkspace.shared.open(mapsURL(for: panorama))
373+
}
374+
.padding(.top, 4)
375+
} else {
376+
Text("Pick a random place to begin.")
377+
.foregroundStyle(.secondary)
303378
}
304-
detail("Latitude", formatCoord(panorama.location.lat))
305-
detail("Longitude", formatCoord(panorama.location.lng))
306-
detail("Metadata checks", "\(panorama.attempts)")
307-
detail("Image date", panorama.date ?? "Unknown")
308379

309-
Button("Open in Google Maps") {
310-
NSWorkspace.shared.open(mapsURL(for: panorama))
311-
}
312-
.padding(.top, 4)
313-
} else {
314-
Text("Pick a random place to begin.")
315-
.foregroundStyle(.secondary)
380+
Spacer()
316381
}
317-
318-
Spacer()
319382
}
320383
.panelText()
321384
}
@@ -448,6 +511,22 @@ struct SettingsView: View {
448511
}
449512
}
450513

514+
Divider()
515+
516+
VStack(alignment: .leading, spacing: 10) {
517+
Text("Feedback")
518+
.font(.system(size: 13, weight: .semibold))
519+
520+
HStack(spacing: 10) {
521+
requestMetric("Signals", formatCount(model.feedbackCount))
522+
requestMetric("Config", model.samplerConfigSourceLabel)
523+
Spacer()
524+
Button("Reset Feedback") {
525+
model.resetFeedback()
526+
}
527+
}
528+
}
529+
451530
HStack {
452531
Button("Import .env") {
453532
if model.importEnvFile() {
@@ -512,6 +591,13 @@ private func formatCount(_ value: Int) -> String {
512591
NumberFormatter.localizedString(from: NSNumber(value: value), number: .decimal)
513592
}
514593

594+
private func formatDistance(_ meters: Double) -> String {
595+
if meters >= 1_000 {
596+
return String(format: "%.1f km", meters / 1_000)
597+
}
598+
return String(format: "%.0f m", meters)
599+
}
600+
515601
private func mapsURL(for panorama: Panorama) -> URL {
516602
var components = URLComponents(string: "https://www.google.com/maps/search/")!
517603
components.queryItems = [

0 commit comments

Comments
 (0)