PromtGarageMac / Sources /PromptGarageApp /Views /ImportCenterView.swift
smlflg's picture
Initial public upload from Projekte/PromtGarageMac
4977db2 verified
Raw History Blame Contribute Delete
10.9 kB
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")
}
}