feat: i18n support
This commit is contained in:
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"i18n-ally.localesPaths": [
|
||||||
|
"i18n"
|
||||||
|
],
|
||||||
|
"i18n-ally.keystyle": "nested"
|
||||||
|
}
|
@ -1,7 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { config } = useConfigStore()
|
const { config } = useConfigStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
|
|
||||||
|
const aiProviderOptions = computed(() => [
|
||||||
|
{
|
||||||
|
label: t('settings.ai.providers.openaiCompatible.title'),
|
||||||
|
help: t('settings.ai.providers.openaiCompatible.description'),
|
||||||
|
apiBasePlaceholder: t(
|
||||||
|
'settings.ai.providers.openaiCompatible.apiBasePlaceholder',
|
||||||
|
),
|
||||||
|
value: 'openai-compatible',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
const selectedAiProvider = computed(() =>
|
||||||
|
aiProviderOptions.value.find((o) => o.value === config.ai.provider),
|
||||||
|
)
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show() {
|
show() {
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
@ -11,41 +27,35 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<UModal v-model:open="showModal" title="Settings">
|
<UModal v-model:open="showModal" :title="$t('settings.title')">
|
||||||
<UButton icon="i-lucide-settings" />
|
<UButton icon="i-lucide-settings" />
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="space-y-2">
|
<div class="flex flex-col gap-y-2">
|
||||||
<!-- AI provider -->
|
<!-- AI provider -->
|
||||||
<h3 class="font-bold">AI Provider</h3>
|
<h3 class="font-bold">{{ $t('settings.ai.provider') }}</h3>
|
||||||
<UFormField label="Provider">
|
<UFormField>
|
||||||
<template #help>
|
<template v-if="selectedAiProvider" #help>
|
||||||
Currently only OpenAI compatible providers are supported, e.g.
|
{{ selectedAiProvider.help }}
|
||||||
Gemini, Together AI, SiliconCloud, ...
|
|
||||||
</template>
|
</template>
|
||||||
<USelect
|
<USelect v-model="config.ai.provider" :items="aiProviderOptions" />
|
||||||
v-model="config.ai.provider"
|
|
||||||
:items="[
|
|
||||||
{ label: 'OpenAI Compatible', value: 'openai-compatible' },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</UFormField>
|
</UFormField>
|
||||||
<div
|
<div
|
||||||
v-if="config.ai.provider === 'openai-compatible'"
|
v-if="config.ai.provider === 'openai-compatible'"
|
||||||
class="space-y-2"
|
class="space-y-2"
|
||||||
>
|
>
|
||||||
<UFormField label="API Key" required>
|
<UFormField :label="$t('settings.ai.apiKey')" required>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
v-model="config.ai.apiKey"
|
v-model="config.ai.apiKey"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
placeholder="API Key"
|
:placeholder="$t('settings.ai.apiKey')"
|
||||||
/>
|
/>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
<UFormField label="API Base URL">
|
<UFormField :label="$t('settings.ai.apiBase')">
|
||||||
<UInput
|
<UInput
|
||||||
v-model="config.ai.apiBase"
|
v-model="config.ai.apiBase"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
placeholder="https://api.openai.com/v1"
|
:placeholder="selectedAiProvider?.apiBasePlaceholder"
|
||||||
/>
|
/>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
<UFormField label="Model" required>
|
<UFormField label="Model" required>
|
||||||
@ -57,34 +67,33 @@
|
|||||||
</UFormField>
|
</UFormField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<USeparator class="my-4" />
|
<USeparator class="my-2" />
|
||||||
|
|
||||||
<!-- Web search provider -->
|
<!-- Web search provider -->
|
||||||
<h3 class="font-bold">Web Search Provider</h3>
|
<h3 class="font-bold"> {{ $t('settings.webSearch.provider') }} </h3>
|
||||||
<UFormField label="Provider">
|
<UFormField>
|
||||||
<template #help>
|
<template #help>
|
||||||
Tavily is similar to Firecrawl, but with more free quota (1000
|
<i18n-t keypath="settings.webSearch.providerHelp" tag="p">
|
||||||
credits / month). Get one API key at
|
<UButton
|
||||||
<UButton
|
class="!p-0"
|
||||||
class="!p-0"
|
to="https://app.tavily.com/home"
|
||||||
to="https://app.tavily.com/home"
|
target="_blank"
|
||||||
target="_blank"
|
variant="link"
|
||||||
variant="link"
|
>
|
||||||
>
|
app.tavily.com
|
||||||
app.tavily.com
|
</UButton>
|
||||||
</UButton>
|
</i18n-t>
|
||||||
.
|
|
||||||
</template>
|
</template>
|
||||||
<USelect
|
<USelect
|
||||||
v-model="config.webSearch.provider"
|
v-model="config.webSearch.provider"
|
||||||
:items="[{ label: 'Tavily', value: 'tavily' }]"
|
:items="[{ label: 'Tavily', value: 'tavily' }]"
|
||||||
/>
|
/>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
<UFormField label="API Key" required>
|
<UFormField :label="$t('settings.webSearch.apiKey')" required>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
v-model="config.webSearch.apiKey"
|
v-model="config.webSearch.apiKey"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
placeholder="API Key"
|
:placeholder="$t('settings.webSearch.apiKey')"
|
||||||
/>
|
/>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
</div>
|
</div>
|
||||||
@ -92,14 +101,14 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex items-center justify-between gap-2 w-full">
|
<div class="flex items-center justify-between gap-2 w-full">
|
||||||
<p class="text-sm text-gray-500">
|
<p class="text-sm text-gray-500">
|
||||||
Settings are stored locally in your browser.
|
{{ $t('settings.disclaimer') }}
|
||||||
</p>
|
</p>
|
||||||
<UButton
|
<UButton
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="i-lucide-check"
|
icon="i-lucide-check"
|
||||||
@click="showModal = false"
|
@click="showModal = false"
|
||||||
>
|
>
|
||||||
Save
|
{{ $t('settings.save') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -6,14 +6,16 @@
|
|||||||
type ResearchStep,
|
type ResearchStep,
|
||||||
} from '~/lib/deep-research'
|
} from '~/lib/deep-research'
|
||||||
import type { TreeNode } from './Tree.vue'
|
import type { TreeNode } from './Tree.vue'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'complete', results: ResearchResult): void
|
(e: 'complete', results: ResearchResult): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const tree = ref<TreeNode>({
|
const tree = ref<TreeNode>({
|
||||||
id: '0',
|
id: '0',
|
||||||
label: 'Start',
|
label: t('webBrowsing.startNode.label'),
|
||||||
children: [],
|
children: [],
|
||||||
})
|
})
|
||||||
const selectedNode = ref<TreeNode>()
|
const selectedNode = ref<TreeNode>()
|
||||||
@ -38,8 +40,8 @@
|
|||||||
// 创建新节点
|
// 创建新节点
|
||||||
node = {
|
node = {
|
||||||
id: nodeId,
|
id: nodeId,
|
||||||
label: 'Generating...',
|
label: t('webBrowsing.generating'),
|
||||||
researchGoal: 'Generating research goal...',
|
researchGoal: t('webBrowsing.generating'),
|
||||||
learnings: [],
|
learnings: [],
|
||||||
children: [],
|
children: [],
|
||||||
}
|
}
|
||||||
@ -60,7 +62,7 @@
|
|||||||
}
|
}
|
||||||
// 更新节点的查询内容
|
// 更新节点的查询内容
|
||||||
if (step.result) {
|
if (step.result) {
|
||||||
node.label = step.result.query ?? 'Generating...'
|
node.label = step.result.query ?? t('webBrowsing.generating')
|
||||||
node.researchGoal = step.result.researchGoal
|
node.researchGoal = step.result.researchGoal
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@ -166,12 +168,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<UCard>
|
<UCard>
|
||||||
<template #header>
|
<template #header>
|
||||||
<h2 class="font-bold">3. Web Browsing</h2>
|
<h2 class="font-bold">{{ t('webBrowsing.title') }}</h2>
|
||||||
<p class="text-sm text-gray-500">
|
<p class="text-sm text-gray-500">
|
||||||
The AI will then search the web based on our research goal, and iterate
|
{{ t('webBrowsing.description') }}
|
||||||
until the depth is reached.
|
|
||||||
<br />
|
<br />
|
||||||
Click a child node to view details.
|
{{ t('webBrowsing.clickToView') }}
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
@ -179,31 +180,44 @@
|
|||||||
<Tree :node="tree" :selected-node="selectedNode" @select="selectNode" />
|
<Tree :node="tree" :selected-node="selectedNode" @select="selectNode" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedNode" class="p-4">
|
<div v-if="selectedNode" class="p-4">
|
||||||
<USeparator label="Node Details" />
|
<USeparator :label="t('webBrowsing.nodeDetails')" />
|
||||||
<h2 class="text-xl font-bold mt-2">{{ selectedNode.label }}</h2>
|
<h2 class="text-xl font-bold mt-2">{{ selectedNode.label }}</h2>
|
||||||
|
|
||||||
<!-- Root node has no additional information -->
|
<!-- Root node has no additional information -->
|
||||||
<p v-if="selectedNode.id === '0'">
|
<p v-if="selectedNode.id === '0'">
|
||||||
This is the beginning of your deep research journey!
|
{{ t('webBrowsing.startNode.description') }}
|
||||||
</p>
|
</p>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h3 class="text-lg font-semibold mt-2">Research Goal:</h3>
|
<h3 class="text-lg font-semibold mt-2">
|
||||||
|
{{ t('webBrowsing.researchGoal') }}
|
||||||
|
</h3>
|
||||||
<p>{{ selectedNode.researchGoal }}</p>
|
<p>{{ selectedNode.researchGoal }}</p>
|
||||||
|
|
||||||
<h3 class="text-lg font-semibold mt-2">Visited URLs:</h3>
|
<h3 class="text-lg font-semibold mt-2">
|
||||||
|
{{ t('webBrowsing.visitedUrls') }}
|
||||||
|
</h3>
|
||||||
<ul class="list-disc list-inside">
|
<ul class="list-disc list-inside">
|
||||||
<li v-for="(url, index) in selectedNode.visitedUrls" :key="index">
|
<li v-for="(url, index) in selectedNode.visitedUrls" :key="index">
|
||||||
<ULink :href="url" target="_blank">{{ url }}</ULink>
|
<UButton
|
||||||
|
class="!p-0 break-all whitespace-pre-wrap"
|
||||||
|
variant="link"
|
||||||
|
:href="url"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ url }}
|
||||||
|
</UButton>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 class="text-lg font-semibold mt-2">Learnings:</h3>
|
<h3 class="text-lg font-semibold mt-2">
|
||||||
|
{{ t('webBrowsing.learnings') }}
|
||||||
|
</h3>
|
||||||
<ul class="list-disc list-inside">
|
<ul class="list-disc list-inside">
|
||||||
<li
|
<li
|
||||||
v-for="(learning, index) in selectedNode.learnings"
|
v-for="(learning, index) in selectedNode.learnings"
|
||||||
:key="index"
|
:key="index"
|
||||||
>{{ learning }}</li
|
v-html="marked(learning)"
|
||||||
>
|
></li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,5 @@
|
|||||||
to="https://github.com/AnotiaWang/deep-research-web-ui"
|
to="https://github.com/AnotiaWang/deep-research-web-ui"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
GitHub
|
|
||||||
</UButton>
|
</UButton>
|
||||||
</template>
|
</template>
|
||||||
|
17
components/LangSwitcher.vue
Normal file
17
components/LangSwitcher.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { locale, availableLocales, t, setLocale } = useI18n()
|
||||||
|
|
||||||
|
const localeOptions = availableLocales.map((locale) => ({
|
||||||
|
value: locale,
|
||||||
|
label: t('language', {}, { locale }),
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<USelect
|
||||||
|
icon="i-lucide-languages"
|
||||||
|
:model-value="locale"
|
||||||
|
:items="localeOptions"
|
||||||
|
@update:model-value="setLocale($event)"
|
||||||
|
/>
|
||||||
|
</template>
|
@ -14,6 +14,7 @@
|
|||||||
(e: 'submit', feedback: ResearchFeedbackResult[]): void
|
(e: 'submit', feedback: ResearchFeedbackResult[]): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const feedback = ref<ResearchFeedbackResult[]>([])
|
const feedback = ref<ResearchFeedbackResult[]>([])
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
@ -52,7 +53,7 @@
|
|||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('Error getting feedback:', e)
|
console.error('Error getting feedback:', e)
|
||||||
error.value = e.message
|
error.value = t('modelFeedback.error', [e.message])
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
@ -73,16 +74,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<UCard>
|
<UCard>
|
||||||
<template #header>
|
<template #header>
|
||||||
<h2 class="font-bold">2. Model Feedback</h2>
|
<h2 class="font-bold">{{ $t('modelFeedback.title') }}</h2>
|
||||||
<p class="text-sm text-gray-500">
|
<p class="text-sm text-gray-500">
|
||||||
The AI will ask you some follow up questions to help you clarify the
|
{{ $t('modelFeedback.description') }}
|
||||||
research direction.
|
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p v-if="error" class="text-red-500">{{ error }}</p>
|
<p v-if="error" class="text-red-500">{{ error }}</p>
|
||||||
<div v-if="!feedback.length && !error">Waiting for model feedback...</div>
|
<div v-if="!feedback.length && !error">{{ $t('modelFeedback.waiting') }}</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div v-if="error" class="text-red-500">{{ error }}</div>
|
<div v-if="error" class="text-red-500">{{ error }}</div>
|
||||||
<div
|
<div
|
||||||
@ -90,7 +90,7 @@
|
|||||||
class="flex flex-col gap-2"
|
class="flex flex-col gap-2"
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
Assistant: {{ feedback.assistantQuestion }}
|
{{ feedback.assistantQuestion }}
|
||||||
<UInput v-model="feedback.userAnswer" />
|
<UInput v-model="feedback.userAnswer" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -101,7 +101,7 @@
|
|||||||
block
|
block
|
||||||
@click="$emit('submit', feedback)"
|
@click="$emit('submit', feedback)"
|
||||||
>
|
>
|
||||||
Submit Answer
|
{{ $t('modelFeedback.submit') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
|
@ -39,22 +39,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<UCard>
|
<UCard>
|
||||||
<template #header>
|
<template #header>
|
||||||
<h2 class="font-bold">1. Research Topic</h2>
|
<h2 class="font-bold">{{ $t('researchTopic.title') }}</h2>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<UFormField label="Research Topic" required>
|
<UFormField :label="$t('researchTopic.title')" required>
|
||||||
<UTextarea
|
<UTextarea
|
||||||
class="w-full"
|
class="w-full"
|
||||||
v-model="form.query"
|
v-model="form.query"
|
||||||
:rows="3"
|
:rows="3"
|
||||||
placeholder="Enter whatever you want to research..."
|
:placeholder="$t('researchTopic.placeholder')"
|
||||||
required
|
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" required>
|
<UFormField :label="$t('researchTopic.numOfQuestions')" required>
|
||||||
<template #help> Number of questions for you to clarify. </template>
|
<template #help>
|
||||||
|
{{ $t('researchTopic.numOfQuestionsHelp') }}
|
||||||
|
</template>
|
||||||
<UInput
|
<UInput
|
||||||
v-model="form.numQuestions"
|
v-model="form.numQuestions"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@ -65,8 +67,8 @@
|
|||||||
/>
|
/>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<UFormField label="Depth" required>
|
<UFormField :label="$t('researchTopic.depth')" required>
|
||||||
<template #help> How deep you want to dig. </template>
|
<template #help>{{ $t('researchTopic.depthHelp') }}</template>
|
||||||
<UInput
|
<UInput
|
||||||
v-model="form.depth"
|
v-model="form.depth"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@ -77,8 +79,8 @@
|
|||||||
/>
|
/>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<UFormField label="Breadth" required>
|
<UFormField :label="$t('researchTopic.breadth')" required>
|
||||||
<template #help> Number of searches in each depth. </template>
|
<template #help>{{ $t('researchTopic.breadthHelp') }}</template>
|
||||||
<UInput
|
<UInput
|
||||||
v-model="form.breadth"
|
v-model="form.breadth"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@ -100,7 +102,7 @@
|
|||||||
block
|
block
|
||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
>
|
>
|
||||||
{{ isLoadingFeedback ? 'Researching...' : 'Start Research' }}
|
{{ isLoadingFeedback ? 'Researching...' : $t('researchTopic.start') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</template>
|
</template>
|
||||||
</UCard>
|
</UCard>
|
||||||
|
@ -9,12 +9,13 @@
|
|||||||
visitedUrls: string[]
|
visitedUrls: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
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(() =>
|
const reportHtml = computed(() =>
|
||||||
marked(reportContent.value, { gfm: true, silent: true }),
|
marked(reportContent.value, { silent: true, gfm: true, breaks: true }),
|
||||||
)
|
)
|
||||||
const isExportButtonDisabled = computed(
|
const isExportButtonDisabled = computed(
|
||||||
() => !reportContent.value || loading.value || loadingExportPdf.value,
|
() => !reportContent.value || loading.value || loadingExportPdf.value,
|
||||||
@ -28,10 +29,12 @@
|
|||||||
for await (const chunk of writeFinalReport(params).textStream) {
|
for await (const chunk of writeFinalReport(params).textStream) {
|
||||||
reportContent.value += chunk
|
reportContent.value += chunk
|
||||||
}
|
}
|
||||||
reportContent.value += `\n\n## Sources\n\n${params.visitedUrls.map((url) => `- ${url}`).join('\n')}`
|
reportContent.value += `\n\n## ${t(
|
||||||
|
'researchReport.sources',
|
||||||
|
)}\n\n${params.visitedUrls.map((url) => `- ${url}`).join('\n')}`
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`Generate report failed`, e)
|
console.error(`Generate report failed`, e)
|
||||||
error.value = e.message
|
error.value = t('researchReport.error', [e.message])
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@ -98,7 +101,7 @@
|
|||||||
<UCard>
|
<UCard>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="font-bold">4. Research Report</h2>
|
<h2 class="font-bold">{{ $t('researchReport.title') }}</h2>
|
||||||
<UButton
|
<UButton
|
||||||
color="info"
|
color="info"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -107,7 +110,7 @@
|
|||||||
:loading="loadingExportPdf"
|
:loading="loadingExportPdf"
|
||||||
@click="exportToPdf"
|
@click="exportToPdf"
|
||||||
>
|
>
|
||||||
Export PDF
|
{{ $t('researchReport.exportPdf') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -120,9 +123,13 @@
|
|||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div v-if="error" class="text-red-500">{{ error }}</div>
|
<div v-if="error" class="text-red-500">{{ error }}</div>
|
||||||
<div v-else
|
<div v-else>
|
||||||
>{{ loading ? 'Generating report...' : 'Waiting for report..' }}.</div
|
{{
|
||||||
>
|
loading
|
||||||
|
? $t('researchReport.generating')
|
||||||
|
: $t('researchReport.waiting')
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UCard>
|
</UCard>
|
||||||
</template>
|
</template>
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<UIcon name="i-lucide-circle-dot" />
|
<UIcon name="i-lucide-circle-dot" />
|
||||||
<UButton
|
<UButton
|
||||||
:class="icon.pulse && 'animate-pulse'"
|
:class="['max-w-90 shrink-0', icon.pulse && 'animate-pulse']"
|
||||||
:icon="icon.name"
|
:icon="icon.name"
|
||||||
size="sm"
|
size="sm"
|
||||||
:color="selectedNode?.id === node.id ? 'primary' : 'info'"
|
:color="selectedNode?.id === node.id ? 'primary' : 'info'"
|
||||||
|
@ -6,7 +6,7 @@ export const useAiModel = () => {
|
|||||||
case 'openai-compatible':
|
case 'openai-compatible':
|
||||||
const openai = createOpenAI({
|
const openai = createOpenAI({
|
||||||
apiKey: config.config.ai.apiKey,
|
apiKey: config.config.ai.apiKey,
|
||||||
baseURL: config.config.ai.apiBase || 'https://api.openai.com/v1',
|
baseURL: config.config.ai.apiBase || 'https://api.openai.com/v1', // TODO: better default
|
||||||
})
|
})
|
||||||
return openai(config.config.ai.model)
|
return openai(config.config.ai.model)
|
||||||
default:
|
default:
|
||||||
|
12
i18n.config.ts
Normal file
12
i18n.config.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import en from '~/i18n/en.json'
|
||||||
|
import zh from '~/i18n/zh.json'
|
||||||
|
|
||||||
|
export default defineI18nConfig(() => ({
|
||||||
|
legacy: false,
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
availableLocales: ['en', 'zh'],
|
||||||
|
messages: {
|
||||||
|
en,
|
||||||
|
zh,
|
||||||
|
},
|
||||||
|
}))
|
74
i18n/en.json
Normal file
74
i18n/en.json
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"language": "English",
|
||||||
|
"index": {
|
||||||
|
"projectDescription": "This is a web UI for {0} that allows AI to search online and dig deeper on its own based on specific questions, and then output a research report.\nThis project features streaming AI responses for realtime feedback, and visualization of the research process using a tree structure.\nAll API requests are directly sent from your browser. No remote data stored.",
|
||||||
|
"missingConfigTitle": "Config not set",
|
||||||
|
"missingConfigDescription": "This project requires you to bring your own API keys."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"disclaimer": "Settings are stored locally in your browser.",
|
||||||
|
"save": "Save",
|
||||||
|
"ai": {
|
||||||
|
"provider": "AI Provider",
|
||||||
|
"apiKey": "API Key",
|
||||||
|
"apiBase": "API Base URL",
|
||||||
|
"model": "Model",
|
||||||
|
"contextSize": "Context Size",
|
||||||
|
"contextSizeHelp": "The maximum size of the context in tokens. This is the maximum number of tokens that will be sent to the model. The default is 128,000 tokens.",
|
||||||
|
"providers": {
|
||||||
|
"openaiCompatible": {
|
||||||
|
"title": "OpenAI Compatible",
|
||||||
|
"description": "Currently only supports OpenAI compatible providers, e.g. Gemini, Together AI, DeepSeek, SiliconCloud, ...",
|
||||||
|
"apiBasePlaceholder": "https://api.openai.com/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webSearch": {
|
||||||
|
"provider": "Web Search Provider",
|
||||||
|
"providerHelp": "Currently only supports Tavily. It provides lots of free quota (1000 credits / month).\nGet one API key at {0}.",
|
||||||
|
"apiKey": "API Key"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"researchTopic": {
|
||||||
|
"title": "1. Research Topic",
|
||||||
|
"placeholder": "Whatever you want to research...",
|
||||||
|
"numOfQuestions": "Number of Questions",
|
||||||
|
"numOfQuestionsHelp": "The number of follow-up questions to clarify.",
|
||||||
|
"depth": "Depth",
|
||||||
|
"depthHelp": "Number of iterations.",
|
||||||
|
"breadth": "Breadth",
|
||||||
|
"breadthHelp": "Number of searches in the first iteration. The search width of each iteration is half of the previous one.",
|
||||||
|
"start": "Start Research",
|
||||||
|
"researching": "Researching..."
|
||||||
|
},
|
||||||
|
"modelFeedback": {
|
||||||
|
"title": "2. Model Feedback",
|
||||||
|
"description": "The AI will ask you some follow up questions to help you clarify the research direction.",
|
||||||
|
"waiting": "Waiting for model feedback...",
|
||||||
|
"submit": "Submit Answer",
|
||||||
|
"error": "Error getting feedback: {0}"
|
||||||
|
},
|
||||||
|
"webBrowsing": {
|
||||||
|
"title": "3. Web Browsing",
|
||||||
|
"description": "The AI will then search the web based on our research goal, and iterate until the depth is reached.",
|
||||||
|
"clickToView": "Click a child node to view details.",
|
||||||
|
"nodeDetails": "Node Details",
|
||||||
|
"startNode": {
|
||||||
|
"label": "Start",
|
||||||
|
"description": "This is the beginning of your deep research journey!"
|
||||||
|
},
|
||||||
|
"researchGoal": "Research Goal",
|
||||||
|
"visitedUrls": "Visited URLs",
|
||||||
|
"learnings": "Learnings",
|
||||||
|
"generating": "Generating..."
|
||||||
|
},
|
||||||
|
"researchReport": {
|
||||||
|
"title": "4. Research Report",
|
||||||
|
"exportPdf": "Export PDF",
|
||||||
|
"sources": "Sources",
|
||||||
|
"waiting": "Waiting for report...",
|
||||||
|
"generating": "Generating report...",
|
||||||
|
"error": "Generate report failed: {0}"
|
||||||
|
}
|
||||||
|
}
|
74
i18n/zh.json
Normal file
74
i18n/zh.json
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"language": "中文",
|
||||||
|
"index": {
|
||||||
|
"projectDescription": "Deep Research 是 {0} 的可视化 UI,可以让 AI 根据特定问题联网搜索并自行深挖,并输出研究报告。\n本项目可以流式传输 AI 的回答来实时反馈,并使用树状结构可视化搜索过程。\n全部 API 请求都在浏览器本地完成。",
|
||||||
|
"missingConfigTitle": "需要配置 API",
|
||||||
|
"missingConfigDescription": "本项目需要您自备 AI 和联网搜索服务的配置 (Bring Your Own Key)"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "设置",
|
||||||
|
"disclaimer": "所有设置本地保存",
|
||||||
|
"save": "保存",
|
||||||
|
"ai": {
|
||||||
|
"provider": "AI 服务",
|
||||||
|
"apiKey": "API 密钥",
|
||||||
|
"apiBase": "API Base URL",
|
||||||
|
"model": "模型名称",
|
||||||
|
"contextSize": "上下文大小",
|
||||||
|
"contextSizeHelp": "上下文的最大大小(以 token 计)。这是将发送给模型的最大 token 数量。默认值为 128,000 个 token。",
|
||||||
|
"providers": {
|
||||||
|
"openaiCompatible": {
|
||||||
|
"title": "OpenAI Compatiible",
|
||||||
|
"description": "目前仅支持与 OpenAI 兼容的提供商,如 Gemini、Together AI、DeepSeek、SiliconCloud……",
|
||||||
|
"apiBasePlaceholder": "https://api.openai.com/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webSearch": {
|
||||||
|
"provider": "联网搜索服务",
|
||||||
|
"providerHelp": "目前仅支持 Tavily,每个月可以免费搜索 1000 次。\n请在 {0} 生成一个 API 密钥。",
|
||||||
|
"apiKey": "API 密钥"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"researchTopic": {
|
||||||
|
"title": "1. 研究主题",
|
||||||
|
"placeholder": "任何你想了解的内容...",
|
||||||
|
"numOfQuestions": "问题数量",
|
||||||
|
"numOfQuestionsHelp": "AI 询问你的问题数量。这些问题能让 AI 更好地了解你的研究目标。",
|
||||||
|
"depth": "研究深度 (Depth)",
|
||||||
|
"depthHelp": "联网搜索的迭代轮数。",
|
||||||
|
"breadth": "研究广度 (Breadth)",
|
||||||
|
"breadthHelp": "第一次迭代中的搜索次数。后续每轮迭代的搜索次数为上一轮的一半。",
|
||||||
|
"start": "开始研究",
|
||||||
|
"researching": "正在研究..."
|
||||||
|
},
|
||||||
|
"modelFeedback": {
|
||||||
|
"title": "2. 模型反馈",
|
||||||
|
"description": "AI 将会跟你确认一些细节,帮助你明确研究方向。",
|
||||||
|
"waiting": "等待模型反馈...",
|
||||||
|
"submit": "提交回答",
|
||||||
|
"error": "获取反馈失败:{0}"
|
||||||
|
},
|
||||||
|
"webBrowsing": {
|
||||||
|
"title": "3. 联网搜索",
|
||||||
|
"description": "AI 将根据上述信息联网搜索并自动迭代,直到迭代次数 = depth。",
|
||||||
|
"clickToView": "点击下面的节点查看搜索详情。",
|
||||||
|
"nodeDetails": "节点详情",
|
||||||
|
"startNode": {
|
||||||
|
"label": "Start",
|
||||||
|
"description": "这是本次研究的起点"
|
||||||
|
},
|
||||||
|
"researchGoal": "研究目标",
|
||||||
|
"visitedUrls": "访问网址",
|
||||||
|
"learnings": "结论",
|
||||||
|
"generating": "生成中..."
|
||||||
|
},
|
||||||
|
"researchReport": {
|
||||||
|
"title": "4. 研究报告",
|
||||||
|
"exportPdf": "导出 PDF",
|
||||||
|
"sources": "来源",
|
||||||
|
"waiting": "等待报告...",
|
||||||
|
"generating": "生成报告中...",
|
||||||
|
"error": "生成报告失败:{0}"
|
||||||
|
}
|
||||||
|
}
|
93
lib/run.ts
93
lib/run.ts
@ -1,93 +0,0 @@
|
|||||||
import * as fs from 'fs/promises'
|
|
||||||
import * as readline from 'readline'
|
|
||||||
|
|
||||||
import { deepResearch, writeFinalReport } from './deep-research'
|
|
||||||
import { generateFeedback } from './feedback'
|
|
||||||
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Helper function to get user input
|
|
||||||
function askQuestion(query: string): Promise<string> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
rl.question(query, (answer) => {
|
|
||||||
resolve(answer)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// run the agent
|
|
||||||
async function run() {
|
|
||||||
// Get initial query
|
|
||||||
const initialQuery = await askQuestion('What would you like to research? ')
|
|
||||||
|
|
||||||
// Get breath and depth parameters
|
|
||||||
const breadth =
|
|
||||||
parseInt(
|
|
||||||
await askQuestion(
|
|
||||||
'Enter research breadth (recommended 2-10, default 4): ',
|
|
||||||
),
|
|
||||||
10,
|
|
||||||
) || 4
|
|
||||||
const depth =
|
|
||||||
parseInt(
|
|
||||||
await askQuestion('Enter research depth (recommended 1-5, default 2): '),
|
|
||||||
10,
|
|
||||||
) || 2
|
|
||||||
|
|
||||||
console.log(`Creating research plan...`)
|
|
||||||
|
|
||||||
// Generate follow-up questions
|
|
||||||
const followUpQuestions = await generateFeedback({
|
|
||||||
query: initialQuery,
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'\nTo better understand your research needs, please answer these follow-up questions:',
|
|
||||||
)
|
|
||||||
|
|
||||||
// Collect answers to follow-up questions
|
|
||||||
const answers: string[] = []
|
|
||||||
for (const question of followUpQuestions) {
|
|
||||||
const answer = await askQuestion(`\n${question}\nYour answer: `)
|
|
||||||
answers.push(answer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine all information for deep research
|
|
||||||
const combinedQuery = `
|
|
||||||
Initial Query: ${initialQuery}
|
|
||||||
Follow-up Questions and Answers:
|
|
||||||
${followUpQuestions.map((q, i) => `Q: ${q}\nA: ${answers[i]}`).join('\n')}
|
|
||||||
`
|
|
||||||
|
|
||||||
console.log('\nResearching your topic...')
|
|
||||||
|
|
||||||
const { learnings, visitedUrls } = await deepResearch({
|
|
||||||
query: combinedQuery,
|
|
||||||
breadth,
|
|
||||||
depth,
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`\n\nLearnings:\n\n${learnings.join('\n')}`)
|
|
||||||
console.log(
|
|
||||||
`\n\nVisited URLs (${visitedUrls.length}):\n\n${visitedUrls.join('\n')}`,
|
|
||||||
)
|
|
||||||
console.log('Writing final report...')
|
|
||||||
|
|
||||||
const report = await writeFinalReport({
|
|
||||||
prompt: combinedQuery,
|
|
||||||
learnings,
|
|
||||||
visitedUrls,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Save report to file
|
|
||||||
await fs.writeFile('output.md', report, 'utf-8')
|
|
||||||
|
|
||||||
console.log(`\n\nFinal Report:\n\n${report}`)
|
|
||||||
console.log('\nReport has been saved to output.md')
|
|
||||||
rl.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
run().catch(console.error)
|
|
@ -1,6 +1,24 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
modules: ['@pinia/nuxt', '@nuxt/ui', '@nuxtjs/color-mode', '@vueuse/nuxt'],
|
modules: [
|
||||||
|
'@pinia/nuxt',
|
||||||
|
'@nuxt/ui',
|
||||||
|
'@nuxtjs/color-mode',
|
||||||
|
'@vueuse/nuxt',
|
||||||
|
'@nuxtjs/i18n',
|
||||||
|
],
|
||||||
|
|
||||||
|
i18n: {
|
||||||
|
vueI18n: './i18n.config.ts',
|
||||||
|
strategy: 'no_prefix',
|
||||||
|
locales: ['en', 'zh'],
|
||||||
|
detectBrowserLanguage: {
|
||||||
|
alwaysRedirect: true,
|
||||||
|
useCookie: true,
|
||||||
|
cookieKey: 'i18n_redirected',
|
||||||
|
redirectOn: 'root',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
colorMode: {
|
colorMode: {
|
||||||
preference: 'system',
|
preference: 'system',
|
||||||
|
@ -14,9 +14,9 @@
|
|||||||
"@ai-sdk/ui-utils": "^1.1.11",
|
"@ai-sdk/ui-utils": "^1.1.11",
|
||||||
"@ai-sdk/vue": "^1.1.11",
|
"@ai-sdk/vue": "^1.1.11",
|
||||||
"@iconify-json/lucide": "^1.2.26",
|
"@iconify-json/lucide": "^1.2.26",
|
||||||
"@mendable/firecrawl-js": "^1.16.0",
|
|
||||||
"@nuxt/ui": "3.0.0-alpha.12",
|
"@nuxt/ui": "3.0.0-alpha.12",
|
||||||
"@nuxtjs/color-mode": "^3.5.2",
|
"@nuxtjs/color-mode": "^3.5.2",
|
||||||
|
"@nuxtjs/i18n": "9.2.0",
|
||||||
"@pinia/nuxt": "^0.10.0",
|
"@pinia/nuxt": "^0.10.0",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tavily/core": "^0.0.3",
|
"@tavily/core": "^0.0.3",
|
||||||
|
@ -3,27 +3,29 @@
|
|||||||
<UContainer>
|
<UContainer>
|
||||||
<div class="max-w-4xl mx-auto py-8 flex flex-col gap-y-4">
|
<div class="max-w-4xl mx-auto py-8 flex flex-col gap-y-4">
|
||||||
<div class="flex flex-col sm:flex-row gap-2">
|
<div class="flex flex-col sm:flex-row gap-2">
|
||||||
<h1 class="text-3xl font-bold text-center mb-2">
|
<h1 class="text-3xl font-bold text-center mb-2"> Deep Research </h1>
|
||||||
Deep Research Assistant
|
|
||||||
</h1>
|
|
||||||
<div class="mx-auto sm:ml-auto sm:mr-0 flex items-center gap-2">
|
<div class="mx-auto sm:ml-auto sm:mr-0 flex items-center gap-2">
|
||||||
<GitHubButton />
|
<GitHubButton />
|
||||||
<ConfigManager ref="configManagerRef" />
|
<ConfigManager ref="configManagerRef" />
|
||||||
<ColorModeButton />
|
<ColorModeButton />
|
||||||
|
<LangSwitcher />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<i18n-t
|
||||||
This is a web UI for
|
class="whitespace-pre-wrap"
|
||||||
<ULink target="_blank" href="https://github.com/dzhng/deep-research">
|
keypath="index.projectDescription"
|
||||||
|
tag="p"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
class="!p-0"
|
||||||
|
variant="link"
|
||||||
|
href="https://github.com/dzhng/deep-research"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
dzhng/deep-research
|
dzhng/deep-research
|
||||||
</ULink>
|
</UButton>
|
||||||
. It features streaming AI responses for realtime feedback, and
|
</i18n-t>
|
||||||
viasualization of the research process using a tree structure.
|
|
||||||
<br />
|
|
||||||
All API requests are directly sent from your browser. No remote data
|
|
||||||
stored.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ResearchForm
|
<ResearchForm
|
||||||
:is-loading-feedback="!!feedbackRef?.isLoading"
|
:is-loading-feedback="!!feedbackRef?.isLoading"
|
||||||
@ -52,6 +54,7 @@
|
|||||||
import type { ResearchFeedbackResult } from '~/components/ResearchFeedback.vue'
|
import type { ResearchFeedbackResult } from '~/components/ResearchFeedback.vue'
|
||||||
import type { ResearchResult } from '~/lib/deep-research'
|
import type { ResearchResult } from '~/lib/deep-research'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
@ -79,8 +82,8 @@ ${feedback.value
|
|||||||
|
|
||||||
if (!aiConfig.model || !aiConfig.apiKey || !webSearchConfig.apiKey) {
|
if (!aiConfig.model || !aiConfig.apiKey || !webSearchConfig.apiKey) {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: 'Config not set',
|
title: t('index.missingConfigTitle'),
|
||||||
description: 'Please configure AI and Web Search settings.',
|
description: t('index.missingConfigDescription'),
|
||||||
color: 'error',
|
color: 'error',
|
||||||
})
|
})
|
||||||
configManagerRef.value?.show()
|
configManagerRef.value?.show()
|
||||||
|
932
pnpm-lock.yaml
generated
932
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,33 +0,0 @@
|
|||||||
// This file is currently unused
|
|
||||||
import { deepResearch, ResearchStep } from '~/lib/deep-research'
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const { initialQuery, feedback, depth, breadth } = await readBody(event)
|
|
||||||
console.log({ initialQuery, feedback, depth, breadth })
|
|
||||||
|
|
||||||
// 设置 SSE 响应头
|
|
||||||
setHeader(event, 'Content-Type', 'text/event-stream')
|
|
||||||
setHeader(event, 'Cache-Control', 'no-cache')
|
|
||||||
setHeader(event, 'Connection', 'keep-alive')
|
|
||||||
|
|
||||||
const combinedQuery = `
|
|
||||||
Initial Query: ${initialQuery}
|
|
||||||
Follow-up Questions and Answers:
|
|
||||||
${feedback.map((qa: { question: string; answer: string }) => `Q: ${qa.question}\nA: ${qa.answer}`).join('\n')}
|
|
||||||
`
|
|
||||||
|
|
||||||
return new Promise<void>(async (resolve, reject) => {
|
|
||||||
const onProgress = (data: ResearchStep) => {
|
|
||||||
console.log(data)
|
|
||||||
// 发送进度事件
|
|
||||||
event.node.res.write(`data: ${JSON.stringify(data)}\n\n`)
|
|
||||||
}
|
|
||||||
await deepResearch({
|
|
||||||
query: combinedQuery,
|
|
||||||
breadth,
|
|
||||||
depth,
|
|
||||||
onProgress,
|
|
||||||
})
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,19 +0,0 @@
|
|||||||
// This file is currently unused
|
|
||||||
import { generateFeedback } from '~/lib/feedback'
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const { query, numQuestions } = await readBody(event)
|
|
||||||
console.log({ query, numQuestions })
|
|
||||||
const feedback = generateFeedback({
|
|
||||||
query,
|
|
||||||
numQuestions,
|
|
||||||
})
|
|
||||||
|
|
||||||
return feedback.toDataStreamResponse({
|
|
||||||
sendUsage: true,
|
|
||||||
getErrorMessage(error) {
|
|
||||||
console.error('Error generating feedback:', error)
|
|
||||||
return 'Error generating feedback'
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
16
test.ts
16
test.ts
@ -1,16 +0,0 @@
|
|||||||
import { generateFeedback } from './lib/feedback'
|
|
||||||
|
|
||||||
async function test() {
|
|
||||||
console.log('test')
|
|
||||||
const feedback = generateFeedback({
|
|
||||||
query: 'Why is the sky blue?',
|
|
||||||
})
|
|
||||||
console.log('feedback', feedback)
|
|
||||||
|
|
||||||
for await (const partial of feedback.textStream) {
|
|
||||||
console.log(partial)
|
|
||||||
}
|
|
||||||
console.log('end', feedback)
|
|
||||||
}
|
|
||||||
|
|
||||||
test()
|
|
Reference in New Issue
Block a user