diff --git a/README.md b/README.md index a61df09..eb9076a 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,15 @@ Currently available providers: Please give a 🌟 Star if you like this project! - + ## Recent updates +25/02/16 + +- Refactored the search visualization using VueFlow +- Style & bug fixes + 25/02/15 - Added AI providers DeepSeek, OpenRouter and Ollama; Added web search provider Firecrawl @@ -43,10 +48,6 @@ Please give a 🌟 Star if you like this project! - Added Docker support - Fixed "export as PDF" issues -25/02/12 - -- Added Chinese translation. The models will respond in the user's language. -- Various fixes ## How to use diff --git a/README_zh.md b/README_zh.md index b037ebf..bd33b2d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -14,10 +14,15 @@ - AI 服务:OpenAPI 每月 1000 次免费搜索)、Firecrawl -喜欢本项目请点 ⭐ 收藏! +喜欢本项目请点 ⭐ 收藏! ## 近期更新 +25/02/16 + +- 使用 VueFlow 重构了搜索可视化功能 +- 一些样式和 bug 修复 + 25/02/15 - AI 提供商支持 DeepSeek,OpenRouter 和 Ollama,联网搜素支持 Firecrawl @@ -37,11 +42,6 @@ - 支持 Docker 部署 - 修复“导出 PDF”不可用的问题 -25/02/12 - -- 添加中文支持。模型会自动使用用户的语言回答了。 -- 修复一些 bug - ## 使用指南 在线演示:https://deep-research.ataw.top diff --git a/components/DeepResearch.vue b/components/DeepResearch/DeepResearch.vue similarity index 51% rename from components/DeepResearch.vue rename to components/DeepResearch/DeepResearch.vue index 809cfad..46ec20b 100644 --- a/components/DeepResearch.vue +++ b/components/DeepResearch/DeepResearch.vue @@ -4,13 +4,33 @@ type PartialProcessedSearchResult, type ResearchStep, } from '~/lib/deep-research' - import type { TreeNode } from './Tree.vue' import { marked } from 'marked' import { feedbackInjectionKey, formInjectionKey, researchResultInjectionKey, } from '~/constants/injection-keys' + import Flow from './SearchFlow.vue' + import SearchFlow from './SearchFlow.vue' + + export type DeepResearchNodeStatus = Exclude + + export type DeepResearchNode = { + id: string + /** Label, represents the search query. Generated from parent node. */ + label: string + /** The research goal of this node. Generated from parent node. */ + researchGoal?: string + /** Reasoning content when generating queries for the next iteration. */ + generateQueriesReasoning?: string + /** Reasoning content when generating learnings for this iteration. */ + generateLearningsReasoning?: string + searchResults?: WebSearchResult[] + /** Learnings from search results */ + learnings?: string[] + status?: DeepResearchNodeStatus + error?: string + } const emit = defineEmits<{ (e: 'complete'): void @@ -20,29 +40,43 @@ const { t, locale } = useI18n() const { config } = storeToRefs(useConfigStore()) - const tree = ref({ - id: '0', - label: t('webBrowsing.startNode.label'), - children: [], - }) - const selectedNode = ref() + const flowRef = ref>() + const rootNode: DeepResearchNode = { id: '0', label: 'Start' } + // The complete search data. + // There's another tree stored in SearchNode.vue, with only basic data (id, status, ...) + const nodes = ref([{ ...rootNode }]) + const selectedNodeId = ref() const searchResults = ref>({}) const isLoading = ref(false) + const selectedNode = computed(() => { + if (selectedNodeId.value) { + return nodes.value.find((n) => n.id === selectedNodeId.value) + } + }) + // Inject global data from index.vue const form = inject(formInjectionKey)! const feedback = inject(feedbackInjectionKey)! const completeResult = inject(researchResultInjectionKey)! function handleResearchProgress(step: ResearchStep) { - let node: TreeNode | null = null + let node: DeepResearchNode | undefined let nodeId = '' if (step.type !== 'complete') { nodeId = step.nodeId - node = findNode(tree.value, step.nodeId) - if (node) { + node = nodes.value.find((n) => n.id === nodeId) + if (node && node.status !== step.type) { + // FIXME: currently `node_complete` is always triggered last, + // so error is possibly overridden + if (node.status === 'error') { + return + } node.status = step.type + flowRef.value?.updateNode(nodeId, { + status: step.type, + }) } } @@ -57,31 +91,31 @@ case 'generating_query': { if (!node) { - // 创建新节点 node = { id: nodeId, - label: '', + label: step.result.query ?? '', researchGoal: '', learnings: [], - children: [], } - const parentNodeId = getParentNodeId(nodeId) - // 如果是根节点的直接子节点 - if (parentNodeId === '0') { - tree.value.children.push(node) - } else { - // 找到父节点并添加 - const parentNode = findNode( - tree.value, - getParentNodeId(step.nodeId), - ) - if (parentNode) { - parentNode.children.push(node) - } + const parentNodeId = step.parentNodeId + nodes.value.push(node) + flowRef.value?.addNode( + nodeId, + { + title: node.label, + status: node.status, + }, + parentNodeId, + ) + } else { + if (node.label !== step.result.query) { + flowRef.value?.updateNode(nodeId, { + title: step.result.query ?? '', + }) } } - // 更新节点的查询内容 - if (step.result) { + // Update the node + if (!isRootNode(node)) { node.label = step.result.query ?? '' node.researchGoal = step.result.researchGoal } @@ -121,12 +155,12 @@ break } - case 'processed_search_result': { + case 'node_complete': { console.log( `[DeepResearch] node ${nodeId} processed_search_result:`, step, ) - if (node) { + if (node && step.result) { node.learnings = step.result.learnings searchResults.value[nodeId] = step.result } @@ -134,7 +168,11 @@ } case 'error': - console.error(`[DeepResearch] node ${nodeId} error:`, step.message) + console.error( + `[DeepResearch] node ${nodeId} error:`, + node, + step.message, + ) node!.error = step.message toast.add({ title: t('webBrowsing.nodeFailedToast', { @@ -158,45 +196,30 @@ } } - // 辅助函数:根据节点ID查找节点 - function findNode(root: TreeNode, targetId: string): TreeNode | null { - if (!targetId) return null - if (root.id === targetId) { - return root - } - for (const child of root.children) { - const found = findNode(child, targetId) - if (found) { - return found - } - } - return null + function isRootNode(node: DeepResearchNode) { + return node.id === '0' } - function selectNode(node: TreeNode) { - if (selectedNode.value?.id === node.id) { - selectedNode.value = undefined + function selectNode(nodeId: string) { + if (selectedNodeId.value === nodeId) { + selectedNodeId.value = undefined } else { - selectedNode.value = node + selectedNodeId.value = nodeId } } - // 辅助函数:获取节点的父节点 ID - function getParentNodeId(nodeId: string): string { - const parts = nodeId.split('-') - parts.pop() - return parts.join('-') - } - async function startResearch() { if (!form.value.query || !form.value.breadth || !form.value.depth) return - tree.value.children = [] - selectedNode.value = undefined + nodes.value = [{ ...rootNode }] + selectedNodeId.value = undefined searchResults.value = {} isLoading.value = true - // Clear the root node's reasoning content - tree.value.generateQueriesReasoning = '' + flowRef.value?.clearNodes() + + // Wait after the flow is cleared + await new Promise((r) => requestAnimationFrame(r)) + try { const searchLanguage = config.value.webSearch.searchLanguage ? t('language', {}, { locale: config.value.webSearch.searchLanguage }) @@ -234,7 +257,11 @@
- +
@@ -251,64 +278,82 @@ {{ selectedNode.label ?? $t('webBrowsing.generating') }} - - -

{{ t('webBrowsing.researchGoal') }}

-

+

{{ t('webBrowsing.startNode.description') }}

-