import PromptGarageCore import SwiftUI struct ImportCenterView: View { @Bindable var model: AppModel var body: some View { ScrollView { VStack(alignment: .leading, spacing: 22) { VStack(alignment: .leading, spacing: 7) { Text("Import Center") .font(.largeTitle.weight(.semibold)) Text("Quelle lokal analysieren, Ergebnis prüfen und erst danach atomar importieren.") .foregroundStyle(.secondary) } providerPicker formatAndSourceCard if let preview = model.importPreview { previewCard(preview) } else if model.importSourceURL == nil { ContentUnavailableView { Label("Noch keine Quelle", systemImage: "square.and.arrow.down") } description: { Text("Wähle Provider, Format und anschließend eine Datei oder einen Ordner.") } .frame(maxWidth: .infinity, minHeight: 210) } if let report = model.lastImportReport { ImportReportCard(report: report) } } .padding(24) .frame(maxWidth: 760) .frame(maxWidth: .infinity) } .navigationTitle("Import") .overlay { if model.isAnalyzingImport { LoadingOverlay(label: "Quelle wird lokal analysiert …") } else if model.isCommittingImport { LoadingOverlay(label: "Import wird atomar geschrieben …") } } } private var providerPicker: some View { Picker( "Provider", selection: Binding( get: { model.selectedImportProvider }, set: { model.selectImportProvider($0) } ) ) { ForEach(ImportProvider.allCases) { provider in Text(provider.displayName).tag(provider) } } .pickerStyle(.segmented) .disabled(model.isAnalyzingImport || model.isCommittingImport) } private var formatAndSourceCard: some View { VStack(alignment: .leading, spacing: 16) { HStack(alignment: .firstTextBaseline) { Text("Format") .font(.headline) Spacer() StatusChip( title: model.selectedImportProvider.displayName, systemImage: "shippingbox", tint: AppUI.accent ) } Picker( "Importformat", selection: Binding( get: { model.selectedImportFormat }, set: { model.selectImportFormat($0) } ) ) { ForEach(model.selectedImportProvider.formats) { format in Text(format.displayName).tag(format) } } .labelsHidden() .disabled(model.isAnalyzingImport || model.isCommittingImport) HStack(spacing: 12) { Image(systemName: model.importSourceURL == nil ? "doc.badge.plus" : "doc.fill") .font(.title2) .foregroundStyle(model.importSourceURL == nil ? .secondary : AppUI.accent) VStack(alignment: .leading, spacing: 2) { Text(model.importSourceURL?.lastPathComponent ?? "Datei oder Ordner auswählen") .font(.callout.weight(.medium)) .lineLimit(1) Text(model.importSourceURL?.path(percentEncoded: false) ?? "Es werden nur bewusst ausgewählte lokale Daten gelesen.") .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) .truncationMode(.middle) } Spacer() Button("Auswählen & analysieren …") { Task { await model.chooseAndPreviewImportSource() } } .buttonStyle(.borderedProminent) .disabled(model.isAnalyzingImport || model.isCommittingImport) } .padding(14) .background(Color(nsColor: .controlBackgroundColor), in: RoundedRectangle(cornerRadius: 10)) } .padding(18) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: AppUI.cardCornerRadius)) .overlay { RoundedRectangle(cornerRadius: AppUI.cardCornerRadius) .stroke(Color(nsColor: .separatorColor).opacity(0.6)) } } private func previewCard(_ preview: ImportPreview) -> some View { VStack(alignment: .leading, spacing: 16) { HStack { Label("Vorschau", systemImage: "doc.text.magnifyingglass") .font(.headline) Spacer() StatusChip( title: "Keine Änderung vorgenommen", systemImage: "eye", tint: .secondary ) } LazyVGrid(columns: [GridItem(.adaptive(minimum: 140), spacing: 10)], spacing: 10) { ImportMetric(title: "Gescannt", value: preview.statistics.scanned) ImportMetric(title: "Neu", value: preview.candidates.count, tint: .green) ImportMetric(title: "Duplikate", value: preview.statistics.skippedDuplicate) ImportMetric(title: "Agent", value: preview.statistics.skippedAgent) ImportMetric(title: "System", value: preview.statistics.skippedSystem) ImportMetric(title: "Zu kurz", value: preview.statistics.skippedShort) ImportMetric(title: "Zu lang", value: preview.statistics.skippedLong) ImportMetric(title: "Fehlerhaft", value: preview.statistics.skippedMalformed) } HStack { Button("Vorschau verwerfen") { model.resetImportPreview() } .disabled(model.isCommittingImport) Spacer() Button { Task { await model.commitImport() } } label: { Label( "\(preview.candidates.count.formatted()) Prompts importieren", systemImage: "square.and.arrow.down.fill" ) } .buttonStyle(.borderedProminent) .disabled(preview.candidates.isEmpty || !model.canMutatePrompts) } Label( "Ein Schreibfehler rollt den gesamten Import zurück. Es gibt keinen Teilimport.", systemImage: "arrow.counterclockwise.circle" ) .font(.caption) .foregroundStyle(.secondary) } .padding(18) .background(AppUI.accent.opacity(0.055), in: RoundedRectangle(cornerRadius: AppUI.cardCornerRadius)) .overlay { RoundedRectangle(cornerRadius: AppUI.cardCornerRadius) .stroke(AppUI.accent.opacity(0.22)) } } } private struct ImportMetric: View { let title: String let value: Int var tint: Color = .secondary var body: some View { VStack(alignment: .leading, spacing: 3) { Text(value.formatted()) .font(.title3.weight(.semibold).monospacedDigit()) .foregroundStyle(tint) Text(title) .font(.caption) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) .padding(11) .background(Color(nsColor: .controlBackgroundColor), in: RoundedRectangle(cornerRadius: 9)) } } private struct ImportReportCard: View { let report: ImportReport var body: some View { HStack(spacing: 12) { Image(systemName: "checkmark.circle.fill") .font(.title2) .foregroundStyle(.green) VStack(alignment: .leading, spacing: 3) { Text("Import abgeschlossen") .font(.headline) Text("\(report.statistics.imported.formatted()) Prompts aus \(report.sourceName)") .font(.callout) .foregroundStyle(.secondary) } Spacer() Text(report.completedAt, style: .time) .font(.caption.monospacedDigit()) .foregroundStyle(.tertiary) } .padding(16) .background(.green.opacity(0.07), in: RoundedRectangle(cornerRadius: AppUI.cardCornerRadius)) } } struct ImportPreviewDetailView: View { @Bindable var model: AppModel var body: some View { Group { if let preview = model.importPreview, !preview.candidates.isEmpty { VStack(spacing: 0) { HStack { VStack(alignment: .leading, spacing: 2) { Text("Neue Prompts") .font(.title2.weight(.semibold)) Text("Nur Vorschau · noch nicht gespeichert") .font(.caption) .foregroundStyle(.secondary) } Spacer() Text(preview.candidates.count.formatted()) .font(.title2.weight(.semibold).monospacedDigit()) } .padding(18) Divider() List(preview.candidates.prefix(200), id: \.self) { candidate in VStack(alignment: .leading, spacing: 5) { Text(candidate.title) .font(.body.weight(.medium)) .lineLimit(1) Text(candidate.content.replacingOccurrences(of: "\n", with: " ")) .font(.caption) .foregroundStyle(.secondary) .lineLimit(2) StatusChip(title: candidate.source, systemImage: "shippingbox") } .padding(.vertical, 4) } } } else { ContentUnavailableView { Label("Importvorschau", systemImage: "doc.text.magnifyingglass") } description: { Text("Nach der lokalen Analyse erscheinen hier die erkannten neuen Prompts.") } } } .navigationTitle("Vorschau") } }