Răsfoiți Sursa

Add SwiftUI home screen with sidebar layout.

Migrate the main window to SwiftUI via NSHostingView with MVVM structure, themed UI components, and sidebar recents placed directly below the section header.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 3 săptămâni în urmă
părinte
comite
260112921b

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+DerivedData/
+.DS_Store
+.cursor/

+ 13 - 12
clone _of_clarus_ai_chat_bot/AppDelegate.swift

@@ -9,22 +9,23 @@ import Cocoa
 
 @main
 class AppDelegate: NSObject, NSApplicationDelegate {
-
-    
-
-
-    func applicationDidFinishLaunching(_ aNotification: Notification) {
-        // Insert code here to initialize your application
-    }
-
-    func applicationWillTerminate(_ aNotification: Notification) {
-        // Insert code here to tear down your application
+    func applicationDidFinishLaunching(_ notification: Notification) {
+        configureMainWindow()
     }
 
     func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
-        return true
+        true
     }
 
+    private func configureMainWindow() {
+        guard let window = NSApplication.shared.windows.first else { return }
 
+        window.title = "AI ChatBot Assistant"
+        window.setContentSize(NSSize(width: 1280, height: 820))
+        window.minSize = NSSize(width: 960, height: 640)
+        window.center()
+        window.isOpaque = true
+        window.backgroundColor = NSColor(red: 1.0, green: 0.97, blue: 0.96, alpha: 1.0)
+        window.appearance = NSAppearance(named: .aqua)
+    }
 }
-

+ 4 - 4
clone _of_clarus_ai_chat_bot/Base.lproj/Main.storyboard

@@ -683,10 +683,10 @@
         <scene sceneID="R2V-B0-nI4">
             <objects>
                 <windowController id="B8D-0N-5wS" sceneMemberID="viewController">
-                    <window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
+                    <window key="window" title="AI ChatBot Assistant" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="YES" animationBehavior="default" id="IQv-IB-iLA">
                         <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
                         <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
-                        <rect key="contentRect" x="196" y="240" width="480" height="270"/>
+                        <rect key="contentRect" x="196" y="120" width="1280" height="820"/>
                         <rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
                         <connections>
                             <outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
@@ -705,8 +705,8 @@
             <objects>
                 <viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
                     <view key="view" id="m2S-Jp-Qdl">
-                        <rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
-                        <autoresizingMask key="autoresizingMask"/>
+                        <rect key="frame" x="0.0" y="0.0" width="1280" height="820"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                     </view>
                 </viewController>
                 <customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>

+ 7 - 0
clone _of_clarus_ai_chat_bot/Models/QuickAction.swift

@@ -0,0 +1,7 @@
+import Foundation
+
+struct QuickAction: Identifiable {
+    let id = UUID()
+    let title: String
+    let symbol: String
+}

+ 7 - 0
clone _of_clarus_ai_chat_bot/Models/SidebarItem.swift

@@ -0,0 +1,7 @@
+import Foundation
+
+struct SidebarItem: Identifiable {
+    let id = UUID()
+    let title: String
+    let symbol: String
+}

+ 20 - 0
clone _of_clarus_ai_chat_bot/Utilities/AppTheme.swift

@@ -0,0 +1,20 @@
+import SwiftUI
+
+enum AppTheme {
+    static let accent = Color(red: 1.0, green: 0.55, blue: 0.38)
+    static let accentDark = Color(red: 0.95, green: 0.45, blue: 0.30)
+    static let pageBackground = Color(red: 1.0, green: 0.97, blue: 0.96)
+    static let sidebarBackground = Color.white
+    static let newChatBackground = Color(red: 1.0, green: 0.94, blue: 0.90)
+    static let textPrimary = Color(red: 0.12, green: 0.12, blue: 0.14)
+    static let textSecondary = Color(red: 0.45, green: 0.45, blue: 0.50)
+    static let textPlaceholder = Color(red: 0.72, green: 0.72, blue: 0.76)
+    static let avatarBackground = Color(red: 0.88, green: 0.88, blue: 0.90)
+    static let controlBackground = Color(red: 0.98, green: 0.98, blue: 0.99)
+    static let cardShadow = Color.black.opacity(0.06)
+
+    static let sidebarWidth: CGFloat = 248
+    static let cornerRadiusLarge: CGFloat = 20
+    static let cornerRadiusMedium: CGFloat = 12
+    static let cornerRadiusPill: CGFloat = 22
+}

