|
|
@@ -16,24 +16,32 @@ struct TitleOptimizerView: View {
|
|
|
header
|
|
|
messageBanners
|
|
|
|
|
|
- ScrollView {
|
|
|
- VStack(alignment: .leading, spacing: 12) {
|
|
|
- tabBar
|
|
|
-
|
|
|
- PostPageBoundary {
|
|
|
- switch viewModel.selectedTab {
|
|
|
- case .optimize:
|
|
|
- optimizeContent
|
|
|
- case .compare:
|
|
|
- compareContent
|
|
|
- case .preview:
|
|
|
- previewContent
|
|
|
+ ScrollViewReader { scrollProxy in
|
|
|
+ ScrollView {
|
|
|
+ VStack(alignment: .leading, spacing: 12) {
|
|
|
+ tabBar
|
|
|
+
|
|
|
+ PostPageBoundary {
|
|
|
+ switch viewModel.selectedTab {
|
|
|
+ case .optimize:
|
|
|
+ optimizeContent
|
|
|
+ case .compare:
|
|
|
+ compareContent
|
|
|
+ case .preview:
|
|
|
+ previewContent
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+ .padding(.horizontal, Layout.horizontalPadding)
|
|
|
+ .padding(.top, 12)
|
|
|
+ .padding(.bottom, 24)
|
|
|
+ }
|
|
|
+ .onChange(of: viewModel.variants.count) { _, count in
|
|
|
+ guard count > 0, viewModel.selectedTab == .optimize else { return }
|
|
|
+ withAnimation(.easeInOut(duration: 0.25)) {
|
|
|
+ scrollProxy.scrollTo("title-suggestions", anchor: .top)
|
|
|
+ }
|
|
|
}
|
|
|
- .padding(.horizontal, Layout.horizontalPadding)
|
|
|
- .padding(.top, 12)
|
|
|
- .padding(.bottom, 24)
|
|
|
}
|
|
|
}
|
|
|
.background(AppTheme.background)
|
|
|
@@ -100,7 +108,10 @@ struct TitleOptimizerView: View {
|
|
|
.disabled(viewModel.activeTitle.trimmingCharacters(in: .whitespaces).isEmpty)
|
|
|
|
|
|
Button {
|
|
|
- guard requirePremiumAccess() else { return }
|
|
|
+ guard requirePremiumAccess() else {
|
|
|
+ viewModel.errorMessage = "Premium is required to generate AI title suggestions."
|
|
|
+ return
|
|
|
+ }
|
|
|
Task { await viewModel.optimizeTitles() }
|
|
|
} label: {
|
|
|
HStack(spacing: 6) {
|
|
|
@@ -122,7 +133,7 @@ struct TitleOptimizerView: View {
|
|
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
|
|
}
|
|
|
.buttonStyle(AppPrimaryButtonStyle())
|
|
|
- .disabled(!viewModel.canOptimize)
|
|
|
+ .disabled(viewModel.isOptimizing)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -165,18 +176,7 @@ struct TitleOptimizerView: View {
|
|
|
// MARK: - Optimize
|
|
|
|
|
|
private var optimizeContent: some View {
|
|
|
- ViewThatFits(in: .horizontal) {
|
|
|
- optimizeColumns
|
|
|
-
|
|
|
- VStack(alignment: .leading, spacing: Layout.columnSpacing) {
|
|
|
- optimizeColumn(title: "Configuration", icon: "slider.horizontal.3") {
|
|
|
- configurationPanel
|
|
|
- }
|
|
|
- optimizeColumn(title: "Title & Suggestions", icon: "textformat") {
|
|
|
- editorPanel
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ optimizeColumns
|
|
|
}
|
|
|
|
|
|
private var optimizeColumns: some View {
|
|
|
@@ -185,6 +185,7 @@ struct TitleOptimizerView: View {
|
|
|
configurationPanel
|
|
|
}
|
|
|
.frame(width: Layout.columnWidth)
|
|
|
+ .layoutPriority(1)
|
|
|
|
|
|
Rectangle()
|
|
|
.fill(AppTheme.border)
|
|
|
@@ -194,7 +195,8 @@ struct TitleOptimizerView: View {
|
|
|
optimizeColumn(title: "Title & Suggestions", icon: "textformat") {
|
|
|
editorPanel
|
|
|
}
|
|
|
- .frame(maxWidth: .infinity)
|
|
|
+ .frame(minWidth: 0, maxWidth: .infinity)
|
|
|
+ .layoutPriority(0)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -318,6 +320,7 @@ struct TitleOptimizerView: View {
|
|
|
}
|
|
|
|
|
|
suggestionsSection
|
|
|
+ .id("title-suggestions")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -467,6 +470,8 @@ struct TitleOptimizerView: View {
|
|
|
metadataDivider
|
|
|
metadataRow(label: "Tone", value: viewModel.draft.tone.title)
|
|
|
metadataDivider
|
|
|
+ metadataRow(label: "Engine", value: viewModel.usesLiveAI ? "OpenAI" : "Local templates")
|
|
|
+ metadataDivider
|
|
|
metadataRow(label: "Active", value: viewModel.activeTitle)
|
|
|
}
|
|
|
.padding(14)
|
|
|
@@ -538,6 +543,8 @@ struct TitleOptimizerView: View {
|
|
|
metadataRow(label: "Tone", value: viewModel.draft.tone.title)
|
|
|
metadataDivider
|
|
|
metadataRow(label: "Type", value: viewModel.draft.postType.title)
|
|
|
+ metadataDivider
|
|
|
+ metadataRow(label: "Engine", value: viewModel.usesLiveAI ? "OpenAI" : "Local templates")
|
|
|
|
|
|
if let score = viewModel.analysis?.overallScore {
|
|
|
metadataDivider
|