Alle UI-Strings der Feature-/App-Schicht ziehen aus mana/i18n (Listen/Detail/Marker/Feed/Rückblick/Zusammenfassen/Settings/Account + Archiv/Bulk/Note-Sheet/Palette); Interpolation als generierte Funktionen. Verwaisten-Check grün (0 hartkodierte UI-Strings). Onboarding bleibt seine eigene SOT (mana/content/onboarding). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
127 lines
3.4 KiB
Swift
127 lines
3.4 KiB
Swift
import ManaLLM
|
|
import SwiftUI
|
|
|
|
/// Cross-App-LLM-Pilot (π-AI-1): erzeugt eine kurze Artikel-Summary
|
|
/// via `ManaLLM.summarize(...)` — bevorzugt Foundation Models (iOS 26+
|
|
/// auf Apple-Intelligence-Geräten), fällt sonst auf Gemma E2B zurück
|
|
/// (falls bereits via Memoro im Shared-Container geladen) oder rendert
|
|
/// einen ehrlichen "nicht verfügbar"-State.
|
|
///
|
|
/// Eingehängt in `ArticleDetailView`. Eine eigene Sektion bewusst, weil:
|
|
/// - User soll kontrolliert anstoßen ("Zusammenfassen"), nicht
|
|
/// automatisch — Lokal-LLM-Calls kosten Akku/Compute.
|
|
/// - Result wird nicht in Article persistiert (Server-Wahrheit
|
|
/// bleibt). Re-Open = neuer Call. Caching kommt mit π-AI-2.
|
|
struct ArticleSummarySection: View {
|
|
let article: Article
|
|
|
|
@State private var summary: LLMSummary?
|
|
@State private var generating = false
|
|
@State private var error: String?
|
|
|
|
var body: some View {
|
|
// Empty-State ist kompakter Pill-Trigger — kein leerer Card-
|
|
// Block. Erst nach Generieren entsteht die volle Card.
|
|
if summary == nil, !generating, error == nil {
|
|
compactTrigger
|
|
} else {
|
|
fullCard
|
|
}
|
|
}
|
|
|
|
private var compactTrigger: some View {
|
|
Button {
|
|
Task { await generate() }
|
|
} label: {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: "sparkles")
|
|
Text(PagetaStrings.summarize)
|
|
.font(.subheadline.weight(.medium))
|
|
}
|
|
.padding(.horizontal, 14)
|
|
.padding(.vertical, 8)
|
|
.background(
|
|
Capsule().fill(PagetaTheme.primary.opacity(0.12))
|
|
)
|
|
.foregroundStyle(PagetaTheme.primary)
|
|
.overlay(
|
|
Capsule().stroke(PagetaTheme.primary.opacity(0.35), lineWidth: 1)
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
|
|
private var fullCard: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
header
|
|
content
|
|
}
|
|
.padding(16)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
.fill(PagetaTheme.surfaceHover)
|
|
)
|
|
}
|
|
|
|
private var header: some View {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "sparkles")
|
|
.foregroundStyle(PagetaTheme.primary)
|
|
Text("KI-Zusammenfassung")
|
|
.font(.headline)
|
|
Spacer()
|
|
if !generating, summary != nil {
|
|
Button("Neu") { Task { await generate() } }
|
|
.font(.caption)
|
|
.buttonStyle(.borderless)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var content: some View {
|
|
if generating {
|
|
HStack(spacing: 10) {
|
|
ProgressView()
|
|
Text("Lokale KI denkt nach …")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
} else if let summary {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text(summary.headline)
|
|
.font(.subheadline.weight(.semibold))
|
|
Text(summary.intro)
|
|
.font(.body)
|
|
.foregroundStyle(.primary)
|
|
}
|
|
} else if let error {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Label(error, systemImage: "exclamationmark.triangle")
|
|
.font(.caption)
|
|
.foregroundStyle(.orange)
|
|
Button("Erneut versuchen") { Task { await generate() } }
|
|
.font(.caption)
|
|
.buttonStyle(.borderless)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func generate() async {
|
|
generating = true
|
|
error = nil
|
|
defer { generating = false }
|
|
let text = article.content
|
|
guard !text.isEmpty else {
|
|
error = PagetaStrings.summarizeNoContent
|
|
return
|
|
}
|
|
// Pageta-Kontext liefert mehr Material als Memoro-Audio —
|
|
// `summarize` per ManaLLM funktioniert beidseitig.
|
|
if let result = await ManaLLM.summarize(text, level: .fast) {
|
|
summary = result
|
|
} else {
|
|
error = PagetaStrings.summarizeAiUnavailable
|
|
}
|
|
}
|
|
}
|