+ 8 - 14
clone _of_clarus_ai_chat_bot/ViewController.swift

@@ -6,21 +6,15 @@
 //
 
 import Cocoa
+import SwiftUI
 
-class ViewController: NSViewController {
+final class ViewController: NSViewController {
+    private let viewModel = HomeViewModel()
 
-    override func viewDidLoad() {
-        super.viewDidLoad()
-
-        // Do any additional setup after loading the view.
-    }
-
-    override var representedObject: Any? {
-        didSet {
-        // Update the view, if already loaded.
-        }
+    override func loadView() {
+        let homeView = HomeView(viewModel: viewModel)
+        let hostingView = NSHostingView(rootView: homeView)
+        hostingView.autoresizingMask = [.width, .height]
+        self.view = hostingView
     }
-
-
 }
-

+ 21 - 0
clone _of_clarus_ai_chat_bot/ViewModels/HomeViewModel.swift

@@ -0,0 +1,21 @@
+import Combine
+import Foundation
+
+@MainActor
+final class HomeViewModel: ObservableObject {
+    @Published var promptText = ""
+    @Published var selectedModel = "Claude-Haiku"
+
+    let navigationItems: [SidebarItem] = [
+        SidebarItem(title: "History", symbol: "clock")
+    ]
+
+    let quickActions: [QuickAction] = [
+        QuickAction(title: "Code", symbol: "chevron.left.forwardslash.chevron.right"),
+        QuickAction(title: "Write", symbol: "pencil.line"),
+        QuickAction(title: "Learn", symbol: "graduationcap"),
+        QuickAction(title: "More Stuff", symbol: "cup.and.saucer")
+    ]
+
+    let availableModels = ["Claude-Haiku", "GPT-4o", "Claude-Sonnet"]
+}

+ 140 - 0
clone _of_clarus_ai_chat_bot/Views/ChatInputCard.swift

@@ -0,0 +1,140 @@
+import SwiftUI
+
+struct ChatInputCard: View {
+    @ObservedObject var viewModel: HomeViewModel
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 0) {
+            ZStack(alignment: .topLeading) {
+                if viewModel.promptText.isEmpty {
+                    Text("How can I help you today?")
+                        .font(.system(size: 22, weight: .regular))
+                        .foregroundStyle(AppTheme.textPlaceholder)
+                        .padding(.top, 2)
+                        .allowsHitTesting(false)
+                }
+
+                TextEditor(text: $viewModel.promptText)
+                    .font(.system(size: 22, weight: .regular))
+                    .foregroundStyle(AppTheme.textPrimary)
+                    .scrollContentBackground(.hidden)
+                    .background(Color.clear)
+                    .frame(minHeight: 88, maxHeight: 120)
+            }
+            .padding(.bottom, 16)
+
+            HStack(spacing: 10) {
+                CircleIconButton(symbol: "plus", accessibilityLabel: "Add attachment")
+
+                PillButton(title: "Web search", symbol: "globe")
+
+                Spacer(minLength: 8)
+
+                modelMenu
+
+                CircleIconButton(symbol: "mic", accessibilityLabel: "Voice input")
+
+                sendButton
+            }
+        }
+        .padding(.horizontal, 22)
+        .padding(.vertical, 20)
+        .background(
+            RoundedRectangle(cornerRadius: AppTheme.cornerRadiusLarge, style: .continuous)
+                .fill(Color.white)
+                .shadow(color: AppTheme.cardShadow, radius: 14, y: 4)
+        )
+    }
+
+    private var modelMenu: some View {
+        Menu {
+            ForEach(viewModel.availableModels, id: \.self) { model in
+                Button(model) {
+                    viewModel.selectedModel = model
+                }
+            }
+        } label: {
+            HStack(spacing: 6) {
+                Text(viewModel.selectedModel)
+                    .font(.system(size: 13, weight: .medium))
+                    .foregroundStyle(AppTheme.textPrimary)
+                Image(systemName: "chevron.down")
+                    .font(.system(size: 10, weight: .semibold))
+                    .foregroundStyle(AppTheme.textSecondary)
+            }
+            .padding(.horizontal, 12)
+            .padding(.vertical, 8)
+            .background(
+                RoundedRectangle(cornerRadius: 10, style: .continuous)
+                    .fill(AppTheme.controlBackground)
+            )
+        }
+        .menuStyle(.borderlessButton)
+        .fixedSize()
+    }
+
+    private var sendButton: some View {
+        Button(action: {}) {
+            HStack(spacing: 7) {
+                Image(systemName: "paperplane.fill")
+                    .font(.system(size: 12, weight: .bold))
+                Text("Send")
+                    .font(.system(size: 14, weight: .bold))
+            }
+            .foregroundStyle(.white)
+            .padding(.horizontal, 18)
+            .padding(.vertical, 10)
+            .background(
+                RoundedRectangle(cornerRadius: 12, style: .continuous)
+                    .fill(AppTheme.accent)
+            )
+        }
+        .buttonStyle(.plain)
+        .accessibilityLabel("Send message")
+    }
+}
+
+private struct CircleIconButton: View {
+    let symbol: String
+    let accessibilityLabel: String
+
+    var body: some View {
+        Button(action: {}) {
+            Image(systemName: symbol)
+                .font(.system(size: 13, weight: .semibold))
+                .foregroundStyle(AppTheme.accent)
+                .frame(width: 34, height: 34)
+                .background(
+                    Circle()
+                        .fill(AppTheme.controlBackground)
+                )
+        }
+        .buttonStyle(.plain)
+        .accessibilityLabel(accessibilityLabel)
+    }
+}
+
+private struct PillButton: View {
+    let title: String
+    let symbol: String
+
+    var body: some View {
+        Button(action: {}) {
+            HStack(spacing: 6) {
+                Image(systemName: symbol)
+                    .font(.system(size: 11, weight: .semibold))
+                Text(title)
+                    .font(.system(size: 13, weight: .medium))
+            }
+            .foregroundStyle(AppTheme.textPrimary)
+            .padding(.horizontal, 12)
+            .padding(.vertical, 8)
+            .background(
+                RoundedRectangle(cornerRadius: 10, style: .continuous)
+                    .fill(AppTheme.controlBackground)
+            )
+        }
+        .buttonStyle(.plain)
+        .accessibilityLabel(title)
+    }
+}

