useResearchWorkspace.ts 7.5 KB
import { computed, reactive, ref, shallowRef, watch } from 'vue'

import type {
  ResearchOptions,
  ResearchTask,
  ResearchTaskDraft,
  TaskCrawlerResource,
  TaskReportResource,
  TaskViewResource,
} from '@/types'
import { fetchJson, postJson } from '@/utils/http'
import { bindPersistentState, loadStoredValue } from './usePersistentState'

interface ResearchOptionsResponse extends ResearchOptions {
  success: boolean
}

interface ResearchTasksResponse {
  success: boolean
  active_task: ResearchTask | null
  tasks: ResearchTask[]
}

interface TaskCrawlerResourceResponse {
  success: boolean
  task_id: string
  crawler: TaskCrawlerResource
}

interface TaskReportResourceResponse {
  success: boolean
  task_id: string
  report: TaskReportResource
}

interface TaskViewResourcesResponse {
  success: boolean
  active_task_id: string | null
  task_views: TaskViewResource[]
}

const STORAGE_KEY = 'bettafish.frontendState.v2'
const RESEARCH_TASK_RESOURCE_BASE = '/api/research-tasks'

function createDefaultDraft(): ResearchTaskDraft {
  return {
    task_id: '',
    venue_name: '',
    city: '',
    venue_type: 'museum',
    research_focus: 'strengths_improvements',
    time_range: 'recent_90_days',
    benchmark_venues_text: '',
    notes: '',
  }
}

