@@ -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+
515601private func mapsURL( for panorama: Panorama ) -> URL {
516602 var components = URLComponents ( string: " https://www.google.com/maps/search/ " ) !
517603 components. queryItems = [
0 commit comments