Newer
Older
navi-1 / webclient / src / components / ui / SelectionToolbar.vue
<template>
  <Teleport to="body">
    <div
      v-if="visible"
      class="selection-toolbar"
      :style="{ top: pos.y + 'px', left: pos.x + 'px' }"
      @mousedown.prevent
    >
      <GnButton variant="warning" size="sm" icon="ph ph-quotes" style="color:#000" title="Reply to selection" @click="handleReply">
        Reply
      </GnButton>
    </div>
  </Teleport>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useQuoteReply } from '@/composables/useQuoteReply.js'

const { requestQuote } = useQuoteReply()

const visible = ref(false)
const pos = ref({ x: 0, y: 0 })
let selectedText = ''

function getAssistantSelection() {
  const sel = window.getSelection()
  if (!sel || sel.isCollapsed || !sel.toString().trim()) return null
  const range = sel.getRangeAt(0)
  const container = range.commonAncestorContainer
  const el = container.nodeType === Node.TEXT_NODE ? container.parentElement : container
  if (!el.closest('.msg-assistant-content')) return null
  return { text: sel.toString().trim(), range }
}

function updatePos() {
  const sel = window.getSelection()
  if (!sel || sel.isCollapsed || !sel.rangeCount) { visible.value = false; return }
  const range = sel.getRangeAt(0)
  const rect = range.getBoundingClientRect()
  if (!rect.width && !rect.height) { visible.value = false; return }
  // viewport-relative coords — correct for position:fixed
  pos.value = {
    x: Math.round(rect.left + rect.width / 2),
    y: Math.round(rect.top - 44),
  }
}

function onMouseUp() {
  setTimeout(() => {
    const result = getAssistantSelection()
    if (!result) { visible.value = false; return }
    selectedText = result.text
    updatePos()
    visible.value = true
  }, 10)
}

function onSelectionChange() {
  const sel = window.getSelection()
  if (!sel || sel.isCollapsed) visible.value = false
}

function handleReply() {
  if (!selectedText) return
  requestQuote(selectedText)
  window.getSelection()?.removeAllRanges()
  visible.value = false
}

onMounted(() => {
  document.addEventListener('mouseup', onMouseUp)
  document.addEventListener('selectionchange', onSelectionChange)
  // capture:true catches scroll inside nested containers (message list)
  document.addEventListener('scroll', updatePos, true)
})

onUnmounted(() => {
  document.removeEventListener('mouseup', onMouseUp)
  document.removeEventListener('selectionchange', onSelectionChange)
  document.removeEventListener('scroll', updatePos, true)
})
</script>

<style scoped>
.selection-toolbar {
  position: fixed;
  transform: translateX(-50%);
  z-index: 900;
  pointer-events: all;
  background: #e0af68;
  color: black;
}
</style>