+ 15 - 0
clone _of_clarus_ai_chat_bot/Views/HomeView.swift

@@ -0,0 +1,15 @@
+import SwiftUI
+
+struct HomeView: View {
+    @ObservedObject var viewModel: HomeViewModel
+
+    var body: some View {
+        HStack(spacing: 0) {
+            SidebarView(viewModel: viewModel)
+            MainContentView(viewModel: viewModel)
+        }
+        .frame(maxWidth: .infinity, maxHeight: .infinity)
+        .background(AppTheme.pageBackground)
+        .preferredColorScheme(.light)
+    }
+}

+ 99 - 0
clone _of_clarus_ai_chat_bot/Views/MainContentView.swift

@@ -0,0 +1,99 @@
+import SwiftUI
+
+struct MainContentView: View {
+    @ObservedObject var viewModel: HomeViewModel
+
+    var body: some View {
+        VStack(spacing: 0) {
+            profileHeader
+                .padding(.horizontal, 32)
+                .padding(.top, 20)
+
+            Spacer(minLength: 20)
+
+            welcomeSection
+                .padding(.bottom, 28)
+
+            ChatInputCard(viewModel: viewModel)
+                .padding(.horizontal, 48)
+                .frame(maxWidth: 760)
+
+            quickActionRow
+                .padding(.top, 22)
+                .padding(.horizontal, 48)
+
+            Spacer(minLength: 40)
+        }
+        .frame(maxWidth: .infinity, maxHeight: .infinity)
+        .background(AppTheme.pageBackground)
+    }
+
+    private var profileHeader: some View {
+        HStack {
+            Spacer()
+            Circle()
+                .fill(AppTheme.avatarBackground)
+                .frame(width: 40, height: 40)
+                .overlay {
+                    Text("JD")
+                        .font(.system(size: 14, weight: .semibold))
+                        .foregroundStyle(AppTheme.textPrimary)
+                }
+                .accessibilityLabel("User profile")
+        }
+    }
+
+    private var welcomeSection: some View {
+        VStack(spacing: 10) {
+            Image(systemName: "sparkles")
+                .font(.system(size: 26, weight: .medium))
+                .foregroundStyle(AppTheme.accent)
+                .padding(.bottom, 4)
+
+            Text("Hi! Welcome")
+                .font(.system(size: 44, weight: .bold))
+                .foregroundStyle(AppTheme.textPrimary)
+
+            Text("What are you thinking about today?")
+                .font(.system(size: 22, weight: .medium))
+                .foregroundStyle(AppTheme.textSecondary)
+        }
+        .multilineTextAlignment(.center)
+    }
+
+    private var quickActionRow: some View {
+        HStack(spacing: 12) {
+            ForEach(viewModel.quickActions) { action in
+                QuickActionChip(action: action)
+            }
+        }
+        .frame(maxWidth: 760)
+    }
+}
+
+private struct QuickActionChip: View {
+    let action: QuickAction
+
+    var body: some View {
+        Button(action: {}) {
+            HStack(spacing: 7) {
+                Image(systemName: action.symbol)
+                    .font(.system(size: 12, weight: .semibold))
+                    .foregroundStyle(AppTheme.accent)
+
+                Text(action.title)
+                    .font(.system(size: 13, weight: .semibold))
+                    .foregroundStyle(AppTheme.textPrimary)
+            }
+            .padding(.horizontal, 18)
+            .padding(.vertical, 11)
+            .background(
+                RoundedRectangle(cornerRadius: AppTheme.cornerRadiusPill, style: .continuous)
+                    .fill(Color.white)
+                    .shadow(color: AppTheme.cardShadow, radius: 8, y: 2)
+            )
+        }
+        .buttonStyle(.plain)
+        .accessibilityLabel(action.title)
+    }
+}

