feat: handle loading states

This commit is contained in:
AnotiaWang
2025-02-11 21:37:11 +08:00
parent 8c81b9a425
commit 501e25d835
5 changed files with 87 additions and 41 deletions

View File

@ -13,6 +13,7 @@
}) })
const selectedNode = ref<TreeNode>() const selectedNode = ref<TreeNode>()
const searchResults = ref<Record<string, PartialSearchResult>>({}) const searchResults = ref<Record<string, PartialSearchResult>>({})
const isLoading = ref(false)
function handleResearchProgress(step: ResearchStep) { function handleResearchProgress(step: ResearchStep) {
let node: TreeNode | null = null let node: TreeNode | null = null
@ -94,6 +95,7 @@
case 'complete': case 'complete':
emit('complete', step) emit('complete', step)
isLoading.value = false
break break
} }
} }
@ -132,6 +134,7 @@
tree.value.children = [] tree.value.children = []
selectedNode.value = undefined selectedNode.value = undefined
searchResults.value = {} searchResults.value = {}
isLoading.value = true
try { try {
await deepResearch({ await deepResearch({
query, query,
@ -141,11 +144,14 @@
}) })
} catch (error) { } catch (error) {
console.error('Research failed:', error) console.error('Research failed:', error)
} finally {
isLoading.value = false
} }
} }
defineExpose({ defineExpose({
startResearch, startResearch,
isLoading,
}) })
</script> </script>

View File

@ -8,6 +8,10 @@
userAnswer: string userAnswer: string
} }
const props = defineProps<{
isLoadingSearch?: boolean
}>()
defineEmits<{ defineEmits<{
(e: 'submit', feedback: ResearchFeedbackResult[]): void (e: 'submit', feedback: ResearchFeedbackResult[]): void
}>() }>()
@ -24,7 +28,8 @@
// All questions should be answered // All questions should be answered
feedback.value.some((v) => !v.assistantQuestion || !v.userAnswer) || feedback.value.some((v) => !v.assistantQuestion || !v.userAnswer) ||
// Should not be loading // Should not be loading
isLoading.value, isLoading.value ||
props.isLoadingSearch,
) )
async function getFeedback(query: string, numQuestions = 3) { async function getFeedback(query: string, numQuestions = 3) {
@ -94,6 +99,7 @@
defineExpose({ defineExpose({
getFeedback, getFeedback,
clear, clear,
isLoading,
}) })
</script> </script>
@ -113,7 +119,13 @@
<UInput v-model="feedback.userAnswer" /> <UInput v-model="feedback.userAnswer" />
</div> </div>
</template> </template>
<UButton color="primary" :loading="isLoading" :disabled="isSubmitButtonDisabled" block @click="$emit('submit', feedback)"> <UButton
color="primary"
:loading="isLoadingSearch || isLoading"
:disabled="isSubmitButtonDisabled"
block
@click="$emit('submit', feedback)"
>
Submit Answer Submit Answer
</UButton> </UButton>
</div> </div>

View File

@ -6,27 +6,31 @@
numQuestions: number numQuestions: number
} }
defineProps<{
isLoadingFeedback: boolean
}>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'submit', value: ResearchInputData): void (e: 'submit', value: ResearchInputData): void
}>() }>()
const input = ref('天空为什么是蓝的?') const form = reactive({
const breadth = ref(2) query: '',
const depth = ref(2) breadth: 2,
const numQuestions = ref(3) depth: 2,
const isLoading = ref(false) numQuestions: 3,
})
const isSubmitButtonDisabled = computed(() => !form.query || !form.breadth || !form.depth || !form.numQuestions)
function handleSubmit() { function handleSubmit() {
emit('submit', { emit('submit', {
query: input.value, ...form,
breadth: breadth.value,
depth: depth.value,
numQuestions: numQuestions.value,
}) })
} }
onMounted(() => { defineExpose({
input.value = '天空为什么是蓝的?' // default form,
}) })
</script> </script>
@ -37,30 +41,30 @@
</template> </template>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<UFormField label="Research Topic" required> <UFormField label="Research Topic" required>
<UTextarea class="w-full" v-model="input" :rows="3" placeholder="Enter whatever you want to research..." required /> <UTextarea class="w-full" v-model="form.query" :rows="3" placeholder="Enter whatever you want to research..." required />
</UFormField> </UFormField>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<UFormField label="Number of Questions"> <UFormField label="Number of Questions" required>
<template #help> Number of questions for you to clarify. </template> <template #help> Number of questions for you to clarify. </template>
<UInput v-model="numQuestions" class="w-full" type="number" :min="1" :max="5" :step="1" /> <UInput v-model="form.numQuestions" class="w-full" type="number" :min="1" :max="5" :step="1" />
</UFormField> </UFormField>
<UFormField label="Depth"> <UFormField label="Depth" required>
<template #help> How deep you want to dig. </template> <template #help> How deep you want to dig. </template>
<UInput v-model="depth" class="w-full" type="number" :min="1" :max="5" :step="1" /> <UInput v-model="form.depth" class="w-full" type="number" :min="1" :max="5" :step="1" />
</UFormField> </UFormField>
<UFormField label="Breadth"> <UFormField label="Breadth" required>
<template #help> Number of searches in each depth. </template> <template #help> Number of searches in each depth. </template>
<UInput v-model="breadth" class="w-full" type="number" :min="1" :max="5" :step="1" /> <UInput v-model="form.breadth" class="w-full" type="number" :min="1" :max="5" :step="1" />
</UFormField> </UFormField>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<UButton type="submit" color="primary" :loading="isLoading" block @click="handleSubmit"> <UButton type="submit" color="primary" :loading="isLoadingFeedback" :disabled="isSubmitButtonDisabled" block @click="handleSubmit">
{{ isLoading ? 'Researching...' : 'Start Research' }} {{ isLoadingFeedback ? 'Researching...' : 'Start Research' }}
</UButton> </UButton>
</template> </template>
</UCard> </UCard>

