pageta-native/Sources/Features/ArticleSummarySection.swift
Till JS 8d6fc1a666 i18n: pageta-native voll über PagetaStrings (Phase 4)
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>
2026-06-09 16:06:55 +02:00

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
}
}
}