feat: i18n support

This commit is contained in:
AnotiaWang
2025-02-12 15:20:41 +08:00
parent 272f417e80
commit 6d6124d4ef
21 changed files with 1226 additions and 294 deletions

View File

@ -1,7 +1,23 @@
<script setup lang="ts">
const { config } = useConfigStore()
const { t } = useI18n()
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({
show() {
showModal.value = true
@ -11,41 +27,35 @@
<template>
<div>
<UModal v-model:open="showModal" title="Settings">
<UModal v-model:open="showModal" :title="$t('settings.title')">
<UButton icon="i-lucide-settings" />
<template #body>
<div class="space-y-2">
<div class="flex flex-col gap-y-2">
<!-- AI provider -->
<h3 class="font-bold">AI Provider</h3>
<UFormField label="Provider">
<template #help>
Currently only OpenAI compatible providers are supported, e.g.
Gemini, Together AI, SiliconCloud, ...
<h3 class="font-bold">{{ $t('settings.ai.provider') }}</h3>
<UFormField>
<template v-if="selectedAiProvider" #help>
{{ selectedAiProvider.help }}
</template>
<USelect
v-model="config.ai.provider"
:items="[
{ label: 'OpenAI Compatible', value: 'openai-compatible' },
]"
/>
<USelect v-model="config.ai.provider" :items="aiProviderOptions" />
</UFormField>
<div
v-if="config.ai.provider === 'openai-compatible'"
class="space-y-2"
>
<UFormField label="API Key" required>
<UFormField :label="$t('settings.ai.apiKey')" required>
<PasswordInput
v-model="config.ai.apiKey"
class="w-full"
placeholder="API Key"
:placeholder="$t('settings.ai.apiKey')"
/>
</UFormField>
<UFormField label="API Base URL">
<UFormField :label="$t('settings.ai.apiBase')">
<UInput
v-model="config.ai.apiBase"
class="w-full"
placeholder="https://api.openai.com/v1"
:placeholder="selectedAiProvider?.apiBasePlaceholder"
/>
</UFormField>
<UFormField label="Model" required>
@ -57,34 +67,33 @@
</UFormField>
</div>
<USeparator class="my-4" />
<USeparator class="my-2" />
<!-- Web search provider -->
<h3 class="font-bold">Web Search Provider</h3>
<UFormField label="Provider">
<h3 class="font-bold"> {{ $t('settings.webSearch.provider') }} </h3>
<UFormField>
<template #help>
Tavily is similar to Firecrawl, but with more free quota (1000
credits / month). Get one API key at
<UButton
class="!p-0"
to="https://app.tavily.com/home"
target="_blank"
variant="link"
>
app.tavily.com
</UButton>
.
<i18n-t keypath="settings.webSearch.providerHelp" tag="p">
<UButton
class="!p-0"
to="https://app.tavily.com/home"
target="_blank"
variant="link"
>
app.tavily.com
</UButton>
</i18n-t>
</template>
<USelect
v-model="config.webSearch.provider"
:items="[{ label: 'Tavily', value: 'tavily' }]"
/>
</UFormField>
<UFormField label="API Key" required>
<UFormField :label="$t('settings.webSearch.apiKey')" required>
<PasswordInput
v-model="config.webSearch.apiKey"
class="w-full"
placeholder="API Key"
:placeholder="$t('settings.webSearch.apiKey')"
/>
</UFormField>
</div>
@ -92,14 +101,14 @@
<template #footer>
<div class="flex items-center justify-between gap-2 w-full">
<p class="text-sm text-gray-500">
Settings are stored locally in your browser.
{{ $t('settings.disclaimer') }}
</p>
<UButton
color="primary"
icon="i-lucide-check"
@click="showModal = false"
>
Save
{{ $t('settings.save') }}
</UButton>
</div>
</template>

View File

@ -6,14 +6,16 @@
type ResearchStep,
} from '~/lib/deep-research'
import type { TreeNode } from './Tree.vue'
import { marked } from 'marked'
const { t } = useI18n()
const emit = defineEmits<{
(e: 'complete', results: ResearchResult): void
}>()
const tree = ref<TreeNode>({
id: '0',
label: 'Start',
label: t('webBrowsing.startNode.label'),
children: [],
})
const selectedNode = ref<TreeNode>()
@ -38,8 +40,8 @@
// 创建新节点
node = {
id: nodeId,
label: 'Generating...',
researchGoal: 'Generating research goal...',
label: t('webBrowsing.generating'),
researchGoal: t('webBrowsing.generating'),
learnings: [],
children: [],
}
@ -60,7 +62,7 @@
}
// 更新节点的查询内容
if (step.result) {
node.label = step.result.query ?? 'Generating...'
node.label = step.result.query ?? t('webBrowsing.generating')
node.researchGoal = step.result.researchGoal
}
break
@ -166,12 +168,11 @@
<template>
<UCard>
<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">
The AI will then search the web based on our research goal, and iterate
until the depth is reached.
{{ t('webBrowsing.description') }}
<br />
Click a child node to view details.
{{ t('webBrowsing.clickToView') }}
</p>
</template>
<div class="flex flex-col">
@ -179,31 +180,44 @@
<Tree :node="tree" :selected-node="selectedNode" @select="selectNode" />
</div>
<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>
<!-- Root node has no additional information -->
<p v-if="selectedNode.id === '0'">
This is the beginning of your deep research journey!
{{ t('webBrowsing.startNode.description') }}
</p>
<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>
<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">
<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>
</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">
<li
v-for="(learning, index) in selectedNode.learnings"
:key="index"
>{{ learning }}</li
>
v-html="marked(learning)"
></li>
</ul>
</template>
</div>

