-
+
@@ -251,64 +278,82 @@
{{ selectedNode.label ?? $t('webBrowsing.generating') }}
-
-
-
{{ t('webBrowsing.researchGoal') }}
-
+
{{ t('webBrowsing.startNode.description') }}
-
-
+
-
-
- {{ t('webBrowsing.visitedUrls') }}
-
-
- -
+
+ {{ t('webBrowsing.visitedUrls') }}
+
+
+ -
+
-
- {{ item.title || item.url }}
-
-
-
+ {{ item.title || item.url }}
+
+
+
+ -
-
-
- {{ t('webBrowsing.learnings') }}
+
+
+ {{ t('webBrowsing.learnings') }}
+
+
+
+
+ -
+
+
+
+
+
+ {{ t('webBrowsing.followUpQuestions') }}
+
-
diff --git a/components/DeepResearch/SearchFlow.vue b/components/DeepResearch/SearchFlow.vue
new file mode 100644
index 0000000..a5573f9
--- /dev/null
+++ b/components/DeepResearch/SearchFlow.vue
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/DeepResearch/SearchNode.vue b/components/DeepResearch/SearchNode.vue
new file mode 100644
index 0000000..7cacda6
--- /dev/null
+++ b/components/DeepResearch/SearchNode.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+ {{ data.title }}
+
+
+
+
diff --git a/components/Tree.vue b/components/Tree.vue
deleted file mode 100644
index f436ff7..0000000
--- a/components/Tree.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-
-
-
-
-
-
- {{ node.label }}
-
-
- -
-
-
-
-
-
diff --git a/composables/useNodeLayout.ts b/composables/useNodeLayout.ts
new file mode 100644
index 0000000..5e572be
--- /dev/null
+++ b/composables/useNodeLayout.ts
@@ -0,0 +1,62 @@
+import dagre from '@dagrejs/dagre'
+import { Position, useVueFlow, type Edge, type Node } from '@vue-flow/core'
+
+// Picked from https://vueflow.dev/examples/layout/animated.html
+export function useNodeLayout() {
+ const { findNode } = useVueFlow()
+
+ function layout(nodes: Node[], edges: Edge[]) {
+ // we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there
+ const dagreGraph = new dagre.graphlib.Graph()
+
+ dagreGraph.setDefaultEdgeLabel(() => ({}))
+
+ const isHorizontal = true
+ dagreGraph.setGraph({
+ rankdir: 'LR',
+ // distance between nodes at the same level
+ nodesep: 25,
+ // distance between levels
+ ranksep: 30,
+ })
+
+ for (const node of nodes) {
+ // if you need width+height of nodes for your layout, you can use the dimensions property of the internal node (`GraphNode` type)
+ const graphNode = findNode(node.id)
+
+ if (!graphNode) {
+ console.error(`Node with id ${node.id} not found in the graph`)
+ continue
+ }
+
+ dagreGraph.setNode(node.id, {
+ width: graphNode.dimensions.width || 100,
+ height: graphNode.dimensions.height || 50,
+ })
+ }
+
+ for (const edge of edges) {
+ dagreGraph.setEdge(edge.source, edge.target)
+ }
+
+ dagre.layout(dagreGraph, {
+ rankdir: 'LR',
+ nodesep: 25,
+ ranksep: 30,
+ })
+
+ // set nodes with updated positions
+ return nodes.map((node) => {
+ const nodeWithPosition = dagreGraph.node(node.id)
+
+ return {
+ ...node,
+ targetPosition: isHorizontal ? Position.Left : Position.Top,
+ sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
+ position: { x: nodeWithPosition.x, y: nodeWithPosition.y },
+ }
+ })
+ }
+
+ return { layout }
+}
diff --git a/i18n/en.json b/i18n/en.json
index e622cfe..b044a63 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -76,7 +76,8 @@
"learnings": "Learnings",
"generating": "Generating...",
"nodeFailed": "Search failed",
- "nodeFailedToast": "Search node \"{label}\" failed"
+ "nodeFailedToast": "Search node \"{label}\" failed",
+ "followUpQuestions": "Follow-up Questions"
},
"researchReport": {
"title": "4. Research Report",
diff --git a/i18n/zh.json b/i18n/zh.json
index 53b6808..778226b 100644
--- a/i18n/zh.json
+++ b/i18n/zh.json
@@ -76,7 +76,8 @@
"learnings": "结论",
"generating": "生成中...",
"nodeFailed": "搜索失败",
- "nodeFailedToast": "搜索步骤 “{label}” 失败"
+ "nodeFailedToast": "搜索步骤 “{label}” 失败",
+ "followUpQuestions": "后续问题"
},
"researchReport": {
"title": "4. 研究报告",
diff --git a/lib/deep-research.ts b/lib/deep-research.ts
index c8ace10..e56e007 100644
--- a/lib/deep-research.ts
+++ b/lib/deep-research.ts
@@ -27,7 +27,12 @@ export type ProcessedSearchResult = z.infer
export type PartialProcessedSearchResult = DeepPartial
export type ResearchStep =
- | { type: 'generating_query'; result: PartialSearchQuery; nodeId: string }
+ | {
+ type: 'generating_query'
+ result: PartialSearchQuery
+ nodeId: string
+ parentNodeId?: string
+ }
| { type: 'generating_query_reasoning'; delta: string; nodeId: string }
| {
type: 'generated_query'
@@ -49,9 +54,8 @@ export type ResearchStep =
nodeId: string
}
| {
- type: 'processed_search_result'
- query: string
- result: ProcessedSearchResult
+ type: 'node_complete'
+ result?: ProcessedSearchResult
nodeId: string
}
| { type: 'error'; message: string; nodeId: string }
@@ -239,6 +243,12 @@ export async function deepResearch({
const { t } = useNuxtApp().$i18n
const language = t('language', {}, { locale: languageCode })
+ onProgress({
+ type: 'generating_query',
+ nodeId,
+ result: {},
+ })
+
try {
const searchQueriesResult = generateSearchQueries({
query,
@@ -268,9 +278,11 @@ export async function deepResearch({
type: 'generating_query',
result: searchQueries[i],
nodeId: childNodeId(nodeId, i),
+ parentNodeId: nodeId,
})
}
} else if (chunk.type === 'reasoning') {
+ // Reasoning part goes to the parent node
onProgress({
type: 'generating_query_reasoning',
delta: chunk.delta,
@@ -293,6 +305,11 @@ export async function deepResearch({
}
}
+ onProgress({
+ type: 'node_complete',
+ nodeId,
+ })
+
for (let i = 0; i < searchQueries.length; i++) {
onProgress({
type: 'generated_query',
@@ -318,13 +335,13 @@ export async function deepResearch({
nodeId: childNodeId(nodeId, i),
})
try {
- // Use Tavily to search the web
+ // search the web
const results = await useWebSearch()(searchQuery.query, {
maxResults: 5,
lang: languageCode,
})
console.log(
- `Ran ${searchQuery.query}, found ${results.length} contents`,
+ `[DeepResearch] Searched "${searchQuery.query}", found ${results.length} contents`,
)
// Collect URLs from this search
@@ -393,12 +410,11 @@ export async function deepResearch({
const nextDepth = currentDepth + 1
onProgress({
- type: 'processed_search_result',
+ type: 'node_complete',
result: {
learnings: allLearnings,
followUpQuestions: searchResult.followUpQuestions ?? [],
},
- query: searchQuery.query,
nodeId: childNodeId(nodeId, i),
})
diff --git a/package.json b/package.json
index 86603b5..6d7532c 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"@ai-sdk/openai": "^1.1.11",
"@ai-sdk/ui-utils": "^1.1.14",
"@ai-sdk/vue": "^1.1.17",
+ "@dagrejs/dagre": "^1.1.4",
"@iconify-json/lucide": "^1.2.26",
"@mendable/firecrawl-js": "^1.17.0",
"@nuxt/ui": "3.0.0-alpha.12",
@@ -25,6 +26,9 @@
"@pinia/nuxt": "^0.10.1",
"@tailwindcss/typography": "^0.5.16",
"@tavily/core": "^0.3.1",
+ "@vue-flow/background": "^1.3.2",
+ "@vue-flow/controls": "^1.1.2",
+ "@vue-flow/core": "^1.42.1",
"ai": "^4.1.41",
"js-tiktoken": "^1.0.19",
"jspdf": "^2.5.2",
diff --git a/pages/index.vue b/pages/index.vue
index 4551e11..6ac6c54 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -52,7 +52,7 @@