Skip to content

Commit da98182

Browse files
committed
UX: radical simplification — zero friction, maximum clarity
iOS: 3 tabs (Focus → Capture → Insights), removed Context tab macOS: 4 sidebar items (Focus, Insights, Memory, Decisions), removed 5 sections DailyFocusView: no section headers, 'Ignored today' first-class, feedback loop (Yes/No) QuickCaptureView: single text field, auto URL detection, no labels/categories Backend: POST /context/feedback endpoint, stores in working memory CortexEngine + APIService: sendFeedback() for the UX loop 3 new tests (279 total, all passing) Design philosophy: Open → Understand → Act → Close
1 parent 851dd5b commit da98182

File tree

9 files changed

+293
-173
lines changed

9 files changed

+293
-173
lines changed

CortexOSApp/Shared/Models/DecisionResult.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,10 @@ struct InsightCreateRequest: Codable {
9797
case relatedProject = "related_project"
9898
}
9999
}
100+
101+
// MARK: - Feedback
102+
103+
struct FeedbackRequest: Codable {
104+
let item: String
105+
let useful: Bool
106+
}

CortexOSApp/Shared/Services/APIService.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,12 @@ final class APIService: ObservableObject {
223223
func storeInsight(_ body: InsightCreateRequest) async throws -> SyncInsight {
224224
try await request("POST", path: "/context/insight", body: body)
225225
}
226+
227+
// MARK: - Feedback
228+
229+
func sendFeedback(_ body: FeedbackRequest) async throws {
230+
try await requestNoContent("POST", path: "/context/feedback", body: body)
231+
}
226232
}
227233

228234
// MARK: - Helpers

CortexOSApp/Shared/Services/CortexEngine.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,14 @@ final class CortexEngine: ObservableObject {
209209
errorMessage = error.localizedDescription
210210
}
211211
}
212+
213+
// MARK: - Feedback (was this useful?)
214+
215+
func sendFeedback(item: String, useful: Bool) async {
216+
do {
217+
try await api.sendFeedback(FeedbackRequest(item: item, useful: useful))
218+
} catch {
219+
// Silent — feedback is best-effort, never block UX
220+
}
221+
}
212222
}

CortexOSApp/Shared/Views/ContentView.swift

Lines changed: 22 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
// ContentView.swift
33
// CortexOS
44
//
5-
// Root navigation — iOS: 4 focused tabs, macOS: sidebar + detail.
6-
// Pulls sync snapshot on launch.
5+
// Root navigation — radically simple.
6+
// iOS: Focus is the hero. Capture + Insights secondary.
7+
// macOS: 4 sidebar items. That's it.
78
//
89

910
import SwiftUI
@@ -19,80 +20,52 @@ struct ContentView: View {
1920
#endif
2021
}
2122

22-
// MARK: - iOS (4 tabs — daily decisions)
23+
// MARK: - iOS (3 tabs — Focus first, always)
2324

2425
#if os(iOS)
2526
private var iOSRoot: some View {
2627
TabView {
2728
NavigationStack { DailyFocusView() }
2829
.tabItem { Label("Focus", systemImage: "sparkles") }
2930

30-
NavigationStack { InsightFeedView() }
31-
.tabItem { Label("Insights", systemImage: "lightbulb") }
32-
3331
NavigationStack { QuickCaptureView() }
3432
.tabItem { Label("Capture", systemImage: "plus.circle") }
3533

36-
NavigationStack { ContextView() }
37-
.tabItem { Label("Context", systemImage: "brain.head.profile") }
34+
NavigationStack { InsightFeedView() }
35+
.tabItem { Label("Insights", systemImage: "lightbulb") }
3836
}
3937
.tint(CortexColor.accent)
4038
.environmentObject(engine)
4139
.task { await engine.sync() }
4240
}
4341
#endif
4442

