Nucleic-Session: 19C20841-87D4-45F9-A5AD-EBB92B735F82 Co-authored-by: Nucleic <[email protected]>
105 lines
4.7 KiB
Swift
105 lines
4.7 KiB
Swift
import SwiftUI
|
|
import NucleicProtocol
|
|
|
|
/// Projects overview (mirrors the Mac's per-project pages): each project with its session
|
|
/// counts; tap through to that project's sessions + a scoped composer.
|
|
struct ProjectsView: View {
|
|
@EnvironmentObject var store: RemoteStore
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Group {
|
|
if store.dashboard.projects.isEmpty {
|
|
ContentUnavailableView("No projects", systemImage: "folder",
|
|
description: Text("Add a project on the Mac to see it here."))
|
|
} else {
|
|
List(store.dashboard.projects) { project in
|
|
NavigationLink {
|
|
ProjectDetailView(project: project)
|
|
} label: {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: "folder.fill").foregroundStyle(Palette.accent)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(project.name).font(.body.weight(.medium))
|
|
Text(project.defaultBranch).font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
Spacer()
|
|
if project.activeCount > 0 {
|
|
Text("\(project.activeCount) active")
|
|
.font(.caption2.bold())
|
|
.padding(.horizontal, 7).padding(.vertical, 3)
|
|
.background(Palette.active.opacity(0.18), in: Capsule())
|
|
.foregroundStyle(Palette.active)
|
|
}
|
|
Text("\(project.sessionCount)").foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Projects")
|
|
.refreshable { store.refreshSessions() }
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ProjectDetailView: View {
|
|
@EnvironmentObject var store: RemoteStore
|
|
let project: WireProject
|
|
@State private var draft = ""
|
|
@State private var model: String?
|
|
@State private var effort = MobileEfforts.fallback
|
|
|
|
private var sessions: [WireSessionSummary] {
|
|
store.liveSessions
|
|
.filter { $0.projectID == project.id.rawValue }
|
|
.sorted(by: StatusStyle.attentionThenRecency)
|
|
}
|
|
|
|
var body: some View {
|
|
List {
|
|
Section {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack {
|
|
ModelMenu(model: $model, catalog: store.modelCatalog, backend: nil)
|
|
Spacer()
|
|
EffortMenu(effort: $effort, catalog: store.modelCatalog, modelSKU: model,
|
|
controlled: project.isNucleicControlled)
|
|
}
|
|
HStack(alignment: .bottom, spacing: 8) {
|
|
TextField("Start a chat in \(project.name)…", text: $draft, axis: .vertical)
|
|
.textFieldStyle(.roundedBorder).lineLimit(1...4)
|
|
.orchestraGlow(active: MobileEfforts.isOrchestra(effort) && project.isNucleicControlled)
|
|
.keyboardDismissable()
|
|
Button {
|
|
store.startChat(in: project.id, message: draft, model: model, effort: effort)
|
|
draft = ""
|
|
} label: { Image(systemName: "arrow.up.circle.fill").font(.title2) }
|
|
.disabled(draft.trimmingCharacters(in: .whitespaces).isEmpty || !store.canControl)
|
|
}
|
|
}
|
|
}
|
|
Section("Sessions") {
|
|
if sessions.isEmpty {
|
|
Text("No sessions yet.").foregroundStyle(.secondary)
|
|
}
|
|
ForEach(sessions, id: \.sessionID) { summary in
|
|
NavigationLink {
|
|
SessionDetailView(sessionID: summary.sessionID)
|
|
} label: { SessionRow(summary: summary, showProjectName: false) }
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle(project.name)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.scrollDismissesKeyboard(.interactively)
|
|
.onChange(of: model) { _, newModel in
|
|
let levels = store.modelCatalog.offeredEfforts(forModel: newModel)
|
|
let sentinel = store.modelCatalog.orchestraSentinelOrFallback
|
|
if effort.caseInsensitiveCompare(sentinel) != .orderedSame, !levels.contains(effort) {
|
|
effort = levels.last ?? MobileEfforts.fallback
|
|
}
|
|
}
|
|
}
|
|
}
|