// // ChatView.swift // Zyquo Local // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // import SwiftUI /// The chat detail column: 52 pt header, centered transcript, floating input. struct ChatView: View { @Environment(AppModel.self) private var app var body: some View { Group { if app.store.models.isEmpty && app.downloads.tasks.isEmpty { OnboardingHeroView() } else if let conversation = app.selectedConversation { chatBody(conversation) } else { emptySelection } } .background(ZyquoTheme.background) } @ViewBuilder private func chatBody(_ conversation: Conversation) -> some View { VStack(spacing: 0) { ChatHeaderView(conversation: conversation) Rectangle().fill(ZyquoTheme.border).frame(height: ZyquoTheme.hairline) ZStack { TranscriptView(conversation: conversation) if case .loading(let repoID) = app.engineState { ModelLoadingCard(repoID: repoID) } } InputBar(conversation: conversation) } } private var emptySelection: some View { VStack(spacing: ZyquoTheme.Spacing.s) { ZyquoGlyph(size: 40, color: ZyquoTheme.textTertiary) Text("Select a chat or press ⌘N") .font(ZyquoTheme.body) .foregroundStyle(ZyquoTheme.textSecondary) } .frame(maxWidth: .infinity, maxHeight: .infinity) } } /// Elegant centered card while a model loads; input is disabled meanwhile. struct ModelLoadingCard: View { let repoID: String @State private var appeared = false var body: some View { VStack(spacing: ZyquoTheme.Spacing.m) { ProgressView() .controlSize(.large) .tint(ZyquoTheme.accent) Text("Loading \(shortModelName(repoID))") .font(ZyquoTheme.bodyEmphasis) .foregroundStyle(ZyquoTheme.textPrimary) Text("Allocating unified memory for the model weights…") .font(ZyquoTheme.caption) .foregroundStyle(ZyquoTheme.textSecondary) } .padding(ZyquoTheme.Spacing.xxl) .background(ZyquoTheme.surface, in: RoundedRectangle(cornerRadius: ZyquoTheme.Radius.l)) .overlay( RoundedRectangle(cornerRadius: ZyquoTheme.Radius.l) .stroke(ZyquoTheme.border, lineWidth: ZyquoTheme.hairline) ) .floatingShadow() .opacity(appeared ? 1 : 0) .offset(y: appeared ? 0 : 6) .onAppear { withAnimation(.easeOut(duration: 0.15)) { appeared = true } } } }