export function useResearchWorkspace() {
  const stored = loadStoredValue(STORAGE_KEY, {
    draft: createDefaultDraft(),
    searchQuery: '',
    selectedResearchTaskId: '',
  })

  const loading = shallowRef(false)
  const taskViewResourcesLoading = shallowRef(false)
  const saving = shallowRef(false)
  const options = ref<ResearchOptions>({
    venue_types: [],
    research_focuses: [],
    time_ranges: [],
    status_labels: {},
  })
  const activeTask = ref<ResearchTask | null>(null)
  const activeTaskCrawlerResource = ref<TaskCrawlerResource | null>(null)
  const activeTaskReportResource = ref<TaskReportResource | null>(null)
  const taskViewResources = ref<TaskViewResource[]>([])
  const tasks = ref<ResearchTask[]>([])
  const searchQuery = shallowRef(String(stored.searchQuery || ''))
  const selectedResearchTaskId = shallowRef(String(stored.selectedResearchTaskId || ''))
  const draft = reactive<ResearchTaskDraft>({
    ...createDefaultDraft(),
    ...stored.draft,
  })

  const summaryItems = computed(() => {
    const source = activeTask.value
    if (!source) {
      return [
        {
          label: '研究指令',
          value: '系统会基于场馆名称、研究重点与时间范围自动生成可直接驱动三引擎的研究语句。',
        },
      ]
    }

    return [
      {
        label: '研究指令',
        value: source.generated_query,
      },
      {
        label: '竞品 / 标杆',
        value: source.benchmark_venues_text || '未设置',
      },
      {
        label: '最近动作',
        value: source.last_action || '暂无',
      },
    ]
  })

  async function loadOptions() {
    const payload = await fetchJson<ResearchOptionsResponse>('/api/research/options')
    options.value = {
      venue_types: payload.venue_types,
      research_focuses: payload.research_focuses,
      time_ranges: payload.time_ranges,
      status_labels: payload.status_labels,
    }
  }

  function hydrateDraftFromTask(task: ResearchTask) {
    draft.task_id = task.task_id
    draft.venue_name = task.venue_name
    draft.city = task.city
    draft.venue_type = task.venue_type
    draft.research_focus = task.research_focus
    draft.time_range = task.time_range
    draft.benchmark_venues_text = task.benchmark_venues_text
    draft.notes = task.notes
    searchQuery.value = task.generated_query
    selectedResearchTaskId.value = task.task_id
  }

  async function refreshTasks(preferredTaskId = '') {
    loading.value = true
    try {
      const payload = await fetchJson<ResearchTasksResponse>(RESEARCH_TASK_RESOURCE_BASE)
      activeTask.value = payload.active_task
      tasks.value = payload.tasks

      const targetTask = payload.tasks.find((item) => item.task_id === preferredTaskId)
        || payload.tasks.find((item) => item.task_id === selectedResearchTaskId.value)
        || payload.active_task

      if (targetTask) {
        hydrateDraftFromTask(targetTask)
      } else if (payload.active_task) {
        hydrateDraftFromTask(payload.active_task)
      }
    } finally {
      loading.value = false
    }
  }

  async function saveTask() {
    saving.value = true
    try {
      const payload = await postJson<ResearchTasksResponse & { task: ResearchTask }>(RESEARCH_TASK_RESOURCE_BASE, {
        task_id: draft.task_id,
        venue_name: draft.venue_name,
        city: draft.city,
        venue_type: draft.venue_type,
        research_focus: draft.research_focus,
        time_range: draft.time_range,
        benchmark_venues: draft.benchmark_venues_text,
        notes: draft.notes,
      })

      activeTask.value = payload.task
      tasks.value = payload.tasks
      hydrateDraftFromTask(payload.task)
      return payload.task
    } finally {
      saving.value = false
    }
  }

  async function activateTask(taskId: string) {
    const payload = await postJson<ResearchTasksResponse>(`${RESEARCH_TASK_RESOURCE_BASE}/${taskId}/activate`)
    activeTask.value = payload.active_task
    tasks.value = payload.tasks
    const selected = payload.tasks.find((item) => item.task_id === taskId) || payload.active_task
    if (selected) {
      hydrateDraftFromTask(selected)
    }
  }

  async function refreshActiveTaskCrawlerResource(taskId = activeTask.value?.task_id || '') {
    const normalizedTaskId = String(taskId || '').trim()
    if (!normalizedTaskId) {
      activeTaskCrawlerResource.value = null
      return null
    }

    const payload = await fetchJson<TaskCrawlerResourceResponse>(
      `${RESEARCH_TASK_RESOURCE_BASE}/${normalizedTaskId}/crawler`,
    )
    activeTaskCrawlerResource.value = payload.crawler
    return payload.crawler
  }

  async function refreshActiveTaskReportResource(taskId = activeTask.value?.task_id || '') {
    const normalizedTaskId = String(taskId || '').trim()
    if (!normalizedTaskId) {
      activeTaskReportResource.value = null
      return null
    }

    const payload = await fetchJson<TaskReportResourceResponse>(
      `${RESEARCH_TASK_RESOURCE_BASE}/${normalizedTaskId}/report`,
    )
    activeTaskReportResource.value = payload.report
    return payload.report
  }

  async function refreshTaskViewResources() {
    taskViewResourcesLoading.value = true
    try {
      const payload = await fetchJson<TaskViewResourcesResponse>(`${RESEARCH_TASK_RESOURCE_BASE}/task-views`)
      taskViewResources.value = payload.task_views
      return payload.task_views
    } finally {
      taskViewResourcesLoading.value = false
    }
  }

  watch(
    draft,
    () => {
      if (!activeTask.value) {
        searchQuery.value = draft.venue_name
          ? `请围绕${draft.city ? `${draft.city}${draft.venue_name}` : draft.venue_name}开展场馆运营反馈调研`
          : ''
      }
    },
    { deep: true },
  )

  const persistedState = computed(() => ({
    draft: { ...draft },
    searchQuery: searchQuery.value,
    selectedResearchTaskId: selectedResearchTaskId.value,
  }))
  bindPersistentState(STORAGE_KEY, persistedState)

  return {
    loading,
    taskViewResourcesLoading,
    saving,
    options,
    draft,
    activeTask,
    activeTaskCrawlerResource,
    activeTaskReportResource,
    taskViewResources,
    tasks,
    searchQuery,
    summaryItems,
    loadOptions,
    refreshTasks,
    saveTask,
    activateTask,
    refreshActiveTaskCrawlerResource,
    refreshActiveTaskReportResource,
    refreshTaskViewResources,
  }
}