Newer
Older
navi-1 / webclient / src / App.vue
<template>
  <GnToastProvider>
    <div class="app-shell">
      <!-- Mobile sidebar backdrop -->
    <div
      v-if="sidebarOpen"
      class="sidebar-backdrop"
      @click="closeSidebar"
    />

    <Transition name="backdrop-fade">
      <div
        v-if="artifactsOpen"
        class="artifacts-backdrop"
        @click="closeArtifacts"
      />
    </Transition>

    <!-- Sidebar (desktop: always visible; mobile: drawer) -->
    <AppSidebar
      :mobile-open="sidebarOpen"
      :class="{ 'is-hidden-by-artifacts': artifactsOpen }"
      @close="closeSidebar"
    />

    <!-- Global confirm dialog -->
    <ConfirmDialog />

    <!-- Global image lightbox -->
    <ImageLightbox />

    <!-- Selection reply toolbar -->
    <SelectionToolbar />

    <!-- Main content -->
    <div class="app-main">
      <Transition name="fade" mode="out-in">
        <WelcomeScreen
          v-if="showWelcome"
          key="welcome"
          @toggle-sidebar="toggleSidebar"
        />
        <SettingsView
          v-else-if="route === 'settings'"
          key="settings"
          @toggle-sidebar="toggleSidebar"
        />
        <ChatArea
          v-else
          key="chat"
          @toggle-sidebar="toggleSidebar"
          @toggle-artifacts="toggleArtifacts"
        />
      </Transition>
    </div>

    <ArtifactsPanel
      :open="artifactsOpen"
      @close="closeArtifacts"
    />

    <!-- Login overlay: blocks the UI when auth is required but user is not logged in -->
    <LoginScreen
      v-if="authStore.authConfigured && !authStore.isAuthenticated && !authStore.loading"
    />
  </div>
  </GnToastProvider>
</template>


<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useSessionsStore } from '@/stores/sessions'
import { useProfilesStore } from '@/stores/profiles'
import { useChatStore } from '@/stores/chat'
import { useAuthStore } from '@/stores/auth'
import AppSidebar from '@/components/sidebar/AppSidebar.vue'
import ConfirmDialog from '@/components/ui/ConfirmDialog.vue'
import ImageLightbox from '@/components/ui/ImageLightbox.vue'
import SelectionToolbar from '@/components/ui/SelectionToolbar.vue'
import ArtifactsPanel from '@/components/artifacts/ArtifactsPanel.vue'
import LoginScreen from '@/components/ui/LoginScreen.vue'
import WelcomeScreen from '@/components/ui/WelcomeScreen.vue'
import ChatArea from '@/components/chat/ChatArea.vue'
import SettingsView from '@/components/settings/SettingsView.vue'

const sessionsStore = useSessionsStore()
const profilesStore = useProfilesStore()
const chatStore = useChatStore()
const authStore = useAuthStore()

const sidebarOpen = ref(false)
const artifactsOpen = ref(false)

function getRouteFromHash() {
  const hash = location.hash
  if (hash === '#settings') return 'settings'
  return 'chat'
}

const route = ref(getRouteFromHash())

const showWelcome = computed(() => route.value === 'chat' && !chatStore.currentId && !chatStore.loading)

const documentTitle = computed(() => {
  if (route.value === 'settings') return 'Navi — Settings'
  if (!chatStore.currentId) return 'Navi'
  const session = sessionsStore.sessions.find(s => s.session_id === chatStore.currentId)
  return session?.name || chatStore.currentId.slice(0, 8)
})

watch(documentTitle, (title) => {
  document.title = title
}, { immediate: true })

function closeSidebar() {
  sidebarOpen.value = false
}

function closeArtifacts() {
  artifactsOpen.value = false
}

function toggleSidebar() {
  const next = !sidebarOpen.value
  if (next) artifactsOpen.value = false
  sidebarOpen.value = next
}

function toggleArtifacts() {
  const next = !artifactsOpen.value
  if (next) sidebarOpen.value = false
  artifactsOpen.value = next
}

onMounted(async () => {
  // Check auth configuration first, before any protected API calls
  await authStore.fetchStatus()

  // Resolve auth state
  try {
    await authStore.fetchMe()
  } catch {
    // unauthenticated — LoginScreen will show if auth is configured
  }

  // Load app data only if authenticated or auth is not configured
  if (!authStore.authConfigured || authStore.isAuthenticated) {
    try {
      await profilesStore.fetchProfiles()
      await sessionsStore.fetchSessions(profilesStore.selectedProfileId)
    } catch {
      // ignore
    }
  }
})

// Hash-based routing for session IDs and settings
window.addEventListener('hashchange', () => {
  route.value = getRouteFromHash()
  const hash = location.hash
  if (hash === '#settings') return
  const id = hash.slice(1)
  if (id && id !== chatStore.currentId) {
    chatStore.loadSession(id)
  }
})

// Load session from initial hash on page load
if (location.hash && location.hash !== '#settings') {
  const id = location.hash.slice(1)
  if (id) chatStore.loadSession(id)
}

</script>

<style scoped>
.app-shell {
  display: flex;
  flex: 1 1 0;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
}

.sidebar-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  z-index: 99;

  @media (max-width: 1280px) {
    display: block;
  }
}

.artifacts-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  z-index: 119;

  @media (max-width: 980px) {
    display: block;
  }
}

.backdrop-fade-enter-active,
.backdrop-fade-leave-active {
  transition: opacity 0.18s ease;
}

.backdrop-fade-enter-from,
.backdrop-fade-leave-to {
  opacity: 0;
}
</style>