Skip to content

Commit e7296df

Browse files
committed
feat: support reasoning models like DeepSeek R1
1 parent 9352759 commit e7296df

17 files changed

+549
-171
lines changed

.dockerignore

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Nuxt dev/build outputs
2+
.output
3+
.data
4+
.nuxt
5+
.nitro
6+
.cache
7+
dist
8+
9+
# Node dependencies
10+
node_modules
11+
12+
# Logs
13+
logs
14+
*.log
15+
16+
# Misc
17+
.DS_Store
18+
.fleet
19+
.idea
20+
21+
# Local env files
22+
.env
23+
.env.*
24+
!.env.example
25+
26+
# TypeScript
27+
*.tsbuildinfo
28+
29+
# Nuxt generate
30+
.generate

README.md

+21-3
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ Features:
88

99
- 🚀 **Safe & Secure**: Everything (config, API requests, ...) stays in your browser locally
1010
- 🕙 **Realtime feedback**: Stream AI responses and reflect on the UI in real-time
11-
- 🌳 **Search visualization**: Shows the research process using a tree structure
11+
- 🌳 **Search visualization**: Shows the research process using a tree structure. Supports searching in different languages
1212
- 📄 **Export as PDF**: Export the final research report as a PDF
13-
- 🌐 **Search in different languages**: Useful when you want to get search results in a different language
1413
- 🤖 **Supports more models**: Uses plain prompts instead of newer, less widely supported features like Structured Outputs. This ensures to work with more providers that haven't caught up with the latest OpenAI capabilities.
14+
- 🐳 **Docker support**: Deploy in your environment in one-line command
1515

1616
Currently available providers:
1717

@@ -20,7 +20,25 @@ Currently available providers:
2020

2121
Please give a 🌟 Star if you like this project!
2222

23-
<video src="https://github.com/user-attachments/assets/2f5a6f9c-18d1-4d40-9822-2de260d55dab" controls></video>
23+
<video width="500" src="https://github.com/user-attachments/assets/2f5a6f9c-18d1-4d40-9822-2de260d55dab" controls></video>
24+
25+
## Recent updates
26+
27+
25/02/14
28+
29+
- Supported reasoning models like DeepSeek R1
30+
- Improved compatibility with more models & error handling
31+
32+
25/02/13
33+
34+
- Significantly reduced bundle size
35+
- Supported searching in different languages
36+
- Added Docker support
37+
- Fixed "export as PDF" issues
38+
39+
25/02/12
40+
- Added Chinese translation. The models will respond in the user's language.
41+
- Various fixes
2442

2543
## How to use
2644

README_zh.md

+19-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,25 @@
1616

1717
喜欢本项目请点 ⭐ 收藏!
1818

19-
<video src="https://github.com/user-attachments/assets/2f5a6f9c-18d1-4d40-9822-2de260d55dab" controls></video>
19+
<video width="500" src="https://github.com/user-attachments/assets/2f5a6f9c-18d1-4d40-9822-2de260d55dab" controls></video>
20+
21+
## 最近更新
22+
23+
25/02/14
24+
25+
- 支持 DeepSeek R1 等思维链模型
26+
- 改进了模型兼容性,改进异常处理
27+
28+
25/02/13
29+
30+
- 大幅缩减了网页体积
31+
- 支持配置搜索时使用的语言
32+
- 支持 Docker 部署
33+
- 修复“导出 PDF”不可用的问题
34+
35+
25/02/12
36+
- 添加中文支持。模型会自动使用用户的语言回答了。
37+
- 修复一些 bug
2038

2139
## 使用指南
2240

components/DeepResearch.vue

