560efb9554
- AppFeature: SageTheme tokens, AppModel (@MainActor @Observable store), RootView (DEVONthink NavigationSplitView), Dashboard/Documents(MD-first+pending fallback+?token= download)/Search/Ask/Memos/Digest pages
- AI seam: AIService actor + AIResult, AppAIComposition (MockAIProvider x4 tiers), AICompletionView (numbered citations + always-visible routing badge), backend picker with visible explicit-unavailable error
- MarkdownView: block-aware renderer (GFM table separator-row skip, AttributedString inline-only)
- DSApp: thin @main, injects FixtureDSClient + mock AIRouter (zero backend / zero LLM)
swift build (full app) + swift test (19) green under Swift 6 strict concurrency. Sources/AI untouched (isolation vs freeze 17f8830 = clean).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
51 lines
2.1 KiB
Swift
51 lines
2.1 KiB
Swift
import SwiftUI
|
|
import DSKit
|
|
|
|
/// Distinct from the Documents table: relevance-forward result cards (score bar + match_reason).
|
|
struct SearchView: View {
|
|
@Environment(AppModel.self) private var model
|
|
|
|
var body: some View {
|
|
@Bindable var model = model
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
HStack(spacing: 8) {
|
|
TextField("검색어를 입력하세요", text: $model.searchQuery)
|
|
.textFieldStyle(.roundedBorder)
|
|
.onSubmit { Task { await model.runSearch() } }
|
|
Button("검색") { Task { await model.runSearch() } }
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
.padding(12)
|
|
|
|
if let response = model.searchResponse {
|
|
List(response.results) { result in
|
|
VStack(alignment: .leading, spacing: 5) {
|
|
HStack(spacing: 6) {
|
|
if let d = result.aiDomain { Chip(d, Sage.domainColor(d)) }
|
|
Text(result.title ?? "문서 \(result.id)")
|
|
.font(.callout.weight(.medium)).foregroundStyle(Sage.ink).lineLimit(1)
|
|
Spacer()
|
|
if let m = result.matchReason {
|
|
Text(m).font(.caption2).foregroundStyle(Sage.muted)
|
|
}
|
|
}
|
|
Text(result.snippet ?? result.aiSummary ?? "")
|
|
.font(.caption).foregroundStyle(Sage.muted).lineLimit(2)
|
|
if let score = result.score { ScoreBar(score: score) }
|
|
}
|
|
.padding(.vertical, 4)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture {
|
|
model.section = .documents
|
|
Task { await model.openDocument(result.id) }
|
|
}
|
|
}
|
|
.listStyle(.inset)
|
|
} else {
|
|
EmptyState(text: "검색어를 입력하세요")
|
|
}
|
|
}
|
|
.background(Sage.surface)
|
|
}
|
|
}
|