Files
hyungi_document_server/clients/ds-app/Sources/AppFeature/AI/AICompletionView.swift
T

88 lines
3.4 KiB
Swift

import SwiftUI
import AIFabric
public extension AIProviderID {
var displayName: String {
switch self {
case .onDevice: return "온디바이스"
case .localMLX: return "맥미니"
case .remoteDS: return "원격 DS"
case .specialized: return "GPU 특화"
}
}
}
/// Reusable renderer for any AICompletionResponse: answer text, numbered citations (deep-link to doc),
/// an ALWAYS-visible provider/routing badge (the no-silent-fallback visibility rule), and confidence.
public struct AICompletionView: View {
public let response: AICompletionResponse
public var onOpenDoc: (Int) -> Void
public init(response: AICompletionResponse, onOpenDoc: @escaping (Int) -> Void) {
self.response = response
self.onOpenDoc = onOpenDoc
}
public var body: some View {
VStack(alignment: .leading, spacing: 12) {
// Routing badges (mandatory, always shown)
HStack(spacing: 8) {
Chip(response.providerUsed.displayName, Sage.brand)
if let note = response.routingNote {
Chip(note, Sage.amber)
}
if response.finishReason != .completed {
Chip(finishLabel(response.finishReason), Sage.danger)
}
if let c = response.confidence {
Chip("신뢰도 \(confidenceLabel(c))", Sage.muted)
}
if let ms = response.latencyMs {
Text("\(Int(ms)) ms").font(.caption2).foregroundStyle(Sage.muted)
}
}
// Answer
Text(response.text)
.font(.body)
.foregroundStyle(Sage.ink)
.textSelection(.enabled)
// Citations
if !response.citations.isEmpty {
VStack(alignment: .leading, spacing: 6) {
Text("출처").font(.caption.weight(.bold)).foregroundStyle(Sage.muted)
ForEach(response.citations) { c in
Button { onOpenDoc(c.docId) } label: {
HStack(alignment: .top, spacing: 8) {
Text("[\(c.n)]").font(.caption.weight(.bold)).foregroundStyle(Sage.brand)
VStack(alignment: .leading, spacing: 2) {
Text(c.title ?? c.sectionTitle ?? "문서 \(c.docId)")
.font(.callout.weight(.medium)).foregroundStyle(Sage.ink)
Text(c.spanText).font(.caption).foregroundStyle(Sage.muted).lineLimit(2)
}
}
}
.buttonStyle(.plain)
}
}
.padding(10)
.background(Sage.surface, in: RoundedRectangle(cornerRadius: 10))
}
}
}
private func finishLabel(_ r: AIFinishReason) -> String {
switch r {
case .completed: return "완료"
case .refused: return "거부"
case .timeout: return "시간 초과"
case .unavailable: return "사용 불가"
case .noEvidence: return "근거 없음"
}
}
private func confidenceLabel(_ c: AIConfidence) -> String {
switch c { case .high: return "높음"; case .medium: return "중간"; case .low: return "낮음" }
}
}