+35-4
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@
3838
}
3939
4040
switch (step.type) {
41+
case 'generating_query_reasoning': {
42+
if (node) {
43+
node.generateQueriesReasoning =
44+
(node.generateQueriesReasoning ?? '') + step.delta
45+
}
46+
break
47+
}
48+
4149
case 'generating_query': {
4250
if (!node) {
4351
// 创建新节点
@@ -86,10 +94,17 @@
8694
break
8795
}
8896
97+
case 'processing_serach_result_reasoning': {
98+
if (node) {
99+
node.generateLearningsReasoning =
100+
(node.generateLearningsReasoning ?? '') + step.delta
101+
}
102+
break
103+
}
104+
89105
case 'processing_serach_result': {
90106
if (node) {
91107
node.learnings = step.result.learnings || []
92-
node.followUpQuestions = step.result.followUpQuestions || []
93108
}
94109
break
95110
}
@@ -157,6 +172,8 @@
157172
selectedNode.value = undefined
158173
searchResults.value = {}
159174
isLoading.value = true
175+
// Clear the root node's reasoning content
176+
tree.value.generateQueriesReasoning = ''
160177
try {
161178
const searchLanguage = config.value.webSearch.searchLanguage
162179
? t('language', {}, { locale: config.value.webSearch.searchLanguage })
@@ -211,20 +228,28 @@
211228
{{ selectedNode.label ?? $t('webBrowsing.generating') }}
212229
</h2>
213230

231+
<!-- Set loading default to true, because currently don't know how to handle it otherwise -->
232+
<ReasoningAccordion
233+
v-model="selectedNode.generateQueriesReasoning"
234+
loading
235+
/>
236+
237+
<!-- Research goal -->
238+
<h3 class="text-lg font-semibold mt-2">
239+
{{ t('webBrowsing.researchGoal') }}
240+
</h3>
214241
<!-- Root node has no additional information -->
215242
<p v-if="selectedNode.id === '0'">
216243
{{ t('webBrowsing.startNode.description') }}
217244
</p>
218245
<template v-else>
219-
<h3 class="text-lg font-semibold mt-2">
220-
{{ t('webBrowsing.researchGoal') }}
221-
</h3>
222246
<p
223247
v-if="selectedNode.researchGoal"
224248
class="prose max-w-none"
225249
v-html="marked(selectedNode.researchGoal, { gfm: true })"
226250
/>
227251

252+
<!-- Visited URLs -->
228253
<h3 class="text-lg font-semibold mt-2">
229254
{{ t('webBrowsing.visitedUrls') }}
230255
</h3>
@@ -245,9 +270,15 @@
245270
</li>
246271
</ul>
247272

273+
<!-- Learnings -->
248274
<h3 class="text-lg font-semibold mt-2">
249275
{{ t('webBrowsing.learnings') }}
250276
</h3>
277+
278+
<ReasoningAccordion
279+
v-model="selectedNode.generateQueriesReasoning"
280+
loading
281+
/>
251282
<p
252283
v-for="(learning, index) in selectedNode.learnings"
253284
class="prose max-w-none"

components/ReasoningAccordion.vue

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<!-- Shows an accordion for reasoning (CoT) content. The accordion is default invisible,
2+
until modelValue's length > 0 -->
3+
4+
<script setup lang="ts">
5+
const props = defineProps<{
6+
loading?: boolean
7+
}>()
8+
9+
const modelValue = defineModel<string>()
10+
const items = computed(() => [
11+
{
12+
icon: 'i-lucide-brain',
13+
content: modelValue.value,
14+
},
15+
])
16+
const currentOpen = ref('0')
17+
18+
watchEffect(() => {
19+
if (props.loading) {
20+
currentOpen.value = '0'
21+
} else {
22+
currentOpen.value = '-1'
23+
}
24+
})
25+
</script>
26+
27+
<template>
28+
<UAccordion
29+
v-if="modelValue"
30+
v-model="currentOpen"
31+
class="border border-gray-200 dark:border-gray-800 rounded-lg px-3 sm:px-4"
32+
:items="items"
33+
:loading="loading"
34+
>
35+
<template #leading="{ item }">
36+
<div
37+
:class="[
38+
loading && 'animate-pulse',
39+
'flex items-center gap-2 text-(--ui-primary)',
40+
]"
41+
>
42+
<UIcon :name="item.icon" size="20" />
43+
{{ loading ? $t('modelThinking') : $t('modelThinkingComplete') }}
44+
</div>
45+
</template>
46+
<template #content="{ item }">
47+
<p class="text-sm text-gray-500 whitespace-pre-wrap mb-4">
48+
{{ item.content }}
49+
</p>
50+
</template>
51+
</UAccordion>
52+
</template>

components/ResearchFeedback.vue

+26-11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
}>()
1616
1717
const { t, locale } = useI18n()
18+
const reasoningContent = ref('')
1819
const feedback = ref<ResearchFeedbackResult[]>([])
1920
2021
const isLoading = ref(false)
@@ -39,17 +40,27 @@
3940
numQuestions,
4041
language: t('language', {}, { locale: locale.value }),
4142
})) {
42-
const questions = f.questions!.filter((s) => typeof s === 'string')
43-
// Incrementally update modelValue
44-
for (let i = 0; i < questions.length; i += 1) {
45-
if (feedback.value[i]) {
46-
feedback.value[i].assistantQuestion = questions[i]
47-
} else {
48-
feedback.value.push({
49-
assistantQuestion: questions[i],
50-
userAnswer: '',
51-
})
43+
if (f.type === 'reasoning') {
44+
reasoningContent.value += f.delta
45+
} else if (f.type === 'error') {
46+
error.value = f.message
47+
} else if (f.type === 'object') {
48+
const questions = f.value.questions!.filter(
49+
(s) => typeof s === 'string',
50+
)
51+
// Incrementally update modelValue
52+
for (let i = 0; i < questions.length; i += 1) {
53+
if (feedback.value[i]) {
54+
feedback.value[i].assistantQuestion = questions[i]
55+
} else {
56+
feedback.value.push({
57+
assistantQuestion: questions[i],
58+
userAnswer: '',
59+
})
60+
}
5261
}
62+
} else if (f.type === 'bad-end') {
63+
error.value = t('invalidStructuredOutput')
5364
}
5465
}
5566
} catch (e: any) {
@@ -66,6 +77,7 @@
6677
function clear() {
6778
feedback.value = []
6879
error.value = ''
80+
reasoningContent.value = ''
6981
}
7082
7183
defineExpose({
@@ -85,13 +97,16 @@
8597
</template>
8698

8799
<div class="flex flex-col gap-2">
88-
<div v-if="!feedback.length && !error">
100+
<div v-if="!feedback.length && !reasoningContent && !error">
89101
{{ $t('modelFeedback.waiting') }}
90102
</div>
91103
<template v-else>
92104
<div v-if="error" class="text-red-500 whitespace-pre-wrap">
93105
{{ error }}
94106
</div>
107+
108+
<ReasoningAccordion v-model="reasoningContent" :loading="isLoading" />
109+
95110
<div
96111
v-for="(feedback, index) in feedback"
97112
class="flex flex-col gap-2"

components/ResearchForm.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
block
103103
@click="handleSubmit"
104104
>
105-
{{ isLoadingFeedback ? 'Researching...' : $t('researchTopic.start') }}
105+
{{ isLoadingFeedback ? $t('researchTopic.researching') : $t('researchTopic.start') }}
106106
</UButton>
107107
</template>
108108
</UCard>

components/ResearchReport.vue

+28-12
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
const error = ref('')
1717
const loading = ref(false)
1818
const loadingExportPdf = ref(false)
19+
const reasoningContent = ref('')
1920
const reportContent = ref('')
2021
const reportHtml = computed(() =>
2122
marked(reportContent.value, { silent: true, gfm: true, breaks: true }),
@@ -29,9 +30,20 @@
2930
loading.value = true
3031
error.value = ''
3132
reportContent.value = ''
33+
reasoningContent.value = ''
3234
try {
33-
for await (const chunk of writeFinalReport(params).textStream) {
34-
reportContent.value += chunk
35+
for await (const chunk of writeFinalReport(params).fullStream) {
36+
if (chunk.type === 'reasoning') {
37+
reasoningContent.value += chunk.textDelta
38+
} else if (chunk.type === 'text-delta') {
39+
reportContent.value += chunk.textDelta
40+
} else if (chunk.type === 'error') {
41+
error.value = t('researchReport.error', [
42+
chunk.error instanceof Error
43+
? chunk.error.message
44+
: String(chunk.error),
45+
])
46+
}
3547
}
3648
reportContent.value += `\n\n## ${t(
3749
'researchReport.sources',
@@ -158,21 +170,25 @@
158170
</div>
159171
</template>
160172

173+
<div v-if="error" class="text-red-500">{{ error }}</div>
174+
175+
<ReasoningAccordion
176+
v-if="reasoningContent"
177+
v-model="reasoningContent"
178+
class="mb-4"
179+
:loading="loading"
180+
/>
181+
161182
<div
162183
v-if="reportContent"
163184
id="report-content"
164185
class="prose prose-sm max-w-none p-6 bg-gray-50 dark:bg-gray-800 dark:text-white rounded-lg shadow"
165186
v-html="reportHtml"
166187
/>
167-
<template v-else>
168-
<div v-if="error" class="text-red-500">{{ error }}</div>
169-
<div v-else>
170-
{{
171-
loading
172-
? $t('researchReport.generating')
173-
: $t('researchReport.waiting')
174-
}}
175-
</div>
176-
</template>
188+
<div v-else>
189+
{{
190+
loading ? $t('researchReport.generating') : $t('researchReport.waiting')
191+
}}
192+
</div>
177193
</UCard>
178194
</template>

0 commit comments

Comments
 (0)