useSystemController.ts 5.05 KB
import { computed, ref, shallowRef } from 'vue'

import type { AppName, AppStatusInfo, ForumMessage, SystemStatus } from '@/types'
import { appLabel, buildEngineUrl } from '@/utils/format'
import { fetchJson, postJson } from '@/utils/http'
import { bindPersistentState, loadStoredValue } from './usePersistentState'

interface StatusResponse {
  insight: AppStatusInfo
  media: AppStatusInfo
  query: AppStatusInfo
  forum: AppStatusInfo
}

interface SystemPayload extends SystemStatus {
  success: boolean
}

interface SearchResponse {
  success: boolean
  query: string
  research_task_id: string
}

interface OutputPayload {
  success: boolean
  output?: string[]
}

interface ForumLogPayload {
  success: boolean
  parsed_messages: ForumMessage[]
}

interface ReportLogPayload {
  success: boolean
  log_lines: string[]
}

const STORAGE_KEY = 'bettafish.workspaceState.v2'

export function useSystemController() {
  const stored = loadStoredValue(STORAGE_KEY, {
    activeApp: 'insight',
  })

  const activeApp = shallowRef<AppName>((stored.activeApp as AppName) || 'insight')
  const systemStatus = ref<SystemStatus>({
    started: false,
    starting: false,
  })
  const appStatus = ref<Record<AppName, AppStatusInfo>>({
    insight: { status: 'stopped', port: 8501 },
    media: { status: 'stopped', port: 8502 },
    query: { status: 'stopped', port: 8503 },
    forum: { status: 'running', port: null },
    report: { status: 'running', port: null },
  })
  const consoleLines = ref<string[]>([])
  const forumMessages = ref<ForumMessage[]>([])
  const loading = shallowRef(false)
  const actionPending = shallowRef(false)

  let pollingTimer: number | null = null

  const appCards = computed(() => (
    (['insight', 'media', 'query', 'forum'] as AppName[]).map((name) => ({
      name,
      label: appLabel(name),
      status: appStatus.value[name]?.status || 'stopped',
      url: buildEngineUrl(appStatus.value[name]?.port),
    }))
  ))

  const activeEngineUrl = computed(() => buildEngineUrl(appStatus.value[activeApp.value]?.port))

  async function refreshSystemStatus() {
    const payload = await fetchJson<SystemPayload>('/api/system/status')
    systemStatus.value = {
      started: payload.started,
      starting: payload.starting,
    }
  }

  async function refreshAppStatus() {
    const payload = await fetchJson<StatusResponse>('/api/status')
    appStatus.value = {
      ...appStatus.value,
      insight: payload.insight,
      media: payload.media,
      query: payload.query,
      forum: payload.forum,
    }
  }

  async function refreshConsole() {
    if (activeApp.value === 'forum') {
      const payload = await fetchJson<ForumLogPayload>('/api/forum/log')
      forumMessages.value = payload.parsed_messages
      consoleLines.value = payload.parsed_messages.map(
        (item) => `[${item.timestamp}] [${item.source}] ${item.content}`,
      )
      return
    }

    if (activeApp.value === 'report') {
      const payload = await fetchJson<ReportLogPayload>('/api/report/log')
      consoleLines.value = payload.log_lines
      return
    }

    const payload = await fetchJson<OutputPayload>(`/api/output/${activeApp.value}`)
    consoleLines.value = payload.output || []
  }

  async function initialize() {
    loading.value = true
    try {
      await Promise.all([
        refreshSystemStatus(),
        refreshAppStatus(),
        refreshConsole(),
      ])
    } finally {
      loading.value = false
    }
  }

  function startPolling() {
    if (pollingTimer !== null) {
      return
    }

    pollingTimer = window.setInterval(async () => {
      await Promise.all([
        refreshSystemStatus(),
        refreshAppStatus(),
        refreshConsole(),
      ])
    }, 3000)
  }

  function stopPolling() {
    if (pollingTimer !== null) {
      window.clearInterval(pollingTimer)
      pollingTimer = null
    }
  }

  async function startSystem() {
    actionPending.value = true
    try {
      await postJson('/api/system/start')
      await initialize()
    } finally {
      actionPending.value = false
    }
  }

  async function shutdownSystem() {
    actionPending.value = true
    try {
      await postJson('/api/system/shutdown')
    } finally {
      actionPending.value = false
    }
  }

  async function search(query: string, researchTaskId = '') {
    actionPending.value = true
    try {
      const payload = await postJson<SearchResponse>('/api/search', {
        query,
        research_task_id: researchTaskId,
      })
      await refreshConsole()
      return payload
    } finally {
      actionPending.value = false
    }
  }

  function selectApp(app: AppName) {
    activeApp.value = app
    void refreshConsole()
  }

  bindPersistentState(
    STORAGE_KEY,
    computed(() => ({
      activeApp: activeApp.value,
    })),
  )

  return {
    activeApp,
    systemStatus,
    appStatus,
    appCards,
    activeEngineUrl,
    consoleLines,
    forumMessages,
    loading,
    actionPending,
    initialize,
    startPolling,
    stopPolling,
    startSystem,
    shutdownSystem,
    search,
    selectApp,
    refreshSystemStatus,
    refreshAppStatus,
    refreshConsole,
  }
}