View File

@ -10,7 +10,7 @@
const loading = ref(false) const loading = ref(false)
const loadingExportPdf = ref(false) const loadingExportPdf = ref(false)
const reportContent = ref('') const reportContent = ref('')
const reportHtml = computed(() => marked(reportContent.value)) const reportHtml = computed(() => marked(reportContent.value, { gfm: true, silent: true }))
const isExportButtonDisabled = computed(() => !reportContent.value || loading.value || loadingExportPdf.value) const isExportButtonDisabled = computed(() => !reportContent.value || loading.value || loadingExportPdf.value)
async function generateReport(params: CustomReportParams) { async function generateReport(params: CustomReportParams) {
@ -31,28 +31,52 @@
} }
async function exportToPdf() { async function exportToPdf() {
const element = document.getElementById('report-content')
if (!element) return
// Create a temp container
const tempContainer = document.createElement('div')
loadingExportPdf.value = true loadingExportPdf.value = true
try { try {
// 动态导入 html2pdf确保只在客户端执行 // Dinamically import html2pdf
// @ts-ignore // @ts-ignore
const html2pdf = (await import('html2pdf.js')).default const html2pdf = (await import('html2pdf.js')).default
const element = document.getElementById('report-content') tempContainer.innerHTML = element.innerHTML
tempContainer.className = element.className
if (element) { // Use print-friendly styles
const opt = { tempContainer.style.cssText = `
margin: [10, 10], font-family: Arial, sans-serif;
filename: 'research-report.pdf', color: black;
image: { type: 'jpeg', quality: 0.98 }, background-color: white;
html2canvas: { scale: 2 }, padding: 20px;
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, `
}
await html2pdf().set(opt).from(element).save() document.body.appendChild(tempContainer)
const opt = {
margin: [10, 10],
filename: 'research-report.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
backgroundColor: '#ffffff',
},
jsPDF: {
unit: 'mm',
format: 'a4',
orientation: 'portrait',
},
} }
await html2pdf().set(opt).from(tempContainer).save()
} catch (error) { } catch (error) {
console.error('Export to PDF failed:', error) console.error('Export to PDF failed:', error)
} finally { } finally {
document.body.removeChild(tempContainer)
loadingExportPdf.value = false loadingExportPdf.value = false
} }
} }