45-
// MARK: - macOS (sidebar — deep thinking)
43+
// MARK: - macOS (4 sidebar items — clarity, not chrome)
4644

4745
#if os(macOS)
4846
@State private var selection: MacSection? = .focus
4947

5048
private var macOSRoot: some View {
5149
NavigationSplitView {
5250
List(selection: $selection) {
53-
Section("Intelligence") {
54-
Label("Focus", systemImage: "sparkles")
55-
.tag(MacSection.focus)
56-
Label("Insights", systemImage: "lightbulb")
57-
.tag(MacSection.insights)
58-
Label("Signals", systemImage: "antenna.radiowaves.left.and.right")
59-
.tag(MacSection.signals)
60-
}
61-
62-
Section("Depth") {
63-
Label("Decisions", systemImage: "checkmark.seal")
64-
.tag(MacSection.decisions)
65-
Label("Memory", systemImage: "brain")
66-
.tag(MacSection.memory)
67-
Label("Knowledge", systemImage: "doc.text")
68-
.tag(MacSection.knowledge)
69-
}
70-
71-
Section("System") {
72-
Label("Pipeline", systemImage: "arrow.triangle.branch")
73-
.tag(MacSection.pipeline)
74-
Label("Profile", systemImage: "person.crop.circle")
75-
.tag(MacSection.profile)
76-
Label("Settings", systemImage: "gear")
77-
.tag(MacSection.settings)
78-
}
51+
Label("Focus", systemImage: "sparkles")
52+
.tag(MacSection.focus)
53+
Label("Insights", systemImage: "lightbulb")
54+
.tag(MacSection.insights)
55+
Label("Memory", systemImage: "brain")
56+
.tag(MacSection.memory)
57+
Label("Decisions", systemImage: "checkmark.seal")
58+
.tag(MacSection.decisions)
7959
}
8060
.navigationTitle("CortexOS")
8161
.listStyle(.sidebar)
8262
} detail: {
83-
Group {
84-
switch selection {
85-
case .focus: DailyFocusView()
86-
case .insights: InsightFeedView()
87-
case .signals: SignalsView()
88-
case .decisions: DecisionHistoryView()
89-
case .memory: MemoryExplorerView()
90-
case .knowledge: KnowledgeListView()
91-
case .pipeline: PipelineView()
92-
case .profile: ProfileView()
93-
case .settings: SettingsView()
94-
case nil: DailyFocusView()
95-
}
63+
switch selection {
64+
case .focus: DailyFocusView()
65+
case .insights: InsightFeedView()
66+
case .memory: MemoryExplorerView()
67+
case .decisions: DecisionHistoryView()
68+
case nil: DailyFocusView()
9669
}
9770
}
9871
.environmentObject(engine)
@@ -101,7 +74,7 @@ struct ContentView: View {
10174
}
10275

10376
enum MacSection: Hashable {
104-
case focus, insights, signals, decisions, memory, knowledge, pipeline, profile, settings
77+
case focus, insights, memory, decisions
10578
}
10679
#endif
10780
}

CortexOSApp/Shared/Views/DailyFocusView.swift

Lines changed: 140 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
// DailyFocusView.swift
33
// CortexOS
44
//
5-
// The most important screen. Shows today's top priorities,
6-
// why they matter, and the next action. No scrolling if ≤ 3.
7-
// Feels like "today matters".
5+
// The only screen that really matters.
6+
// Open → Understand → Act → Close.
7+
// No scrolling if ≤ 3 priorities. No clutter. Pre-computed clarity.
88
//
99

1010
import SwiftUI
@@ -33,56 +33,65 @@ struct DailyFocusView: View {
3333
.refreshable { await engine.sync() }
3434
}
3535

36-
// MARK: - Sync-powered focus (priority brief)
36+
// MARK: - Sync-powered focus
3737

3838
@ViewBuilder
3939
private func focusContent(_ brief: PriorityBrief) -> some View {
40-
ScrollView {
41-
VStack(alignment: .leading, spacing: CortexSpacing.xl) {
42-
// Date header
43-
Text(brief.date)
44-
.font(CortexFont.caption)
45-
.foregroundStyle(CortexColor.textTertiary)
46-
47-
// Priorities — the core
48-
if !brief.priorities.isEmpty {
49-
VStack(alignment: .leading, spacing: CortexSpacing.sm) {
50-
Text("Priorities")
51-
.font(CortexFont.headline)
52-
.foregroundStyle(CortexColor.textPrimary)
40+
let needsScroll = brief.priorities.count > 3
5341

54-
PriorityList(priorities: brief.priorities)
55-
}
56-
}
42+
Group {
43+
if needsScroll {
44+
ScrollView { focusBody(brief) }
45+
} else {
46+
focusBody(brief)
47+
}
48+
}
49+
}
5750

58-
// Emerging signals
59-
if !brief.emergingSignals.isEmpty {
60-
VStack(alignment: .leading, spacing: CortexSpacing.sm) {
61-
Text("Emerging Signals")
62-
.font(CortexFont.captionMedium)
63-
.foregroundStyle(CortexColor.textSecondary)
51+
@ViewBuilder
52+
private func focusBody(_ brief: PriorityBrief) -> some View {
53+
VStack(alignment: .leading, spacing: CortexSpacing.lg) {
54+
// Date — subtle
55+
Text(brief.date)
56+
.font(CortexFont.caption)
57+
.foregroundStyle(CortexColor.textTertiary)
58+
.padding(.bottom, CortexSpacing.xs)
6459

65-
FlowTags(items: brief.emergingSignals)
66-
}
60+
// Priorities — no header, just show them
61+
ForEach(Array(brief.priorities.prefix(5).enumerated()), id: \.element.title) { index, priority in
62+
FocusPriorityCard(priority: priority, position: index + 1) { useful in
63+
Task { await engine.sendFeedback(item: priority.title, useful: useful) }
6764
}
65+
}
6866

69-
// Changes since yesterday
70-
if !brief.changesSinceYesterday.isEmpty {
71-
VStack(alignment: .leading, spacing: CortexSpacing.sm) {
72-
Text("Changed Since Yesterday")
73-
.font(CortexFont.captionMedium)
74-
.foregroundStyle(CortexColor.textSecondary)
67+
// Ignored today — first-class citizen
68+
if !brief.ignored.isEmpty {
69+
VStack(alignment: .leading, spacing: CortexSpacing.xs) {
70+
Text("Ignored today")
71+
.font(CortexFont.captionMedium)
72+
.foregroundStyle(CortexColor.textTertiary)
7573

76-
ForEach(brief.changesSinceYesterday, id: \.self) { change in
77-
Label(change, systemImage: "arrow.triangle.2.circlepath")
74+
ForEach(brief.ignored, id: \.self) { item in
75+
HStack(spacing: CortexSpacing.xs) {
76+
Image(systemName: "minus.circle")
77+
.font(.caption2)
78+
.foregroundStyle(CortexColor.textTertiary)
79+
Text(item)
7880
.font(CortexFont.caption)
79-
.foregroundStyle(CortexColor.textSecondary)
81+
.foregroundStyle(CortexColor.textTertiary)
8082
}
8183
}
8284
}
85+
.padding(.top, CortexSpacing.sm)
86+
}
87+
88+
// Emerging signals — compact pills, only if present
89+
if !brief.emergingSignals.isEmpty {
90+
FlowTags(items: brief.emergingSignals)
91+
.padding(.top, CortexSpacing.xs)
8392
}
84-
.padding(CortexSpacing.xl)
8593
}
94+
.padding(CortexSpacing.xl)
8695
}
8796

8897
// MARK: - Legacy focus (DailyBrief from /focus/today)
@@ -104,7 +113,99 @@ struct DailyFocusView: View {
104113
}
105114
}
106115

107-
// MARK: - Legacy row (for /focus/today data)
116+
// MARK: - Focus Priority Card (with feedback)
117+
118+
private struct FocusPriorityCard: View {
119+
let priority: SyncPriority
120+
let position: Int
121+
let onFeedback: (Bool) -> Void
122+
123+
@State private var feedbackGiven: Bool? = nil
124+
125+
var body: some View {
126+
VStack(alignment: .leading, spacing: CortexSpacing.sm) {
127+
HStack(alignment: .top, spacing: CortexSpacing.md) {
128+
// Rank badge
129+
Text("\(position)")
130+
.font(CortexFont.captionMedium)
131+
.foregroundStyle(.white)
132+
.frame(width: 24, height: 24)
133+
.background(CortexColor.rank(position))
134+
.clipShape(Circle())
135+
136+
VStack(alignment: .leading, spacing: CortexSpacing.xs) {
137+
Text(priority.title)
138+
.font(CortexFont.bodyMedium)
139+
.foregroundStyle(CortexColor.textPrimary)
140+
141+
if !priority.whyItMatters.isEmpty {
142+
Text(priority.whyItMatters)
143+
.font(CortexFont.caption)
144+
.foregroundStyle(CortexColor.textSecondary)
145+
.lineLimit(2)
146+
}
147+
148+
if !priority.nextStep.isEmpty {
149+
Label {
150+
Text(priority.nextStep)
151+
.lineLimit(1)
152+
} icon: {
153+
Image(systemName: "arrow.right.circle.fill")
154+
}
155+
.font(CortexFont.caption)
156+
.foregroundStyle(CortexColor.accent)
157+
}
158+
}
159+
160+
Spacer(minLength: 0)
161+
}
162+
163+
// Feedback — was this useful?
164+
if feedbackGiven == nil {
165+
HStack(spacing: CortexSpacing.md) {
166+
Spacer()
167+
Button { submitFeedback(true) } label: {
168+
Label("Yes", systemImage: "hand.thumbsup")
169+
.font(CortexFont.caption)
170+
}
171+
.buttonStyle(.plain)
172+
.foregroundStyle(CortexColor.textTertiary)
173+
174+
Button { submitFeedback(false) } label: {
175+
Label("No", systemImage: "hand.thumbsdown")
176+
.font(CortexFont.caption)
177+
}
178+
.buttonStyle(.plain)
179+
.foregroundStyle(CortexColor.textTertiary)
180+
}
181+
} else {
182+
HStack {
183+
Spacer()
184+
Label(
185+
feedbackGiven == true ? "Noted" : "Got it",
186+
systemImage: "checkmark"
187+
)
188+
.font(CortexFont.caption)
189+
.foregroundStyle(CortexColor.textTertiary)
190+
}
191+
.transition(.opacity)
192+
}
193+
}
194+
.padding(CortexSpacing.md)
195+
.background(CortexColor.bgSurface)
196+
.clipShape(RoundedRectangle(cornerRadius: CortexRadius.card, style: .continuous))
197+
.cortexShadow()
198+
}
199+
200+
private func submitFeedback(_ useful: Bool) {
201+
withAnimation(.easeOut(duration: 0.2)) {
202+
feedbackGiven = useful
203+
}
204+
onFeedback(useful)
205+
}
206+
}
207+
208+
// MARK: - Legacy row
108209

109210
private struct LegacyFocusRow: View {
110211
let item: FocusItem
@@ -151,7 +252,6 @@ struct FlowTags: View {
151252
let items: [String]
152253

153254
var body: some View {
154-
// Simple horizontal scroll for now — wrapping layout is complex
155255
ScrollView(.horizontal, showsIndicators: false) {
156256
HStack(spacing: CortexSpacing.xs) {
157257
ForEach(items, id: \.self) { item in

0 commit comments

Comments
 (0)