WIP - UI drawer initial
This commit is contained in:
@@ -21,7 +21,7 @@ const handleSend = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="ai-chat-area sticky bottom-0 z-20 bg-background border-t border-border p-4 bg-neutral-50">
|
<div class="ai-chat-area w-full border-t border-border p-4 bg-neutral-50">
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputGroupTextarea
|
<InputGroupTextarea
|
||||||
v-model="chatInput"
|
v-model="chatInput"
|
||||||
@@ -50,8 +50,6 @@ const handleSend = () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.ai-chat-area {
|
.ai-chat-area {
|
||||||
height: calc(100vh / 6);
|
min-height: 120px;
|
||||||
min-height: 140px;
|
|
||||||
max-height: 200px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -22,12 +22,19 @@ import { useSoftphone } from '~/composables/useSoftphone'
|
|||||||
|
|
||||||
const { logout } = useAuth()
|
const { logout } = useAuth()
|
||||||
const { api } = useApi()
|
const { api } = useApi()
|
||||||
|
const isDrawerOpen = useState<boolean>('bottomDrawerOpen', () => false)
|
||||||
|
const drawerTab = useState<string>('bottomDrawerTab', () => 'softphone')
|
||||||
const softphone = useSoftphone()
|
const softphone = useSoftphone()
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await logout()
|
await logout()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openSoftphoneDrawer = () => {
|
||||||
|
drawerTab.value = 'softphone'
|
||||||
|
isDrawerOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// Check if user is central admin (by checking if we're on a central subdomain)
|
// Check if user is central admin (by checking if we're on a central subdomain)
|
||||||
// Use ref instead of computed to avoid hydration mismatch
|
// Use ref instead of computed to avoid hydration mismatch
|
||||||
const isCentralAdmin = ref(false)
|
const isCentralAdmin = ref(false)
|
||||||
@@ -336,10 +343,13 @@ const centralAdminMenuItems: Array<{
|
|||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem v-if="!isCentralAdmin">
|
<SidebarMenuItem v-if="!isCentralAdmin">
|
||||||
<SidebarMenuButton @click="softphone.open" class="cursor-pointer hover:bg-accent">
|
<SidebarMenuButton @click="openSoftphoneDrawer" class="cursor-pointer hover:bg-accent">
|
||||||
<Phone class="h-4 w-4" />
|
<Phone class="h-4 w-4" />
|
||||||
<span>Softphone</span>
|
<span>Softphone</span>
|
||||||
<span v-if="softphone.hasIncomingCall.value" class="ml-auto h-2 w-2 rounded-full bg-red-500 animate-pulse"></span>
|
<span
|
||||||
|
v-if="softphone.hasIncomingCall.value"
|
||||||
|
class="ml-auto h-2 w-2 rounded-full bg-red-500 animate-pulse"
|
||||||
|
/>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
|
|||||||
153
frontend/components/BottomDrawer.vue
Normal file
153
frontend/components/BottomDrawer.vue
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import AIChatBar from '@/components/AIChatBar.vue'
|
||||||
|
import { Phone, Sparkles, X } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const isDrawerOpen = useState<boolean>('bottomDrawerOpen', () => false)
|
||||||
|
const activeTab = useState<string>('bottomDrawerTab', () => 'softphone')
|
||||||
|
|
||||||
|
const drawerHeight = ref(240)
|
||||||
|
const minHeight = 160
|
||||||
|
const maxHeight = ref(480)
|
||||||
|
const isResizing = ref(false)
|
||||||
|
const resizeStartY = ref(0)
|
||||||
|
const resizeStartHeight = ref(0)
|
||||||
|
|
||||||
|
const softphoneEnabled = ref(false)
|
||||||
|
const softphoneConnected = ref(false)
|
||||||
|
|
||||||
|
const statusLabel = computed(() => (softphoneConnected.value ? 'Connected' : 'Offline'))
|
||||||
|
|
||||||
|
const clampHeight = (height: number) => Math.min(Math.max(height, minHeight), maxHeight.value)
|
||||||
|
|
||||||
|
const updateMaxHeight = () => {
|
||||||
|
if (!process.client) return
|
||||||
|
maxHeight.value = Math.round(window.innerHeight * 0.6)
|
||||||
|
drawerHeight.value = clampHeight(drawerHeight.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const startResize = (event: MouseEvent | TouchEvent) => {
|
||||||
|
isResizing.value = true
|
||||||
|
resizeStartY.value = 'touches' in event ? event.touches[0].clientY : event.clientY
|
||||||
|
resizeStartHeight.value = drawerHeight.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = (event: MouseEvent | TouchEvent) => {
|
||||||
|
if (!isResizing.value) return
|
||||||
|
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY
|
||||||
|
const delta = resizeStartY.value - clientY
|
||||||
|
drawerHeight.value = clampHeight(resizeStartHeight.value + delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopResize = () => {
|
||||||
|
isResizing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDrawer = () => {
|
||||||
|
isDrawerOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(softphoneEnabled, (enabled) => {
|
||||||
|
if (!enabled) {
|
||||||
|
softphoneConnected.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
softphoneConnected.value = true
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateMaxHeight()
|
||||||
|
window.addEventListener('mousemove', handleResize)
|
||||||
|
window.addEventListener('mouseup', stopResize)
|
||||||
|
window.addEventListener('touchmove', handleResize, { passive: true })
|
||||||
|
window.addEventListener('touchend', stopResize)
|
||||||
|
window.addEventListener('resize', updateMaxHeight)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('mousemove', handleResize)
|
||||||
|
window.removeEventListener('mouseup', stopResize)
|
||||||
|
window.removeEventListener('touchmove', handleResize)
|
||||||
|
window.removeEventListener('touchend', stopResize)
|
||||||
|
window.removeEventListener('resize', updateMaxHeight)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="pointer-events-none absolute inset-x-0 bottom-0 z-30 flex justify-center px-4 pb-4">
|
||||||
|
<div
|
||||||
|
class="pointer-events-auto w-full max-w-5xl rounded-xl border border-border bg-background shadow-xl"
|
||||||
|
:class="isDrawerOpen ? 'opacity-100' : 'opacity-0 translate-y-6 pointer-events-none'"
|
||||||
|
:style="{ height: `${drawerHeight}px` }"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between border-b border-border px-4 py-2">
|
||||||
|
<div
|
||||||
|
class="flex h-6 flex-1 cursor-row-resize items-center justify-center"
|
||||||
|
@mousedown="startResize"
|
||||||
|
@touchstart.prevent="startResize"
|
||||||
|
>
|
||||||
|
<span class="h-1.5 w-12 rounded-full bg-muted" />
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="icon" class="ml-3" @click="closeDrawer">
|
||||||
|
<X class="h-4 w-4" />
|
||||||
|
<span class="sr-only">Close drawer</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs v-model="activeTab" class="flex h-[calc(100%-48px)] flex-col">
|
||||||
|
<TabsList class="mx-4 mt-3 grid w-fit grid-cols-2">
|
||||||
|
<TabsTrigger value="softphone" class="flex items-center gap-2">
|
||||||
|
<Phone class="h-4 w-4" />
|
||||||
|
Softphone
|
||||||
|
<span
|
||||||
|
class="inline-flex h-2 w-2 rounded-full"
|
||||||
|
:class="softphoneConnected ? 'bg-emerald-500' : 'bg-muted-foreground/40'"
|
||||||
|
/>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="ai" class="flex items-center gap-2">
|
||||||
|
<Sparkles class="h-4 w-4" />
|
||||||
|
AI Agent
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-hidden">
|
||||||
|
<TabsContent value="softphone" class="h-full">
|
||||||
|
<div class="flex h-full flex-col gap-4 px-6 pb-6 pt-4">
|
||||||
|
<div class="flex items-center justify-between rounded-lg border border-border bg-muted/30 px-4 py-3">
|
||||||
|
<div>
|
||||||
|
<Label class="text-sm font-medium">Softphone availability</Label>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Turn on to initialize the softphone connection.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch v-model:checked="softphoneEnabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3 text-sm text-muted-foreground">
|
||||||
|
<span
|
||||||
|
class="inline-flex h-2.5 w-2.5 rounded-full"
|
||||||
|
:class="softphoneConnected ? 'bg-emerald-500' : 'bg-muted-foreground/40'"
|
||||||
|
/>
|
||||||
|
<span>{{ statusLabel }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 rounded-lg border border-dashed border-border bg-muted/20 p-4 text-sm text-muted-foreground">
|
||||||
|
Softphone controls will appear here.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="ai" class="h-full">
|
||||||
|
<div class="flex h-full flex-col justify-end">
|
||||||
|
<AIChatBar />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import AppSidebar from '@/components/AppSidebar.vue'
|
import AppSidebar from '@/components/AppSidebar.vue'
|
||||||
import AIChatBar from '@/components/AIChatBar.vue'
|
import BottomDrawer from '@/components/BottomDrawer.vue'
|
||||||
import SoftphoneDialog from '@/components/SoftphoneDialog.vue'
|
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
@@ -74,11 +73,7 @@ const breadcrumbs = computed(() => {
|
|||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- AI Chat Bar Component -->
|
<BottomDrawer />
|
||||||
<AIChatBar />
|
|
||||||
|
|
||||||
<!-- Softphone Dialog (Global) -->
|
|
||||||
<SoftphoneDialog />
|
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user