+ 167 - 0
clone _of_clarus_ai_chat_bot/Views/SidebarView.swift

@@ -0,0 +1,167 @@
+import SwiftUI
+
+struct SidebarView: View {
+    @ObservedObject var viewModel: HomeViewModel
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 0) {
+            brandHeader
+                .padding(.bottom, 28)
+
+            newChatButton
+                .padding(.bottom, 20)
+
+            VStack(alignment: .leading, spacing: 4) {
+                ForEach(viewModel.navigationItems) { item in
+                    SidebarNavRow(item: item)
+                }
+            }
+
+            VStack(alignment: .leading, spacing: 0) {
+                Text("Recents")
+                    .font(.system(size: 15, weight: .bold))
+                    .foregroundStyle(AppTheme.textPrimary)
+                    .padding(.top, 28)
+                    .padding(.bottom, 16)
+
+                recentsEmptyState
+            }
+
+            Spacer(minLength: 12)
+
+            upgradeButton
+        }
+        .padding(.horizontal, 20)
+        .padding(.top, 24)
+        .padding(.bottom, 20)
+        .frame(width: AppTheme.sidebarWidth)
+        .frame(maxHeight: .infinity)
+        .background(AppTheme.sidebarBackground)
+    }
+
+    private var brandHeader: some View {
+        HStack(spacing: 10) {
+            RoundedRectangle(cornerRadius: 10, style: .continuous)
+                .fill(AppTheme.accent)
+                .frame(width: 36, height: 36)
+                .overlay {
+                    Image(systemName: "sparkle")
+                        .font(.system(size: 16, weight: .bold))
+                        .foregroundStyle(.white)
+                }
+
+            Text("AI ChatBot")
+                .font(.system(size: 17, weight: .bold))
+                .foregroundStyle(AppTheme.textPrimary)
+        }
+    }
+
+    private var newChatButton: some View {
+        Button(action: {}) {
+            HStack(spacing: 10) {
+                Image(systemName: "plus")
+                    .font(.system(size: 13, weight: .bold))
+                Text("New Chat")
+                    .font(.system(size: 14, weight: .semibold))
+            }
+            .foregroundStyle(AppTheme.accentDark)
+            .frame(maxWidth: .infinity, alignment: .leading)
+            .padding(.horizontal, 14)
+            .padding(.vertical, 11)
+            .background(
+                RoundedRectangle(cornerRadius: AppTheme.cornerRadiusMedium, style: .continuous)
+                    .fill(AppTheme.newChatBackground)
+            )
+        }
+        .buttonStyle(.plain)
+        .accessibilityLabel("New Chat")
+    }
+
+    private var recentsEmptyState: some View {
+        VStack(alignment: .leading, spacing: 10) {
+            ZStack(alignment: .topTrailing) {
+                Image(systemName: "bubble.left.and.bubble.right")
+                    .font(.system(size: 34))
+                    .foregroundStyle(AppTheme.accent.opacity(0.35))
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .padding(.top, 8)
+
+                Circle()
+                    .fill(AppTheme.accent)
+                    .frame(width: 18, height: 18)
+                    .overlay {
+                        Image(systemName: "xmark")
+                            .font(.system(size: 8, weight: .bold))
+                            .foregroundStyle(.white)
+                    }
+                    .offset(x: 8, y: 0)
+            }
+            .frame(maxWidth: .infinity)
+            .padding(.bottom, 4)
+
+            Text("No recents Chats")
+                .font(.system(size: 15, weight: .bold))
+                .foregroundStyle(AppTheme.textPrimary)
+
+            Text("Your conversations will\nappear here.")
+                .font(.system(size: 12))
+                .foregroundStyle(AppTheme.textSecondary)
+                .lineSpacing(2)
+        }
+        .frame(maxWidth: .infinity, alignment: .leading)
+    }
+
+    private var upgradeButton: some View {
+        Button(action: {}) {
+            HStack(spacing: 8) {
+                Image(systemName: "sparkles")
+                    .font(.system(size: 13, weight: .semibold))
+                    .foregroundStyle(AppTheme.accent)
+
+                Text("Upgrade to pro")
+                    .font(.system(size: 13, weight: .semibold))
+                    .foregroundStyle(AppTheme.textPrimary)
+
+                Spacer()
+
+                Image(systemName: "chevron.right")
+                    .font(.system(size: 10, weight: .bold))
+                    .foregroundStyle(AppTheme.textSecondary)
+            }
+            .padding(.horizontal, 14)
+            .padding(.vertical, 12)
+            .background(
+                RoundedRectangle(cornerRadius: AppTheme.cornerRadiusMedium, style: .continuous)
+                    .stroke(AppTheme.accent.opacity(0.45), lineWidth: 1)
+                    .background(
+                        RoundedRectangle(cornerRadius: AppTheme.cornerRadiusMedium, style: .continuous)
+                            .fill(Color.white)
+                    )
+            )
+        }
+        .buttonStyle(.plain)
+        .accessibilityLabel("Upgrade to pro")
+    }
+}
+
+private struct SidebarNavRow: View {
+    let item: SidebarItem
+
+    var body: some View {
+        Button(action: {}) {
+            HStack(spacing: 12) {
+                Image(systemName: item.symbol)
+                    .font(.system(size: 14, weight: .regular))
+                    .frame(width: 18)
+                Text(item.title)
+                    .font(.system(size: 14, weight: .medium))
+            }
+            .foregroundStyle(AppTheme.textPrimary)
+            .frame(maxWidth: .infinity, alignment: .leading)
+            .padding(.horizontal, 10)
+            .padding(.vertical, 9)
+        }
+        .buttonStyle(.plain)
+        .accessibilityLabel(item.title)
+    }
+}