Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion JSONViewer/UI/AppShellView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct AppShellView: View {
#endif
@State private var isInspectorVisible: Bool = false
@State private var isAISidebarVisible: Bool = false
@State private var isPreviewPickerPresented: Bool = false
@Environment(\.openWindow) private var openWindow

private var displayText: String {
Expand Down Expand Up @@ -79,7 +80,10 @@ struct AppShellView: View {
.navigationSplitViewColumnWidth(min: 420, ideal: 680, max: .infinity)
.navigationTitle(viewModel.fileURL?.lastPathComponent ?? "Prism")
}

.sheet(isPresented: $isPreviewPickerPresented) {
PreviewFieldsPickerView(viewModel: viewModel, isPresented: $isPreviewPickerPresented)
.frame(minWidth: 520, minHeight: 520)
}
.toolbar {
ToolbarItemGroup {
Button {
Expand All @@ -104,6 +108,15 @@ struct AppShellView: View {
.frame(width: 100)
.help("Toggle between raw text and tree")

// New: Row preview fields picker
Button {
isPreviewPickerPresented = true
} label: {
Label("Row Preview", systemImage: "slider.horizontal.3")
}
.help("Choose which fields are shown in the JSONL sidebar preview")
.disabled(viewModel.mode == .none)

Button {
withAnimation {
isInspectorVisible.toggle()
Expand Down
129 changes: 129 additions & 0 deletions JSONViewer/UI/PreviewFieldsPickerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import SwiftUI

struct PreviewFieldsPickerView: View {
@ObservedObject var viewModel: AppViewModel
@Binding var isPresented: Bool

@State private var candidates: [String] = []
@State private var selected: Set<String> = []
@State private var search: String = ""
@State private var isLoading: Bool = true

var body: some View {
VStack(alignment: .leading, spacing: 12) {
header
Divider()
content
Divider()
footer
}
.padding(16)
.onAppear {
selected = Set(viewModel.currentSidebarPreviewFields())
Task {
isLoading = true
let paths = await viewModel.collectCandidatePreviewPaths()
await MainActor.run {
self.candidates = paths
self.isLoading = false
}
}
}
}

private var header: some View {
HStack {
Label("Row Preview Fields", systemImage: "slider.horizontal.3")
.font(.system(size: 16, weight: .semibold))
Spacer()
Button {
isPresented = false
} label: {
Image(systemName: "xmark.circle.fill")
}
.buttonStyle(.plain)
}
}

@ViewBuilder
private var content: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Choose which JSON fields to show for each JSONL row in the sidebar.")
.foregroundStyle(.secondary)
.font(.callout)

HStack(spacing: 8) {
Image(systemName: "magnifyingglass")
.foregroundStyle(.secondary)
TextField("Filter fields…", text: $search)
.textFieldStyle(.plain)
}
.padding(.vertical, 8)
.padding(.horizontal, 10)
.background(RoundedRectangle(cornerRadius: 8).fill(Color(nsColor: .controlBackgroundColor)))
.overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(Color.secondary.opacity(0.25), lineWidth: 1))

if isLoading {
VStack(alignment: .center) {
ProgressView().padding(.top, 24)
Text("Scanning document for fields…")
.foregroundStyle(.secondary)
.font(.footnote)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if candidates.isEmpty {
Text("No fields found.")
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
} else {
let filtered = filteredCandidates
List {
ForEach(filtered, id: \.self) { path in
Toggle(isOn: Binding(
get: { selected.contains(path) },
set: { on in
if on { selected.insert(path) } else { selected.remove(path) }
})
) {
Text(path.isEmpty ? "(root)" : path)
.font(.system(.body, design: .monospaced))
}
}
}
.frame(minHeight: 320)
}
}
}

private var footer: some View {
HStack {
Button {
selected.removeAll()
} label: {
Label("Select None", systemImage: "minus.circle")
}
Button {
selected.formUnion(filteredCandidates)
} label: {
Label("Select All Shown", systemImage: "checkmark.circle")
}
Spacer()
Button {
// Persist and refresh previews
let list = candidates.filter { selected.contains($0) }
viewModel.setSidebarPreviewFields(list)
isPresented = false
} label: {
Text("Apply")
}
.keyboardShortcut(.defaultAction)
.disabled(isLoading)
}
}

private var filteredCandidates: [String] {
let q = search.trimmingCharacters(in: .whitespacesAndNewlines)
guard !q.isEmpty else { return candidates }
return candidates.filter { $0.localizedCaseInsensitiveContains(q) }
}
}
18 changes: 16 additions & 2 deletions JSONViewer/UI/SidebarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ struct SidebarView: View {
.lineLimit(2)
}
.id(i)
.task(id: i) {
.task(id: "\(i)-\(viewModel.previewFieldsChangeToken)") {
if previews[i] == nil {
viewModel.preview(for: i) { text in
// Ensure update happens next runloop to avoid update-during-view warnings
Expand Down Expand Up @@ -138,10 +138,20 @@ struct SidebarView: View {
Text("Row \(row.id)")
.font(.callout)
.foregroundStyle(.secondary)
Text(row.preview)
Text(previews[row.id] ?? "Loading…")
.font(.system(.caption, design: .monospaced))
.lineLimit(2)
}
.id(row.id)
.task(id: "\(row.id)-\(viewModel.previewFieldsChangeToken)") {
if previews[row.id] == nil {
viewModel.preview(for: row.id) { text in
DispatchQueue.main.async {
previews[row.id] = text
}
}
}
}
.padding(.vertical, 4)
.contentShape(Rectangle())
.onAppear {
Expand Down Expand Up @@ -197,5 +207,9 @@ struct SidebarView: View {
.onChange(of: viewModel.selectedRowID) { _ in
Task { _ = await viewModel.updateTreeForSelectedRow() }
}
.onChange(of: viewModel.previewFieldsChangeToken) { _ in
// Clear local previews so visible rows re-fetch with new field selection
previews = [:]
}
}
}
Loading