intent-engine 0.11.1

A command-line database service for tracking strategic intent, tasks, and events
Documentation
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useAppStore, type Task } from '../stores/appStore'
import { useI18n } from '../composables/useI18n'
import TaskTreeItem from './TaskTreeItem.vue'
import ModalDialog from './ModalDialog.vue'
import TaskForm from './TaskForm.vue'
import { Search, Plus, ChevronLeft, ChevronRight } from 'lucide-vue-next'
import { useDebounceFn } from '@vueuse/core'

const store = useAppStore()
const { t } = useI18n()

const searchQuery = ref('')
const showAddTaskModal = ref(false)

// Debounced search
const debouncedSearch = useDebounceFn((query: string) => {
  store.search(query, 1) // Reset to page 1 on new search
}, 500)

watch(searchQuery, (newVal) => {
  debouncedSearch(newVal)
})

// Pagination Logic
const currentPagination = computed(() => {
  return searchQuery.value.trim() ? store.searchPagination : store.pagination
})

function handlePageChange(newPage: number) {
  if (newPage < 1 || newPage > currentPagination.value.totalPages) return
  
  if (searchQuery.value.trim()) {
    store.search(searchQuery.value, newPage)
  } else {
    store.fetchTasks(undefined, undefined, newPage)
  }
}

// Tree Logic
const displayedTree = computed(() => {
  // If searching, build tree from search results
  if (searchQuery.value.trim()) {
    if (store.searchResults.length > 0) {
      const tasks: Task[] = []
      
      // Extract tasks from search results
      store.searchResults.forEach(result => {
        if (result.result_type === 'task') {
          tasks.push(result)
        }
        // If it's an event, we might want to show the task it belongs to?
        // For now, let's focus on tasks as per requirement.
      })

      // Build tree from these tasks
      const map = new Map<number, any>()
      const roots: any[] = []

      // 1. Create nodes
      tasks.forEach(task => {
        map.set(task.id, { ...task, children: [], isOpen: true }) // Default open for search results
      })

      // 2. Link children
      tasks.forEach(task => {
        const node = map.get(task.id)
        if (task.parent_id && map.has(task.parent_id)) {
          map.get(task.parent_id).children.push(node)
        } else {
          roots.push(node)
        }
      })

      return roots
    } else {
      return []
    }
  }
  
  // Default: use store's taskTree (which is already built from current page of tasks)
  return store.taskTree
})

async function handleCreateTask(data: { name: string, parentId: number | null, priority: number | null, spec: string }) {
  await store.addTask(data.name, data.parentId, data.priority, data.spec)
  showAddTaskModal.value = false
}
</script>

<template>
  <aside class="flex flex-col h-full bg-sci-panel border-r border-sci-border relative z-10">
    <!-- Search & Actions -->
    <div class="p-4 border-b border-sci-border space-y-3">
      <!-- Search Input -->
      <div class="relative group">
        <div class="absolute inset-0 bg-sci-cyan opacity-0 group-focus-within:opacity-10 transition-opacity rounded blur-sm"></div>
        <Search class="absolute left-3 top-2.5 w-4 h-4 text-sci-text-dim group-focus-within:text-sci-cyan transition-colors" />
        <input 
          v-model="searchQuery"
          type="text" 
          :placeholder="t('SEARCH_TASKS')" 
          class="relative z-10 w-full bg-sci-base border border-sci-border rounded pl-9 pr-3 py-2 text-sm font-mono text-sci-text-pri focus:outline-none focus:border-sci-cyan transition-all placeholder-sci-text-dim"
        >
        <div class="absolute right-2 top-2.5 text-[10px] font-mono text-sci-text-dim border border-sci-border px-1 rounded opacity-50">/</div>
      </div>

      <!-- Add Task Button -->
      <button 
        @click="showAddTaskModal = true"
        class="flex items-center justify-center w-full py-2 bg-sci-cyan-dim border border-sci-cyan text-sci-cyan rounded hover:bg-sci-cyan hover:text-sci-base transition-all text-sm font-mono gap-2 group relative overflow-hidden"
        :title="t('NEW_TASK')"
      >
        <div class="absolute inset-0 bg-sci-cyan opacity-0 group-hover:opacity-10 transition-opacity"></div>
        <Plus class="w-4 h-4" />
        <span class="tracking-wider font-bold">{{ t('NEW_TASK') }}</span>
      </button>
    </div>

    <!-- Tree -->
    <div class="flex-1 overflow-y-auto py-2 scrollbar-thin scrollbar-thumb-sci-border scrollbar-track-transparent animate-fade-in">
      <div v-if="displayedTree.length === 0" class="p-8 text-center text-sci-text-dim text-xs font-mono">
        {{ searchQuery.trim() ? 'NO_RESULTS' : 'NO_DATA_STREAM' }}
      </div>
      <TaskTreeItem 
        v-for="root in displayedTree" 
        :key="root.id" 
        :node="root" 
        :force-open="!!searchQuery.trim()"
      />
    </div>
    
    <!-- Footer / Pagination -->
    <div class="p-3 border-t border-sci-border bg-sci-base/50 backdrop-blur text-[10px] font-mono text-sci-text-dim flex flex-col gap-2">
      <!-- Stats -->
      <div class="flex items-center justify-between">
        <div class="flex items-center gap-3">
          <div class="flex items-center gap-1.5" :title="store.isConnected ? 'Connected' : 'Disconnected'">
            <div class="w-1.5 h-1.5 rounded-full" :class="store.isConnected ? 'bg-sci-success' : 'bg-sci-danger'"></div>
          </div>
          <span>{{ t('TOTAL_TASKS', { count: currentPagination.total.toString() }) }}</span>
        </div>
      </div>

      <!-- Pagination Controls -->
      <div class="flex items-center justify-between pt-2 border-t border-sci-border/50">
        <button 
          @click="handlePageChange(currentPagination.page - 1)"
          :disabled="currentPagination.page <= 1"
          class="p-1 hover:bg-sci-cyan/10 hover:text-sci-cyan rounded disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-inherit transition-colors"
        >
          <ChevronLeft class="w-3 h-3" />
        </button>
        
        <span class="text-sci-cyan">
          PAGE {{ currentPagination.page }} / {{ Math.max(1, currentPagination.totalPages) }}
        </span>

        <button 
          @click="handlePageChange(currentPagination.page + 1)"
          :disabled="currentPagination.page >= currentPagination.totalPages"
          class="p-1 hover:bg-sci-cyan/10 hover:text-sci-cyan rounded disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-inherit transition-colors"
        >
          <ChevronRight class="w-3 h-3" />
        </button>
      </div>
    </div>

    <!-- Add Task Modal -->
    <ModalDialog 
      :is-open="showAddTaskModal" 
      :title="t('NEW_TASK')"
      size="4xl"
      @close="showAddTaskModal = false"
    >
      <TaskForm 
        @submit="handleCreateTask"
        @cancel="showAddTaskModal = false"
      />
    </ModalDialog>
  </aside>
</template>