WIP - UI drawer initial
This commit is contained in:
@@ -21,7 +21,7 @@ const handleSend = () => {
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<InputGroupTextarea
|
||||
v-model="chatInput"
|
||||
@@ -50,8 +50,6 @@ const handleSend = () => {
|
||||
|
||||
<style scoped>
|
||||
.ai-chat-area {
|
||||
height: calc(100vh / 6);
|
||||
min-height: 140px;
|
||||
max-height: 200px;
|
||||
min-height: 120px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,12 +22,19 @@ import { useSoftphone } from '~/composables/useSoftphone'
|
||||
|
||||
const { logout } = useAuth()
|
||||
const { api } = useApi()
|
||||
const isDrawerOpen = useState<boolean>('bottomDrawerOpen', () => false)
|
||||
const drawerTab = useState<string>('bottomDrawerTab', () => 'softphone')
|
||||
const softphone = useSoftphone()
|
||||
|
||||
const handleLogout = async () => {
|
||||
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)
|
||||
// Use ref instead of computed to avoid hydration mismatch
|
||||
const isCentralAdmin = ref(false)
|
||||
@@ -336,10 +343,13 @@ const centralAdminMenuItems: Array<{
|
||||
<SidebarFooter>
|
||||
<SidebarMenu>
|
||||
<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" />
|
||||
<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>
|
||||
</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">
|
||||
import { ref } from 'vue'
|
||||
import AppSidebar from '@/components/AppSidebar.vue'
|
||||
import AIChatBar from '@/components/AIChatBar.vue'
|
||||
import SoftphoneDialog from '@/components/SoftphoneDialog.vue'
|
||||
import BottomDrawer from '@/components/BottomDrawer.vue'
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
@@ -74,11 +73,7 @@ const breadcrumbs = computed(() => {
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- AI Chat Bar Component -->
|
||||
<AIChatBar />
|
||||
|
||||
<!-- Softphone Dialog (Global) -->
|
||||
<SoftphoneDialog />
|
||||
<BottomDrawer />
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user