292 lines
7.2 KiB
Vue
292 lines
7.2 KiB
Vue
<script setup lang="ts">
|
|
import { ref, nextTick, watch, onUnmounted } from 'vue'
|
|
import { Bot, Send, Maximize2 } from '@lucide/vue'
|
|
import type { ChatMessage } from '../../composables/useDashboardData'
|
|
import { useDashboardData } from '../../composables/useDashboardData'
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import Textarea from '@/components/ui/Textarea.vue'
|
|
import Dialog from '@/components/ui/Dialog.vue'
|
|
import DialogHeader from '@/components/ui/DialogHeader.vue'
|
|
import DialogTitle from '@/components/ui/DialogTitle.vue'
|
|
import ChatMessageList from './ChatMessageList.vue'
|
|
|
|
const props = defineProps<{
|
|
messages: ChatMessage[]
|
|
irisBusy: boolean
|
|
irisFocus: string
|
|
}>()
|
|
|
|
const { sendChatMessage, busySince } = useDashboardData()
|
|
|
|
const elapsedSeconds = ref(0)
|
|
let elapsedInterval: ReturnType<typeof setInterval> | null = null
|
|
|
|
function startElapsedTimer(): void {
|
|
stopElapsedTimer()
|
|
const update = () => {
|
|
if (busySince.value > 0) {
|
|
elapsedSeconds.value = Math.floor((Date.now() - busySince.value) / 1000)
|
|
}
|
|
}
|
|
update()
|
|
elapsedInterval = setInterval(update, 1000)
|
|
}
|
|
|
|
function stopElapsedTimer(): void {
|
|
if (elapsedInterval) {
|
|
clearInterval(elapsedInterval)
|
|
elapsedInterval = null
|
|
}
|
|
}
|
|
|
|
watch(() => props.irisBusy, (busy) => {
|
|
if (busy) {
|
|
startElapsedTimer()
|
|
} else {
|
|
stopElapsedTimer()
|
|
elapsedSeconds.value = 0
|
|
}
|
|
}, { immediate: true })
|
|
|
|
onUnmounted(() => {
|
|
stopElapsedTimer()
|
|
})
|
|
|
|
const inputText = ref('')
|
|
const chatListRef = ref<HTMLElement | null>(null)
|
|
const chatModalListRef = ref<HTMLElement | null>(null)
|
|
const dialogOpen = ref(false)
|
|
|
|
function sendMessage(): void {
|
|
if (!inputText.value.trim()) return
|
|
sendChatMessage(inputText.value)
|
|
inputText.value = ''
|
|
}
|
|
|
|
watch(
|
|
() => props.messages.length,
|
|
async () => {
|
|
await nextTick()
|
|
const el = dialogOpen.value ? chatModalListRef.value : chatListRef.value
|
|
if (el) {
|
|
el.scrollTop = el.scrollHeight
|
|
}
|
|
}
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<!-- Inline Chat Panel -->
|
|
<div class="chat-panel">
|
|
<div class="chat-header">
|
|
<div class="chat-header-left">
|
|
<Bot :size="16" class="text-[#a78bfa]" />
|
|
<h2>Iris Chat</h2>
|
|
</div>
|
|
<Button variant="ghost" size="icon" class="h-7 w-7" @click="dialogOpen = true" title="Open larger chat">
|
|
<Maximize2 :size="14" />
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Focus Bar -->
|
|
<div v-if="irisBusy && irisFocus" class="focus-bar">
|
|
<span class="focus-label">Current Focus</span>
|
|
<span class="focus-text">{{ irisFocus }}</span>
|
|
</div>
|
|
|
|
<!-- Messages -->
|
|
<div ref="chatListRef" class="chat-messages">
|
|
<ChatMessageList
|
|
:messages="messages"
|
|
:iris-busy="irisBusy"
|
|
:elapsed-seconds="elapsedSeconds"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Input -->
|
|
<div class="chat-input-row">
|
|
<Textarea
|
|
v-model="inputText"
|
|
rows="1"
|
|
placeholder="Type a message..."
|
|
class="min-h-0 h-9 resize-none text-xs bg-black/30 border-[rgba(255,255,255,0.08)] text-[#e8eaf0] placeholder:text-[#6b7385] text-[10px]"
|
|
@keyup.enter.exact="sendMessage"
|
|
/>
|
|
<Button
|
|
size="icon"
|
|
class="h-8 w-8 bg-[#a78bfa] hover:bg-[#c4b5fd] flex-shrink-0"
|
|
:disabled="!inputText.trim()"
|
|
@click="sendMessage"
|
|
aria-label="Send"
|
|
>
|
|
<Send :size="14" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Expanded Chat Dialog -->
|
|
<Dialog :open="dialogOpen" class="sm:max-w-[820px] sm:h-[78vh] p-0 gap-0" @update:open="dialogOpen = $event">
|
|
<template #default>
|
|
<DialogHeader class="flex-row items-center justify-between px-5 py-4 border-b border-[rgba(255,255,255,0.06)]">
|
|
<div class="flex items-center gap-2">
|
|
<Bot :size="18" class="text-[#a78bfa]" />
|
|
<DialogTitle>Iris Chat</DialogTitle>
|
|
</div>
|
|
<Button variant="ghost" size="icon" class="h-7 w-7" @click="dialogOpen = false" aria-label="Close">
|
|
<span class="text-lg leading-none">×</span>
|
|
</Button>
|
|
</DialogHeader>
|
|
|
|
<div v-if="irisBusy && irisFocus" class="focus-bar !px-5">
|
|
<span class="focus-label">Current Focus</span>
|
|
<span class="focus-text">{{ irisFocus }}</span>
|
|
</div>
|
|
|
|
<div ref="chatModalListRef" class="flex-1 overflow-y-auto px-5 py-4">
|
|
<ChatMessageList
|
|
:messages="messages"
|
|
:iris-busy="irisBusy"
|
|
:elapsed-seconds="elapsedSeconds"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex gap-2 px-4 py-3 border-t border-[rgba(255,255,255,0.06)]">
|
|
<Textarea
|
|
v-model="inputText"
|
|
rows="1"
|
|
placeholder="Type a message..."
|
|
class="min-h-0 h-10 resize-none text-sm bg-black/30 border-[rgba(255,255,255,0.08)] text-[#e8eaf0] placeholder:text-[#6b7385]"
|
|
@keyup.enter.exact="sendMessage"
|
|
/>
|
|
<Button
|
|
size="icon"
|
|
class="h-10 w-10 bg-[#a78bfa] hover:bg-[#c4b5fd] flex-shrink-0"
|
|
:disabled="!inputText.trim()"
|
|
@click="sendMessage"
|
|
aria-label="Send"
|
|
>
|
|
<Send :size="18" />
|
|
</Button>
|
|
</div>
|
|
</template>
|
|
</Dialog>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.chat-panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
min-height: 360px;
|
|
max-height: 480px;
|
|
background: rgba(22, 27, 34, 0.75);
|
|
border: 1px solid rgba(139, 124, 246, 0.12);
|
|
border-radius: 16px;
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
|
|
transition: border-color 0.2s ease;
|
|
overflow: hidden;
|
|
}
|
|
.chat-panel:hover {
|
|
border-color: rgba(139, 124, 246, 0.18);
|
|
}
|
|
|
|
.chat-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 14px 16px;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
}
|
|
.chat-header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.chat-header h2 {
|
|
margin: 0;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #e8eaf0;
|
|
}
|
|
|
|
/* Focus Bar */
|
|
.focus-bar {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
padding: 8px 16px;
|
|
background: rgba(234, 179, 8, 0.04);
|
|
border-bottom: 1px solid rgba(234, 179, 8, 0.08);
|
|
}
|
|
.focus-label {
|
|
font-size: 8px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: #eab308;
|
|
}
|
|
.focus-text {
|
|
font-size: 10px;
|
|
color: #7e8799;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
/* Messages */
|
|
.chat-messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 12px 16px;
|
|
}
|
|
.chat-messages::-webkit-scrollbar {
|
|
width: 5px;
|
|
}
|
|
.chat-messages::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
.chat-messages::-webkit-scrollbar-thumb {
|
|
background: rgba(139, 124, 246, 0.2);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
/* Input */
|
|
.chat-input-row {
|
|
display: flex;
|
|
gap: 6px;
|
|
padding: 10px 12px;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
|
}
|
|
|
|
/* ── Mobile: compact mode ── */
|
|
@media (max-width: 768px) {
|
|
.chat-panel {
|
|
min-height: 280px;
|
|
max-height: 360px;
|
|
border-radius: 12px;
|
|
}
|
|
.chat-header {
|
|
padding: 10px 12px;
|
|
}
|
|
.chat-header h2 {
|
|
font-size: 11px;
|
|
}
|
|
.chat-messages {
|
|
padding: 8px 12px;
|
|
}
|
|
.chat-input-row {
|
|
padding: 8px 10px;
|
|
gap: 4px;
|
|
}
|
|
.focus-bar {
|
|
padding: 6px 12px;
|
|
}
|
|
.focus-label {
|
|
font-size: 7px;
|
|
}
|
|
.focus-text {
|
|
font-size: 9px;
|
|
}
|
|
}
|
|
</style>
|