feat: handle loading states
This commit is contained in:
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
Reference in New Issue
Block a user