@@ -196,26 +196,107 @@ class DashboardViewModel: ObservableObject {
196196struct DashboardPage : View {
197197 @ObservedObject var viewModel : DashboardViewModel
198198 @ObservedObject var appState : AppState
199+ @ObservedObject var appProvider : AppProvider
200+ @ObservedObject var chatProvider : ChatProvider
199201 @Binding var selectedIndex : Int
200- @State private var selectedConversation : ServerConversation ? = nil
202+ @State private var citedConversation : ServerConversation ? = nil
203+ @State private var isLoadingCitation = false
204+
205+ private var selectedApp : OmiApp ? {
206+ guard let appId = chatProvider. selectedAppId else { return nil }
207+ return appProvider. chatApps. first { $0. id == appId }
208+ }
201209
202210 var body : some View {
203- Group {
204- if selectedConversation != nil {
205- // Full-page conversation detail (hides dashboard)
206- ConversationsPage ( appState: appState, selectedConversation: $selectedConversation)
207- } else {
208- // Dashboard + conversations in one scrollable page
209- ScrollView {
210- VStack ( spacing: 0 ) {
211- dashboardWidgets
212- ConversationsPage ( appState: appState, selectedConversation: $selectedConversation, embedded: true )
211+ VStack ( spacing: 0 ) {
212+ dashboardWidgets
213+
214+ // Chat messages — fills remaining vertical space, scrolls internally,
215+ // morphs into the dashboard background (no card chrome).
216+ // A bottom gradient mask softens messages into the background near
217+ // the input field so they fade out instead of clipping abruptly.
218+ ChatMessagesView (
219+ messages: chatProvider. messages,
220+ isSending: chatProvider. isSending,
221+ hasMoreMessages: chatProvider. hasMoreMessages,
222+ isLoadingMoreMessages: chatProvider. isLoadingMoreMessages,
223+ isLoadingInitial: ( chatProvider. isLoading || chatProvider. isLoadingSessions)
224+ && !chatProvider. isClearing,
225+ app: selectedApp,
226+ onLoadMore: { await chatProvider. loadMoreMessages ( ) } ,
227+ onRate: { messageId, rating in
228+ Task { await chatProvider. rateMessage ( messageId, rating: rating) }
229+ } ,
230+ onCitationTap: { citation in
231+ handleCitationTap ( citation)
232+ } ,
233+ sessionsLoadError: chatProvider. sessionsLoadError,
234+ onRetry: { Task { await chatProvider. retryLoad ( ) } } ,
235+ welcomeContent: { dashboardChatWelcome }
236+ )
237+ . frame ( maxWidth: . infinity, maxHeight: . infinity)
238+ . mask (
239+ LinearGradient (
240+ stops: [
241+ . init( color: . clear, location: 0.0 ) ,
242+ . init( color: . black, location: 0.08 ) ,
243+ . init( color: . black, location: 0.92 ) ,
244+ . init( color: . clear, location: 1.0 ) ,
245+ ] ,
246+ startPoint: . top,
247+ endPoint: . bottom
248+ )
249+ )
250+
251+ ChatInputView (
252+ onSend: { text in
253+ AnalyticsManager . shared. chatMessageSent (
254+ messageLength: text. count, hasContext: selectedApp != nil , source: " dashboard_chat " )
255+ Task { await chatProvider. sendMessage ( text) }
256+ } ,
257+ onFollowUp: { text in
258+ Task { await chatProvider. sendFollowUp ( text) }
259+ } ,
260+ onStop: {
261+ chatProvider. stopAgent ( )
262+ } ,
263+ isSending: chatProvider. isSending,
264+ isStopping: chatProvider. isStopping,
265+ placeholder: " Ask omi anything " ,
266+ mode: $chatProvider. chatMode,
267+ inputText: $chatProvider. draftText
268+ )
269+ . padding ( . horizontal, 30 )
270+ . padding ( . top, 12 )
271+ . padding ( . bottom, 20 )
272+ }
273+ . frame ( maxWidth: . infinity, maxHeight: . infinity)
274+ . background ( Color . clear)
275+ . sheet ( item: $citedConversation) { conversation in
276+ ConversationDetailView (
277+ conversation: conversation,
278+ onBack: {
279+ citedConversation = nil
280+ }
281+ )
282+ . frame ( minWidth: 500 , minHeight: 500 )
283+ }
284+ . overlay {
285+ if isLoadingCitation {
286+ ZStack {
287+ Color . black. opacity ( 0.3 )
288+ VStack ( spacing: 12 ) {
289+ ProgressView ( )
290+ Text ( " Loading source... " )
291+ . scaledFont ( size: 13 )
292+ . foregroundColor ( . white)
213293 }
294+ . padding ( 20 )
295+ . background ( OmiColors . backgroundSecondary)
296+ . cornerRadius ( 12 )
214297 }
215298 }
216299 }
217- . frame ( maxWidth: . infinity, maxHeight: . infinity)
218- . background ( Color . clear)
219300 . onAppear {
220301 if PostOnboardingPromptSuggestions . shouldShowPopup && !postOnboardingSuggestions. isEmpty {
221302 NotificationCenter . default. post ( name: . showTryAskingPopup, object: nil )
@@ -226,6 +307,58 @@ struct DashboardPage: View {
226307 }
227308 }
228309
310+ /// Welcome message shown when there are no chat messages yet.
311+ /// Transparent — no card chrome — so it morphs into the dashboard background.
312+ private var dashboardChatWelcome : some View {
313+ VStack ( spacing: 12 ) {
314+ if let logoURL = Bundle . resourceBundle. url ( forResource: " herologo " , withExtension: " png " ) ,
315+ let logoImage = NSImage ( contentsOf: logoURL)
316+ {
317+ Image ( nsImage: logoImage)
318+ . resizable ( )
319+ . scaledToFit ( )
320+ . frame ( width: 40 , height: 40 )
321+ }
322+
323+ Text ( " Ask omi anything " )
324+ . scaledFont ( size: 16 , weight: . semibold)
325+ . foregroundColor ( OmiColors . textPrimary)
326+
327+ Text ( " Your personal AI assistant — knows you through your memories and conversations " )
328+ . scaledFont ( size: 13 )
329+ . foregroundColor ( OmiColors . textSecondary)
330+ . multilineTextAlignment ( . center)
331+ . padding ( . horizontal, 40 )
332+ }
333+ . frame ( maxWidth: . infinity)
334+ . padding ( . vertical, 32 )
335+ }
336+
337+ /// Handle tapping on a citation card — opens the cited conversation in a sheet.
338+ private func handleCitationTap( _ citation: Citation ) {
339+ guard citation. sourceType == . conversation else {
340+ log ( " Citation tapped: \( citation. title) (memory - no detail view) " )
341+ return
342+ }
343+
344+ isLoadingCitation = true
345+
346+ Task {
347+ do {
348+ let conversation = try await APIClient . shared. getConversation ( id: citation. id)
349+ await MainActor . run {
350+ citedConversation = conversation
351+ isLoadingCitation = false
352+ }
353+ } catch {
354+ logError ( " Failed to fetch cited conversation " , error: error)
355+ await MainActor . run {
356+ isLoadingCitation = false
357+ }
358+ }
359+ }
360+ }
361+
229362 private var dashboardWidgets : some View {
230363 VStack ( alignment: . leading, spacing: 28 ) {
231364 if shouldShowSuggestionBanner {
@@ -290,8 +423,8 @@ struct DashboardPage: View {
290423 }
291424 }
292425 . padding ( . horizontal, 30 )
293- . padding ( . top, 40 )
294- . padding ( . bottom, 16 )
426+ . padding ( . top, 32 )
427+ . padding ( . bottom, 12 )
295428 }
296429
297430 private var postOnboardingSuggestions : [ String ] {
@@ -315,7 +448,13 @@ struct DashboardPage: View {
315448}
316449
317450#Preview {
318- DashboardPage ( viewModel: DashboardViewModel ( ) , appState: AppState ( ) , selectedIndex: . constant( 0 ) )
319- . frame ( width: 800 , height: 600 )
320- . background ( OmiColors . backgroundPrimary)
451+ DashboardPage (
452+ viewModel: DashboardViewModel ( ) ,
453+ appState: AppState ( ) ,
454+ appProvider: AppProvider ( ) ,
455+ chatProvider: ChatProvider ( ) ,
456+ selectedIndex: . constant( 0 )
457+ )
458+ . frame ( width: 800 , height: 600 )
459+ . background ( OmiColors . backgroundPrimary)
321460}
0 commit comments