View File

@ -6,8 +6,8 @@
<h1 class="text-3xl font-bold text-center mb-2"> Deep Research Assistant </h1> <h1 class="text-3xl font-bold text-center mb-2"> Deep Research Assistant </h1>
<ColorModeButton /> <ColorModeButton />
</div> </div>
<ResearchForm @submit="generateFeedback" /> <ResearchForm :is-loading-feedback="!!feedbackRef?.isLoading" ref="formRef" @submit="generateFeedback" />
<ResearchFeedback ref="feedbackRef" @submit="startDeepSearch" /> <ResearchFeedback :is-loading-search="!!deepResearchRef?.isLoading" ref="feedbackRef" @submit="startDeepSearch" />
<DeepResearch ref="deepResearchRef" @complete="generateReport" class="mb-8" /> <DeepResearch ref="deepResearchRef" @complete="generateReport" class="mb-8" />
<ResearchReport ref="reportRef" /> <ResearchReport ref="reportRef" />
</div> </div>
@ -16,6 +16,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type ResearchForm from '~/components/ResearchForm.vue'
import type ResearchFeedback from '~/components/ResearchFeedback.vue' import type ResearchFeedback from '~/components/ResearchFeedback.vue'
import type DeepResearch from '~/components/DeepResearch.vue' import type DeepResearch from '~/components/DeepResearch.vue'
import type ResearchReport from '~/components/ResearchReport.vue' import type ResearchReport from '~/components/ResearchReport.vue'
@ -27,31 +28,30 @@
title: 'Deep Research Web UI', title: 'Deep Research Web UI',
}) })
const formRef = ref<InstanceType<typeof ResearchForm>>()
const feedbackRef = ref<InstanceType<typeof ResearchFeedback>>() const feedbackRef = ref<InstanceType<typeof ResearchFeedback>>()
const deepResearchRef = ref<InstanceType<typeof DeepResearch>>() const deepResearchRef = ref<InstanceType<typeof DeepResearch>>()
const reportRef = ref<InstanceType<typeof ResearchReport>>() const reportRef = ref<InstanceType<typeof ResearchReport>>()
const inputData = ref<ResearchInputData>()
const feedback = ref<ResearchFeedbackResult[]>([]) const feedback = ref<ResearchFeedbackResult[]>([])
const researchResult = ref<ResearchResult>() const researchResult = ref<ResearchResult>()
function getCombinedQuery() { function getCombinedQuery() {
return ` return `
Initial Query: ${inputData.value?.query} Initial Query: ${formRef.value?.form.query}
Follow-up Questions and Answers: Follow-up Questions and Answers:
${feedback.value.map((qa) => `Q: ${qa.assistantQuestion}\nA: ${qa.userAnswer}`).join('\n')} ${feedback.value.map((qa) => `Q: ${qa.assistantQuestion}\nA: ${qa.userAnswer}`).join('\n')}
` `
} }
async function generateFeedback(data: ResearchInputData) { async function generateFeedback(data: ResearchInputData) {
inputData.value = data
feedbackRef.value?.getFeedback(data.query, data.numQuestions) feedbackRef.value?.getFeedback(data.query, data.numQuestions)
} }
async function startDeepSearch(_feedback: ResearchFeedbackResult[]) { async function startDeepSearch(_feedback: ResearchFeedbackResult[]) {
if (!inputData.value) return if (!formRef.value?.form.query || !formRef.value?.form.breadth || !formRef.value?.form.depth) return
feedback.value = _feedback feedback.value = _feedback
deepResearchRef.value?.startResearch(getCombinedQuery(), inputData.value.breadth, inputData.value.depth) deepResearchRef.value?.startResearch(getCombinedQuery(), formRef.value.form.breadth, formRef.value.form.depth)
} }
async function generateReport(_researchResult: ResearchResult) { async function generateReport(_researchResult: ResearchResult) {