View File

@ -6,6 +6,5 @@
to="https://github.com/AnotiaWang/deep-research-web-ui"
target="_blank"
>
GitHub
</UButton>
</template>

View 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>

View File

@ -14,6 +14,7 @@
(e: 'submit', feedback: ResearchFeedbackResult[]): void
}>()
const { t } = useI18n()
const feedback = ref<ResearchFeedbackResult[]>([])
const isLoading = ref(false)
@ -52,7 +53,7 @@
}
} catch (e: any) {
console.error('Error getting feedback:', e)
error.value = e.message
error.value = t('modelFeedback.error', [e.message])
} finally {
isLoading.value = false
}
@ -73,16 +74,15 @@
<template>
<UCard>
<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">
The AI will ask you some follow up questions to help you clarify the
research direction.
{{ $t('modelFeedback.description') }}
</p>
</template>
<div class="flex flex-col gap-2">
<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>
<div v-if="error" class="text-red-500">{{ error }}</div>
<div
@ -90,7 +90,7 @@
class="flex flex-col gap-2"
:key="index"
>
Assistant: {{ feedback.assistantQuestion }}
{{ feedback.assistantQuestion }}
<UInput v-model="feedback.userAnswer" />
</div>
</template>
@ -101,7 +101,7 @@
block
@click="$emit('submit', feedback)"
>
Submit Answer
{{ $t('modelFeedback.submit') }}
</UButton>
</div>
</UCard>

View File

@ -39,22 +39,24 @@
<template>
<UCard>
<template #header>
<h2 class="font-bold">1. Research Topic</h2>
<h2 class="font-bold">{{ $t('researchTopic.title') }}</h2>
</template>
<div class="flex flex-col gap-2">
<UFormField label="Research Topic" required>
<UFormField :label="$t('researchTopic.title')" required>
<UTextarea
class="w-full"
v-model="form.query"
:rows="3"
placeholder="Enter whatever you want to research..."
:placeholder="$t('researchTopic.placeholder')"
required
/>
</UFormField>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<UFormField label="Number of Questions" required>
<template #help> Number of questions for you to clarify. </template>
<UFormField :label="$t('researchTopic.numOfQuestions')" required>
<template #help>
{{ $t('researchTopic.numOfQuestionsHelp') }}
</template>
<UInput
v-model="form.numQuestions"
class="w-full"
@ -65,8 +67,8 @@
/>
</UFormField>
<UFormField label="Depth" required>
<template #help> How deep you want to dig. </template>
<UFormField :label="$t('researchTopic.depth')" required>
<template #help>{{ $t('researchTopic.depthHelp') }}</template>
<UInput
v-model="form.depth"
class="w-full"
@ -77,8 +79,8 @@
/>
</UFormField>
<UFormField label="Breadth" required>
<template #help> Number of searches in each depth. </template>
<UFormField :label="$t('researchTopic.breadth')" required>
<template #help>{{ $t('researchTopic.breadthHelp') }}</template>
<UInput
v-model="form.breadth"
class="w-full"
@ -100,7 +102,7 @@
block
@click="handleSubmit"
>
{{ isLoadingFeedback ? 'Researching...' : 'Start Research' }}
{{ isLoadingFeedback ? 'Researching...' : $t('researchTopic.start') }}
</UButton>
</template>
</UCard>

View File

@ -9,12 +9,13 @@
visitedUrls: string[]
}
const { t } = useI18n()
const error = ref('')
const loading = ref(false)
const loadingExportPdf = ref(false)
const reportContent = ref('')
const reportHtml = computed(() =>
marked(reportContent.value, { gfm: true, silent: true }),
marked(reportContent.value, { silent: true, gfm: true, breaks: true }),
)
const isExportButtonDisabled = computed(
() => !reportContent.value || loading.value || loadingExportPdf.value,
@ -28,10 +29,12 @@
for await (const chunk of writeFinalReport(params).textStream) {
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) {
console.error(`Generate report failed`, e)
error.value = e.message
error.value = t('researchReport.error', [e.message])
} finally {
loading.value = false
}
@ -98,7 +101,7 @@
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h2 class="font-bold">4. Research Report</h2>
<h2 class="font-bold">{{ $t('researchReport.title') }}</h2>
<UButton
color="info"
variant="ghost"
@ -107,7 +110,7 @@
:loading="loadingExportPdf"
@click="exportToPdf"
>
Export PDF
{{ $t('researchReport.exportPdf') }}
</UButton>
</div>
</template>
@ -120,9 +123,13 @@
/>
<template v-else>
<div v-if="error" class="text-red-500">{{ error }}</div>
<div v-else
>{{ loading ? 'Generating report...' : 'Waiting for report..' }}.</div
>
<div v-else>
{{
loading
? $t('researchReport.generating')
: $t('researchReport.waiting')
}}
</div>
</template>
</UCard>
</template>

View File

@ -61,7 +61,7 @@
<div class="flex items-center gap-1">
<UIcon name="i-lucide-circle-dot" />
<UButton
:class="icon.pulse && 'animate-pulse'"
:class="['max-w-90 shrink-0', icon.pulse && 'animate-pulse']"
:icon="icon.name"
size="sm"
:color="selectedNode?.id === node.id ? 'primary' : 'info'"