feat: rewritten search visualization using VueFlow
This commit is contained in:
11
README.md
11
README.md
@ -20,10 +20,15 @@ Currently available providers:
|
|||||||
|
|
||||||
Please give a 🌟 Star if you like this project!
|
Please give a 🌟 Star if you like this project!
|
||||||
|
|
||||||
<video width="500" src="https://github.com/user-attachments/assets/2f5a6f9c-18d1-4d40-9822-2de260d55dab" controls></video>
|
<video width="500" src="https://github.com/user-attachments/assets/8f9baa43-a74e-4613-aebb-1bcc29a686f0" controls></video>
|
||||||
|
|
||||||
## Recent updates
|
## Recent updates
|
||||||
|
|
||||||
|
25/02/16
|
||||||
|
|
||||||
|
- Refactored the search visualization using VueFlow
|
||||||
|
- Style & bug fixes
|
||||||
|
|
||||||
25/02/15
|
25/02/15
|
||||||
|
|
||||||
- Added AI providers DeepSeek, OpenRouter and Ollama; Added web search provider Firecrawl
|
- 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
|
- Added Docker support
|
||||||
- Fixed "export as PDF" issues
|
- 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
|
## How to use
|
||||||
|
|
||||||
|
12
README_zh.md
12
README_zh.md
@ -14,10 +14,15 @@
|
|||||||
|
|
||||||
- AI 服务:OpenAPI 每月 1000 次免费搜索)、Firecrawl
|
- AI 服务:OpenAPI 每月 1000 次免费搜索)、Firecrawl
|
||||||
|
|
||||||
喜欢本项目请点 ⭐ 收藏! <video width="500" src="https://github.com/user-attachments/assets/2f5a6f9c-18d1-4d40-9822-2de260d55dab" controls></video>
|
喜欢本项目请点 ⭐ 收藏! <video width="500" src="https://github.com/user-attachments/assets/8f9baa43-a74e-4613-aebb-1bcc29a686f0" controls></video>
|
||||||
|
|
||||||
## 近期更新
|
## 近期更新
|
||||||
|
|
||||||
|
25/02/16
|
||||||
|
|
||||||
|
- 使用 VueFlow 重构了搜索可视化功能
|
||||||
|
- 一些样式和 bug 修复
|
||||||
|
|
||||||
25/02/15
|
25/02/15
|
||||||
|
|
||||||
- AI 提供商支持 DeepSeek,OpenRouter 和 Ollama,联网搜素支持 Firecrawl
|
- AI 提供商支持 DeepSeek,OpenRouter 和 Ollama,联网搜素支持 Firecrawl
|
||||||
@ -37,11 +42,6 @@
|
|||||||
- 支持 Docker 部署
|
- 支持 Docker 部署
|
||||||
- 修复“导出 PDF”不可用的问题
|
- 修复“导出 PDF”不可用的问题
|
||||||
|
|
||||||
25/02/12
|
|
||||||
|
|
||||||
- 添加中文支持。模型会自动使用用户的语言回答了。
|
|
||||||
- 修复一些 bug
|
|
||||||
|
|
||||||
## 使用指南
|
## 使用指南
|
||||||
|
|
||||||
在线演示:<a href="https://deep-research.ataw.top" target="_blank">https://deep-research.ataw.top</a>
|
在线演示:<a href="https://deep-research.ataw.top" target="_blank">https://deep-research.ataw.top</a>
|
||||||
|
@ -4,13 +4,33 @@
|
|||||||
type PartialProcessedSearchResult,
|
type PartialProcessedSearchResult,
|
||||||
type ResearchStep,
|
type ResearchStep,
|
||||||
} from '~/lib/deep-research'
|
} from '~/lib/deep-research'
|
||||||
import type { TreeNode } from './Tree.vue'
|
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
import {
|
import {
|
||||||
feedbackInjectionKey,
|
feedbackInjectionKey,
|
||||||
formInjectionKey,
|
formInjectionKey,
|
||||||
researchResultInjectionKey,
|
researchResultInjectionKey,
|
||||||
} from '~/constants/injection-keys'
|
} from '~/constants/injection-keys'
|
||||||
|
import Flow from './SearchFlow.vue'
|
||||||
|
import SearchFlow from './SearchFlow.vue'
|
||||||
|
|
||||||
|
export type DeepResearchNodeStatus = Exclude<ResearchStep['type'], 'complete'>
|
||||||
|
|
||||||
|
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<{
|
const emit = defineEmits<{
|
||||||
(e: 'complete'): void
|
(e: 'complete'): void
|
||||||
@ -20,29 +40,43 @@
|
|||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
const { config } = storeToRefs(useConfigStore())
|
const { config } = storeToRefs(useConfigStore())
|
||||||
|
|
||||||
const tree = ref<TreeNode>({
|
const flowRef = ref<InstanceType<typeof Flow>>()
|
||||||
id: '0',
|
const rootNode: DeepResearchNode = { id: '0', label: 'Start' }
|
||||||
label: t('webBrowsing.startNode.label'),
|
// The complete search data.
|
||||||
children: [],
|
// There's another tree stored in SearchNode.vue, with only basic data (id, status, ...)
|
||||||
})
|
const nodes = ref<DeepResearchNode[]>([{ ...rootNode }])
|
||||||
const selectedNode = ref<TreeNode>()
|
const selectedNodeId = ref<string>()
|
||||||
const searchResults = ref<Record<string, PartialProcessedSearchResult>>({})
|
const searchResults = ref<Record<string, PartialProcessedSearchResult>>({})
|
||||||
const isLoading = ref(false)
|
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
|
// Inject global data from index.vue
|
||||||
const form = inject(formInjectionKey)!
|
const form = inject(formInjectionKey)!
|
||||||
const feedback = inject(feedbackInjectionKey)!
|
const feedback = inject(feedbackInjectionKey)!
|
||||||
const completeResult = inject(researchResultInjectionKey)!
|
const completeResult = inject(researchResultInjectionKey)!
|
||||||
|
|
||||||
function handleResearchProgress(step: ResearchStep) {
|
function handleResearchProgress(step: ResearchStep) {
|
||||||
let node: TreeNode | null = null
|
let node: DeepResearchNode | undefined
|
||||||
let nodeId = ''
|
let nodeId = ''
|
||||||
|
|
||||||
if (step.type !== 'complete') {
|
if (step.type !== 'complete') {
|
||||||
nodeId = step.nodeId
|
nodeId = step.nodeId
|
||||||
node = findNode(tree.value, step.nodeId)
|
node = nodes.value.find((n) => n.id === nodeId)
|
||||||
if (node) {
|
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
|
node.status = step.type
|
||||||
|
flowRef.value?.updateNode(nodeId, {
|
||||||
|
status: step.type,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,31 +91,31 @@
|
|||||||
|
|
||||||
case 'generating_query': {
|
case 'generating_query': {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
// 创建新节点
|
|
||||||
node = {
|
node = {
|
||||||
id: nodeId,
|
id: nodeId,
|
||||||
label: '',
|
label: step.result.query ?? '',
|
||||||
researchGoal: '',
|
researchGoal: '',
|
||||||
learnings: [],
|
learnings: [],
|
||||||
children: [],
|
|
||||||
}
|
}
|
||||||
const parentNodeId = getParentNodeId(nodeId)
|
const parentNodeId = step.parentNodeId
|
||||||
// 如果是根节点的直接子节点
|
nodes.value.push(node)
|
||||||
if (parentNodeId === '0') {
|
flowRef.value?.addNode(
|
||||||
tree.value.children.push(node)
|
nodeId,
|
||||||
} else {
|
{
|
||||||
// 找到父节点并添加
|
title: node.label,
|
||||||
const parentNode = findNode(
|
status: node.status,
|
||||||
tree.value,
|
},
|
||||||
getParentNodeId(step.nodeId),
|
parentNodeId,
|
||||||
)
|
)
|
||||||
if (parentNode) {
|
} else {
|
||||||
parentNode.children.push(node)
|
if (node.label !== step.result.query) {
|
||||||
|
flowRef.value?.updateNode(nodeId, {
|
||||||
|
title: step.result.query ?? '',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// Update the node
|
||||||
// 更新节点的查询内容
|
if (!isRootNode(node)) {
|
||||||
if (step.result) {
|
|
||||||
node.label = step.result.query ?? ''
|
node.label = step.result.query ?? ''
|
||||||
node.researchGoal = step.result.researchGoal
|
node.researchGoal = step.result.researchGoal
|
||||||
}
|
}
|
||||||
@ -121,12 +155,12 @@
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'processed_search_result': {
|
case 'node_complete': {
|
||||||
console.log(
|
console.log(
|
||||||
`[DeepResearch] node ${nodeId} processed_search_result:`,
|
`[DeepResearch] node ${nodeId} processed_search_result:`,
|
||||||
step,
|
step,
|
||||||
)
|
)
|
||||||
if (node) {
|
if (node && step.result) {
|
||||||
node.learnings = step.result.learnings
|
node.learnings = step.result.learnings
|
||||||
searchResults.value[nodeId] = step.result
|
searchResults.value[nodeId] = step.result
|
||||||
}
|
}
|
||||||
@ -134,7 +168,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
console.error(`[DeepResearch] node ${nodeId} error:`, step.message)
|
console.error(
|
||||||
|
`[DeepResearch] node ${nodeId} error:`,
|
||||||
|
node,
|
||||||
|
step.message,
|
||||||
|
)
|
||||||
node!.error = step.message
|
node!.error = step.message
|
||||||
toast.add({
|
toast.add({
|
||||||
title: t('webBrowsing.nodeFailedToast', {
|
title: t('webBrowsing.nodeFailedToast', {
|
||||||
@ -158,45 +196,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数:根据节点ID查找节点
|
function isRootNode(node: DeepResearchNode) {
|
||||||
function findNode(root: TreeNode, targetId: string): TreeNode | null {
|
return node.id === '0'
|
||||||
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 selectNode(node: TreeNode) {
|
function selectNode(nodeId: string) {
|
||||||
if (selectedNode.value?.id === node.id) {
|
if (selectedNodeId.value === nodeId) {
|
||||||
selectedNode.value = undefined
|
selectedNodeId.value = undefined
|
||||||
} else {
|
} 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() {
|
async function startResearch() {
|
||||||
if (!form.value.query || !form.value.breadth || !form.value.depth) return
|
if (!form.value.query || !form.value.breadth || !form.value.depth) return
|
||||||
|
|
||||||
tree.value.children = []
|
nodes.value = [{ ...rootNode }]
|
||||||
selectedNode.value = undefined
|
selectedNodeId.value = undefined
|
||||||
searchResults.value = {}
|
searchResults.value = {}
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
// Clear the root node's reasoning content
|
flowRef.value?.clearNodes()
|
||||||
tree.value.generateQueriesReasoning = ''
|
|
||||||
|
// Wait after the flow is cleared
|
||||||
|
await new Promise((r) => requestAnimationFrame(r))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const searchLanguage = config.value.webSearch.searchLanguage
|
const searchLanguage = config.value.webSearch.searchLanguage
|
||||||
? t('language', {}, { locale: config.value.webSearch.searchLanguage })
|
? t('language', {}, { locale: config.value.webSearch.searchLanguage })
|
||||||
@ -234,7 +257,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="overflow-y-auto">
|
<div class="overflow-y-auto">
|
||||||
<Tree :node="tree" :selected-node="selectedNode" @select="selectNode" />
|
<SearchFlow
|
||||||
|
ref="flowRef"
|
||||||
|
:selected-node-id="selectedNodeId"
|
||||||
|
@node-click="selectNode"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedNode" class="p-4">
|
<div v-if="selectedNode" class="p-4">
|
||||||
<USeparator :label="$t('webBrowsing.nodeDetails')" />
|
<USeparator :label="$t('webBrowsing.nodeDetails')" />
|
||||||
@ -251,21 +278,14 @@
|
|||||||
{{ selectedNode.label ?? $t('webBrowsing.generating') }}
|
{{ selectedNode.label ?? $t('webBrowsing.generating') }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<!-- Set loading default to true, because currently don't know how to handle it otherwise -->
|
|
||||||
<ReasoningAccordion
|
|
||||||
v-model="selectedNode.generateQueriesReasoning"
|
|
||||||
loading
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Research goal -->
|
<!-- Research goal -->
|
||||||
<h3 class="text-lg font-semibold mt-2">
|
<h3 class="text-lg font-semibold mt-2">
|
||||||
{{ t('webBrowsing.researchGoal') }}
|
{{ t('webBrowsing.researchGoal') }}
|
||||||
</h3>
|
</h3>
|
||||||
<!-- Root node has no additional information -->
|
<!-- Root node has no additional information -->
|
||||||
<p v-if="selectedNode.id === '0'">
|
<p v-if="isRootNode(selectedNode)">
|
||||||
{{ t('webBrowsing.startNode.description') }}
|
{{ t('webBrowsing.startNode.description') }}
|
||||||
</p>
|
</p>
|
||||||
<template v-else>
|
|
||||||
<p
|
<p
|
||||||
v-if="selectedNode.researchGoal"
|
v-if="selectedNode.researchGoal"
|
||||||
class="prose max-w-none dark:prose-invert"
|
class="prose max-w-none dark:prose-invert"
|
||||||
@ -276,7 +296,10 @@
|
|||||||
<h3 class="text-lg font-semibold mt-2">
|
<h3 class="text-lg font-semibold mt-2">
|
||||||
{{ t('webBrowsing.visitedUrls') }}
|
{{ t('webBrowsing.visitedUrls') }}
|
||||||
</h3>
|
</h3>
|
||||||
<ul class="list-disc list-inside">
|
<ul
|
||||||
|
v-if="selectedNode.searchResults?.length"
|
||||||
|
class="list-disc list-inside"
|
||||||
|
>
|
||||||
<li
|
<li
|
||||||
v-for="(item, index) in selectedNode.searchResults"
|
v-for="(item, index) in selectedNode.searchResults"
|
||||||
class="whitespace-pre-wrap break-all"
|
class="whitespace-pre-wrap break-all"
|
||||||
@ -292,6 +315,7 @@
|
|||||||
</UButton>
|
</UButton>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<span v-else> - </span>
|
||||||
|
|
||||||
<!-- Learnings -->
|
<!-- Learnings -->
|
||||||
<h3 class="text-lg font-semibold mt-2">
|
<h3 class="text-lg font-semibold mt-2">
|
||||||
@ -299,10 +323,13 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<ReasoningAccordion
|
<ReasoningAccordion
|
||||||
v-if="selectedNode.generateQueriesReasoning"
|
v-if="selectedNode.generateLearningsReasoning"
|
||||||
v-model="selectedNode.generateQueriesReasoning"
|
v-model="selectedNode.generateLearningsReasoning"
|
||||||
class="my-2"
|
class="my-2"
|
||||||
loading
|
:loading="
|
||||||
|
selectedNode.status === 'processing_serach_result_reasoning' ||
|
||||||
|
selectedNode.status === 'processing_serach_result'
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
v-for="(learning, index) in selectedNode.learnings"
|
v-for="(learning, index) in selectedNode.learnings"
|
||||||
@ -310,6 +337,24 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
v-html="marked(`- ${learning}`, { gfm: true })"
|
v-html="marked(`- ${learning}`, { gfm: true })"
|
||||||
/>
|
/>
|
||||||
|
<span v-if="!selectedNode.learnings?.length"> - </span>
|
||||||
|
|
||||||
|
<!-- Follow up questions -->
|
||||||
|
<!-- Only show if there is reasoning content. Otherwise the follow-ups are basically just child nodes. -->
|
||||||
|
<template v-if="selectedNode.generateQueriesReasoning">
|
||||||
|
<h3 class="text-lg font-semibold my-2">
|
||||||
|
{{ t('webBrowsing.followUpQuestions') }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Set loading default to true, because currently don't know how to handle it otherwise -->
|
||||||
|
<ReasoningAccordion
|
||||||
|
v-if="selectedNode.generateQueriesReasoning"
|
||||||
|
v-model="selectedNode.generateQueriesReasoning"
|
||||||
|
:loading="
|
||||||
|
selectedNode.status === 'generating_query_reasoning' ||
|
||||||
|
selectedNode.status === 'generating_query'
|
||||||
|
"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
140
components/DeepResearch/SearchFlow.vue
Normal file
140
components/DeepResearch/SearchFlow.vue
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import '@vue-flow/core/dist/style.css'
|
||||||
|
import '@vue-flow/core/dist/theme-default.css'
|
||||||
|
import '@vue-flow/controls/dist/style.css'
|
||||||
|
import SearchNode from './SearchNode.vue'
|
||||||
|
import {
|
||||||
|
type Edge,
|
||||||
|
type FlowEvents,
|
||||||
|
type Node,
|
||||||
|
VueFlow,
|
||||||
|
useVueFlow,
|
||||||
|
} from '@vue-flow/core'
|
||||||
|
import { Background } from '@vue-flow/background'
|
||||||
|
import { Controls } from '@vue-flow/controls'
|
||||||
|
import type { DeepResearchNodeStatus } from './DeepResearch.vue'
|
||||||
|
|
||||||
|
export interface SearchNodeData {
|
||||||
|
title: string
|
||||||
|
status?: DeepResearchNodeStatus
|
||||||
|
}
|
||||||
|
export type SearchNode = Node<SearchNodeData>
|
||||||
|
export type SearchEdge = Edge<SearchNodeData>
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'node-click', nodeId: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
selectedNodeId?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isLargeScreen = useMediaQuery('(min-width: 768px)')
|
||||||
|
const defaultPosition = { x: 0, y: 0 }
|
||||||
|
const nodes = ref<SearchNode[]>([defaultRootNode()])
|
||||||
|
const edges = ref<SearchEdge[]>([])
|
||||||
|
let hasUserInteraction = false
|
||||||
|
|
||||||
|
const {
|
||||||
|
addNodes: addFlowNodes,
|
||||||
|
addEdges: addFlowEdges,
|
||||||
|
updateNodeData: updateFlowNodeData,
|
||||||
|
fitView,
|
||||||
|
} = useVueFlow()
|
||||||
|
const { layout } = useNodeLayout()
|
||||||
|
|
||||||
|
function defaultRootNode(): SearchNode {
|
||||||
|
return {
|
||||||
|
id: '0',
|
||||||
|
data: { title: 'Start' },
|
||||||
|
position: { ...defaultPosition },
|
||||||
|
type: 'search', // We only have this type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNodeClick(nodeId: string) {
|
||||||
|
emit('node-click', nodeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function layoutGraph() {
|
||||||
|
nodes.value = layout(nodes.value, edges.value)
|
||||||
|
if (!hasUserInteraction) {
|
||||||
|
nextTick(() => {
|
||||||
|
fitView({})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNode(nodeId: string, data: SearchNodeData, parentId?: string) {
|
||||||
|
addFlowNodes({
|
||||||
|
id: nodeId,
|
||||||
|
data,
|
||||||
|
position: { ...defaultPosition },
|
||||||
|
type: 'search',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (parentId) {
|
||||||
|
addFlowEdges({
|
||||||
|
id: `e:${parentId}:${nodeId}`,
|
||||||
|
source: parentId,
|
||||||
|
target: nodeId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutGraph()
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNode(nodeId: string, data: Partial<SearchNodeData>) {
|
||||||
|
updateFlowNodeData(nodeId, data)
|
||||||
|
layoutGraph()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearNodes() {
|
||||||
|
nodes.value = [defaultRootNode()]
|
||||||
|
edges.value = []
|
||||||
|
layoutGraph()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrag(e: PointerEvent | FlowEvents['move']) {
|
||||||
|
// Triggered by VueFlow internal logic
|
||||||
|
if ('event' in e && !e.event.sourceEvent) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUserInteraction = true
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
addNode,
|
||||||
|
updateNode,
|
||||||
|
clearNodes,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ClientOnly fallback-tag="span" fallback="Loading graph...">
|
||||||
|
<div :class="[isLargeScreen ? 'h-100' : 'h-60']">
|
||||||
|
<VueFlow
|
||||||
|
v-model:nodes="nodes"
|
||||||
|
v-model:edges="edges"
|
||||||
|
:edges-updatable="false"
|
||||||
|
:min-zoom="0.5"
|
||||||
|
:max-zoom="isLargeScreen ? 2.5 : 1.8"
|
||||||
|
:default-edge-options="{ animated: true }"
|
||||||
|
@nodes-initialized="layoutGraph"
|
||||||
|
@move="handleDrag"
|
||||||
|
>
|
||||||
|
<template #node-search="props">
|
||||||
|
<SearchNode
|
||||||
|
:data="props.data"
|
||||||
|
:selected="selectedNodeId === props.id"
|
||||||
|
@click="handleNodeClick(props.id)"
|
||||||
|
@pointerdown="handleDrag"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<Background />
|
||||||
|
<Controls @fit-view="hasUserInteraction = false" />
|
||||||
|
</VueFlow>
|
||||||
|
</div>
|
||||||
|
</ClientOnly>
|
||||||
|
</template>
|
78
components/DeepResearch/SearchNode.vue
Normal file
78
components/DeepResearch/SearchNode.vue
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, defineProps } from 'vue'
|
||||||
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
|
import type { ButtonProps } from '@nuxt/ui'
|
||||||
|
import type { SearchNodeData } from './SearchFlow.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: SearchNodeData
|
||||||
|
selected?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const theme = computed(() => {
|
||||||
|
const result = {
|
||||||
|
icon: '',
|
||||||
|
pulse: false,
|
||||||
|
color: 'info' as ButtonProps['color'],
|
||||||
|
}
|
||||||
|
if (!props.data?.status) return result
|
||||||
|
|
||||||
|
switch (props.data.status) {
|
||||||
|
case 'generating_query':
|
||||||
|
case 'generating_query_reasoning':
|
||||||
|
result.icon = 'i-lucide-clipboard-list'
|
||||||
|
result.pulse = true
|
||||||
|
break
|
||||||
|
case 'generated_query':
|
||||||
|
result.icon = 'i-lucide-circle-pause'
|
||||||
|
break
|
||||||
|
case 'searching':
|
||||||
|
result.icon = 'i-lucide-search'
|
||||||
|
result.pulse = true
|
||||||
|
break
|
||||||
|
case 'search_complete':
|
||||||
|
result.icon = 'i-lucide-search-check'
|
||||||
|
break
|
||||||
|
case 'processing_serach_result':
|
||||||
|
case 'processing_serach_result_reasoning':
|
||||||
|
result.icon = 'i-lucide-brain'
|
||||||
|
result.pulse = true
|
||||||
|
break
|
||||||
|
case 'node_complete':
|
||||||
|
result.icon = 'i-lucide-circle-check-big'
|
||||||
|
break
|
||||||
|
case 'error':
|
||||||
|
result.icon = 'i-lucide-octagon-x'
|
||||||
|
result.color = 'error'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UButton
|
||||||
|
class="process-node"
|
||||||
|
:class="[theme.pulse && 'animate-pulse', 'max-w-90']"
|
||||||
|
:color="selected ? 'primary' : theme.color"
|
||||||
|
:variant="selected ? 'soft' : 'outline'"
|
||||||
|
:icon="theme.icon"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Handle type="target" :position="Position.Left" />
|
||||||
|
<Handle type="source" :position="Position.Right" />
|
||||||
|
|
||||||
|
{{ data.title }}
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Hide the handles */
|
||||||
|
.process-node .vue-flow__handle {
|
||||||
|
border: none;
|
||||||
|
height: unset;
|
||||||
|
width: unset;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,101 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { ButtonProps } from '@nuxt/ui'
|
|
||||||
import type { ResearchStep } from '~/lib/deep-research'
|
|
||||||
|
|
||||||
export type TreeNodeStatus = Exclude<ResearchStep['type'], 'complete'>
|
|
||||||
|
|
||||||
export type TreeNode = {
|
|
||||||
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?: TreeNodeStatus
|
|
||||||
children: TreeNode[]
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
node: TreeNode
|
|
||||||
selectedNode?: TreeNode
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'select', value: TreeNode): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const theme = computed(() => {
|
|
||||||
const result = {
|
|
||||||
icon: '',
|
|
||||||
pulse: false,
|
|
||||||
color: 'info' as ButtonProps['color'],
|
|
||||||
}
|
|
||||||
if (!props.node.status) return result
|
|
||||||
|
|
||||||
switch (props.node.status) {
|
|
||||||
case 'generating_query':
|
|
||||||
result.icon = 'i-lucide-clipboard-list'
|
|
||||||
result.pulse = true
|
|
||||||
break
|
|
||||||
case 'generated_query':
|
|
||||||
// FIXME: 因为 deepResearch 有并发限制,这个 case 是为了明确区分状态。
|
|
||||||
// 但是目前进入这个状态之后再进入 searching 状态,图标不会更新成 search,不知道原因
|
|
||||||
// 暂时禁用了这个 case
|
|
||||||
// result.name = 'i-lucide-pause'
|
|
||||||
// result.pulse = true
|
|
||||||
// break
|
|
||||||
case 'searching':
|
|
||||||
result.icon = 'i-lucide-search'
|
|
||||||
result.pulse = true
|
|
||||||
break
|
|
||||||
case 'search_complete':
|
|
||||||
result.icon = 'i-lucide-search-check'
|
|
||||||
break
|
|
||||||
case 'processing_serach_result':
|
|
||||||
result.icon = 'i-lucide-brain'
|
|
||||||
result.pulse = true
|
|
||||||
break
|
|
||||||
case 'processed_search_result':
|
|
||||||
result.icon = 'i-lucide-circle-check-big'
|
|
||||||
break
|
|
||||||
case 'error':
|
|
||||||
result.icon = 'i-lucide-octagon-x'
|
|
||||||
result.color = 'error'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<UIcon name="i-lucide-circle-dot" />
|
|
||||||
<UButton
|
|
||||||
:class="['max-w-90 shrink-0', theme.pulse && 'animate-pulse']"
|
|
||||||
:icon="theme.icon"
|
|
||||||
size="sm"
|
|
||||||
:color="selectedNode?.id === node.id ? 'primary' : theme.color"
|
|
||||||
:variant="selectedNode?.id === node.id ? 'soft' : 'outline'"
|
|
||||||
@click="emit('select', node)"
|
|
||||||
>
|
|
||||||
{{ node.label }}
|
|
||||||
</UButton>
|
|
||||||
<ol v-if="node.children.length > 0" class="flex flex-col gap-y-2">
|
|
||||||
<li v-for="node in node.children" :key="node.id">
|
|
||||||
<Tree
|
|
||||||
class="ml-2"
|
|
||||||
:node="node"
|
|
||||||
:selected-node
|
|
||||||
@select="emit('select', $event)"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
62
composables/useNodeLayout.ts
Normal file
62
composables/useNodeLayout.ts
Normal file
@ -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 }
|
||||||
|
}
|
@ -76,7 +76,8 @@
|
|||||||
"learnings": "Learnings",
|
"learnings": "Learnings",
|
||||||
"generating": "Generating...",
|
"generating": "Generating...",
|
||||||
"nodeFailed": "Search failed",
|
"nodeFailed": "Search failed",
|
||||||
"nodeFailedToast": "Search node \"{label}\" failed"
|
"nodeFailedToast": "Search node \"{label}\" failed",
|
||||||
|
"followUpQuestions": "Follow-up Questions"
|
||||||
},
|
},
|
||||||
"researchReport": {
|
"researchReport": {
|
||||||
"title": "4. Research Report",
|
"title": "4. Research Report",
|
||||||
|
@ -76,7 +76,8 @@
|
|||||||
"learnings": "结论",
|
"learnings": "结论",
|
||||||
"generating": "生成中...",
|
"generating": "生成中...",
|
||||||
"nodeFailed": "搜索失败",
|
"nodeFailed": "搜索失败",
|
||||||
"nodeFailedToast": "搜索步骤 “{label}” 失败"
|
"nodeFailedToast": "搜索步骤 “{label}” 失败",
|
||||||
|
"followUpQuestions": "后续问题"
|
||||||
},
|
},
|
||||||
"researchReport": {
|
"researchReport": {
|
||||||
"title": "4. 研究报告",
|
"title": "4. 研究报告",
|
||||||
|
@ -27,7 +27,12 @@ export type ProcessedSearchResult = z.infer<typeof searchResultTypeSchema>
|
|||||||
export type PartialProcessedSearchResult = DeepPartial<ProcessedSearchResult>
|
export type PartialProcessedSearchResult = DeepPartial<ProcessedSearchResult>
|
||||||
|
|
||||||
export type ResearchStep =
|
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: 'generating_query_reasoning'; delta: string; nodeId: string }
|
||||||
| {
|
| {
|
||||||
type: 'generated_query'
|
type: 'generated_query'
|
||||||
@ -49,9 +54,8 @@ export type ResearchStep =
|
|||||||
nodeId: string
|
nodeId: string
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'processed_search_result'
|
type: 'node_complete'
|
||||||
query: string
|
result?: ProcessedSearchResult
|
||||||
result: ProcessedSearchResult
|
|
||||||
nodeId: string
|
nodeId: string
|
||||||
}
|
}
|
||||||
| { type: 'error'; message: string; nodeId: string }
|
| { type: 'error'; message: string; nodeId: string }
|
||||||
@ -239,6 +243,12 @@ export async function deepResearch({
|
|||||||
const { t } = useNuxtApp().$i18n
|
const { t } = useNuxtApp().$i18n
|
||||||
const language = t('language', {}, { locale: languageCode })
|
const language = t('language', {}, { locale: languageCode })
|
||||||
|
|
||||||
|
onProgress({
|
||||||
|
type: 'generating_query',
|
||||||
|
nodeId,
|
||||||
|
result: {},
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const searchQueriesResult = generateSearchQueries({
|
const searchQueriesResult = generateSearchQueries({
|
||||||
query,
|
query,
|
||||||
@ -268,9 +278,11 @@ export async function deepResearch({
|
|||||||
type: 'generating_query',
|
type: 'generating_query',
|
||||||
result: searchQueries[i],
|
result: searchQueries[i],
|
||||||
nodeId: childNodeId(nodeId, i),
|
nodeId: childNodeId(nodeId, i),
|
||||||
|
parentNodeId: nodeId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (chunk.type === 'reasoning') {
|
} else if (chunk.type === 'reasoning') {
|
||||||
|
// Reasoning part goes to the parent node
|
||||||
onProgress({
|
onProgress({
|
||||||
type: 'generating_query_reasoning',
|
type: 'generating_query_reasoning',
|
||||||
delta: chunk.delta,
|
delta: chunk.delta,
|
||||||
@ -293,6 +305,11 @@ export async function deepResearch({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onProgress({
|
||||||
|
type: 'node_complete',
|
||||||
|
nodeId,
|
||||||
|
})
|
||||||
|
|
||||||
for (let i = 0; i < searchQueries.length; i++) {
|
for (let i = 0; i < searchQueries.length; i++) {
|
||||||
onProgress({
|
onProgress({
|
||||||
type: 'generated_query',
|
type: 'generated_query',
|
||||||
@ -318,13 +335,13 @@ export async function deepResearch({
|
|||||||
nodeId: childNodeId(nodeId, i),
|
nodeId: childNodeId(nodeId, i),
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
// Use Tavily to search the web
|
// search the web
|
||||||
const results = await useWebSearch()(searchQuery.query, {
|
const results = await useWebSearch()(searchQuery.query, {
|
||||||
maxResults: 5,
|
maxResults: 5,
|
||||||
lang: languageCode,
|
lang: languageCode,
|
||||||
})
|
})
|
||||||
console.log(
|
console.log(
|
||||||
`Ran ${searchQuery.query}, found ${results.length} contents`,
|
`[DeepResearch] Searched "${searchQuery.query}", found ${results.length} contents`,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Collect URLs from this search
|
// Collect URLs from this search
|
||||||
@ -393,12 +410,11 @@ export async function deepResearch({
|
|||||||
const nextDepth = currentDepth + 1
|
const nextDepth = currentDepth + 1
|
||||||
|
|
||||||
onProgress({
|
onProgress({
|
||||||
type: 'processed_search_result',
|
type: 'node_complete',
|
||||||
result: {
|
result: {
|
||||||
learnings: allLearnings,
|
learnings: allLearnings,
|
||||||
followUpQuestions: searchResult.followUpQuestions ?? [],
|
followUpQuestions: searchResult.followUpQuestions ?? [],
|
||||||
},
|
},
|
||||||
query: searchQuery.query,
|
|
||||||
nodeId: childNodeId(nodeId, i),
|
nodeId: childNodeId(nodeId, i),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
"@ai-sdk/openai": "^1.1.11",
|
"@ai-sdk/openai": "^1.1.11",
|
||||||
"@ai-sdk/ui-utils": "^1.1.14",
|
"@ai-sdk/ui-utils": "^1.1.14",
|
||||||
"@ai-sdk/vue": "^1.1.17",
|
"@ai-sdk/vue": "^1.1.17",
|
||||||
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@iconify-json/lucide": "^1.2.26",
|
"@iconify-json/lucide": "^1.2.26",
|
||||||
"@mendable/firecrawl-js": "^1.17.0",
|
"@mendable/firecrawl-js": "^1.17.0",
|
||||||
"@nuxt/ui": "3.0.0-alpha.12",
|
"@nuxt/ui": "3.0.0-alpha.12",
|
||||||
@ -25,6 +26,9 @@
|
|||||||
"@pinia/nuxt": "^0.10.1",
|
"@pinia/nuxt": "^0.10.1",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tavily/core": "^0.3.1",
|
"@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",
|
"ai": "^4.1.41",
|
||||||
"js-tiktoken": "^1.0.19",
|
"js-tiktoken": "^1.0.19",
|
||||||
"jspdf": "^2.5.2",
|
"jspdf": "^2.5.2",
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type ResearchForm from '~/components/ResearchForm.vue'
|
import type ResearchForm from '~/components/ResearchForm.vue'
|
||||||
import type ResearchFeedback from '~/components/ResearchFeedback.vue'
|
import type ResearchFeedback from '~/components/ResearchFeedback.vue'
|
||||||
import type DeepResearch from '~/components/DeepResearch.vue'
|
import type DeepResearch from '~/components/DeepResearch/DeepResearch.vue'
|
||||||
import type ResearchReport from '~/components/ResearchReport.vue'
|
import type ResearchReport from '~/components/ResearchReport.vue'
|
||||||
import type ConfigManager from '~/components/ConfigManager.vue'
|
import type ConfigManager from '~/components/ConfigManager.vue'
|
||||||
import type { ResearchInputData } from '~/components/ResearchForm.vue'
|
import type { ResearchInputData } from '~/components/ResearchForm.vue'
|
||||||
@ -64,7 +64,7 @@
|
|||||||
researchResultInjectionKey,
|
researchResultInjectionKey,
|
||||||
} from '~/constants/injection-keys'
|
} from '~/constants/injection-keys'
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t } = useI18n()
|
||||||
const { config } = storeToRefs(useConfigStore())
|
const { config } = storeToRefs(useConfigStore())
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const version = useRuntimeConfig().public.version
|
const version = useRuntimeConfig().public.version
|
||||||
|
136
pnpm-lock.yaml
generated
136
pnpm-lock.yaml
generated
@ -20,6 +20,9 @@ importers:
|
|||||||
'@ai-sdk/vue':
|
'@ai-sdk/vue':
|
||||||
specifier: ^1.1.17
|
specifier: ^1.1.17
|
||||||
version: 1.1.17(vue@3.5.13(typescript@5.7.3))(zod@3.24.2)
|
version: 1.1.17(vue@3.5.13(typescript@5.7.3))(zod@3.24.2)
|
||||||
|
'@dagrejs/dagre':
|
||||||
|
specifier: ^1.1.4
|
||||||
|
version: 1.1.4
|
||||||
'@iconify-json/lucide':
|
'@iconify-json/lucide':
|
||||||
specifier: ^1.2.26
|
specifier: ^1.2.26
|
||||||
version: 1.2.26
|
version: 1.2.26
|
||||||
@ -47,6 +50,15 @@ importers:
|
|||||||
'@tavily/core':
|
'@tavily/core':
|
||||||
specifier: ^0.3.1
|
specifier: ^0.3.1
|
||||||
version: 0.3.1
|
version: 0.3.1
|
||||||
|
'@vue-flow/background':
|
||||||
|
specifier: ^1.3.2
|
||||||
|
version: 1.3.2(@vue-flow/core@1.42.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
|
||||||
|
'@vue-flow/controls':
|
||||||
|
specifier: ^1.1.2
|
||||||
|
version: 1.1.2(@vue-flow/core@1.42.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
|
||||||
|
'@vue-flow/core':
|
||||||
|
specifier: ^1.42.1
|
||||||
|
version: 1.42.1(vue@3.5.13(typescript@5.7.3))
|
||||||
ai:
|
ai:
|
||||||
specifier: ^4.1.41
|
specifier: ^4.1.41
|
||||||
version: 4.1.41(react@19.0.0)(zod@3.24.2)
|
version: 4.1.41(react@19.0.0)(zod@3.24.2)
|
||||||
@ -341,6 +353,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==}
|
resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==}
|
||||||
engines: {node: '>=16.13'}
|
engines: {node: '>=16.13'}
|
||||||
|
|
||||||
|
'@dagrejs/dagre@1.1.4':
|
||||||
|
resolution: {integrity: sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg==}
|
||||||
|
|
||||||
|
'@dagrejs/graphlib@2.2.4':
|
||||||
|
resolution: {integrity: sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==}
|
||||||
|
engines: {node: '>17.0.0'}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.24.2':
|
'@esbuild/aix-ppc64@0.24.2':
|
||||||
resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
|
resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -1297,6 +1316,23 @@ packages:
|
|||||||
vite: ^5.0.0 || ^6.0.0
|
vite: ^5.0.0 || ^6.0.0
|
||||||
vue: ^3.2.25
|
vue: ^3.2.25
|
||||||
|
|
||||||
|
'@vue-flow/background@1.3.2':
|
||||||
|
resolution: {integrity: sha512-eJPhDcLj1wEo45bBoqTXw1uhl0yK2RaQGnEINqvvBsAFKh/camHJd5NPmOdS1w+M9lggc9igUewxaEd3iCQX2w==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vue-flow/core': ^1.23.0
|
||||||
|
vue: ^3.3.0
|
||||||
|
|
||||||
|
'@vue-flow/controls@1.1.2':
|
||||||
|
resolution: {integrity: sha512-6dtl/JnwDBNau5h3pDBdOCK6tdxiVAOL3cyruRL61gItwq5E97Hmjmj2BIIqX2p7gU1ENg3z80Z4zlu58fGlsg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vue-flow/core': ^1.23.0
|
||||||
|
vue: ^3.3.0
|
||||||
|
|
||||||
|
'@vue-flow/core@1.42.1':
|
||||||
|
resolution: {integrity: sha512-QzzTxMAXfOeETKc+N3XMp5XpiPxKBHK5kq98avgTsE6MXyeU2E8EkANwwgSB/hvJ/k36RjU0Y7BOwCHiqiI1tw==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.3.0
|
||||||
|
|
||||||
'@vue-macros/common@1.16.1':
|
'@vue-macros/common@1.16.1':
|
||||||
resolution: {integrity: sha512-Pn/AWMTjoMYuquepLZP813BIcq8DTZiNCoaceuNlvaYuOTd8DqBZWc5u0uOMQZMInwME1mdSmmBAcTluiV9Jtg==}
|
resolution: {integrity: sha512-Pn/AWMTjoMYuquepLZP813BIcq8DTZiNCoaceuNlvaYuOTd8DqBZWc5u0uOMQZMInwME1mdSmmBAcTluiV9Jtg==}
|
||||||
engines: {node: '>=16.14.0'}
|
engines: {node: '>=16.14.0'}
|
||||||
@ -1869,6 +1905,44 @@ packages:
|
|||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
|
d3-color@3.1.0:
|
||||||
|
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-dispatch@3.0.1:
|
||||||
|
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-drag@3.0.0:
|
||||||
|
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-ease@3.0.1:
|
||||||
|
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-interpolate@3.0.1:
|
||||||
|
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-selection@3.0.0:
|
||||||
|
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-timer@3.0.1:
|
||||||
|
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-transition@3.0.1:
|
||||||
|
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
peerDependencies:
|
||||||
|
d3-selection: 2 - 3
|
||||||
|
|
||||||
|
d3-zoom@3.0.0:
|
||||||
|
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
db0@0.2.3:
|
db0@0.2.3:
|
||||||
resolution: {integrity: sha512-PunuHESDNefmwVy1LDpY663uWwKt2ogLGoB6NOz2sflGREWqDreMwDgF8gfkXxgNXW+dqviyiJGm924H1BaGiw==}
|
resolution: {integrity: sha512-PunuHESDNefmwVy1LDpY663uWwKt2ogLGoB6NOz2sflGREWqDreMwDgF8gfkXxgNXW+dqviyiJGm924H1BaGiw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -4623,6 +4697,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mime: 3.0.0
|
mime: 3.0.0
|
||||||
|
|
||||||
|
'@dagrejs/dagre@1.1.4':
|
||||||
|
dependencies:
|
||||||
|
'@dagrejs/graphlib': 2.2.4
|
||||||
|
|
||||||
|
'@dagrejs/graphlib@2.2.4': {}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.24.2':
|
'@esbuild/aix-ppc64@0.24.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -5860,6 +5940,26 @@ snapshots:
|
|||||||
vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.38.1)(yaml@2.7.0)
|
vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.38.1)(yaml@2.7.0)
|
||||||
vue: 3.5.13(typescript@5.7.3)
|
vue: 3.5.13(typescript@5.7.3)
|
||||||
|
|
||||||
|
'@vue-flow/background@1.3.2(@vue-flow/core@1.42.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
|
||||||
|
dependencies:
|
||||||
|
'@vue-flow/core': 1.42.1(vue@3.5.13(typescript@5.7.3))
|
||||||
|
vue: 3.5.13(typescript@5.7.3)
|
||||||
|
|
||||||
|
'@vue-flow/controls@1.1.2(@vue-flow/core@1.42.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
|
||||||
|
dependencies:
|
||||||
|
'@vue-flow/core': 1.42.1(vue@3.5.13(typescript@5.7.3))
|
||||||
|
vue: 3.5.13(typescript@5.7.3)
|
||||||
|
|
||||||
|
'@vue-flow/core@1.42.1(vue@3.5.13(typescript@5.7.3))':
|
||||||
|
dependencies:
|
||||||
|
'@vueuse/core': 10.11.1(vue@3.5.13(typescript@5.7.3))
|
||||||
|
d3-drag: 3.0.0
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
d3-zoom: 3.0.0
|
||||||
|
vue: 3.5.13(typescript@5.7.3)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@vue/composition-api'
|
||||||
|
|
||||||
'@vue-macros/common@1.16.1(vue@3.5.13(typescript@5.7.3))':
|
'@vue-macros/common@1.16.1(vue@3.5.13(typescript@5.7.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/compiler-sfc': 3.5.13
|
'@vue/compiler-sfc': 3.5.13
|
||||||
@ -6536,6 +6636,42 @@ snapshots:
|
|||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
|
d3-color@3.1.0: {}
|
||||||
|
|
||||||
|
d3-dispatch@3.0.1: {}
|
||||||
|
|
||||||
|
d3-drag@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
|
||||||
|
d3-ease@3.0.1: {}
|
||||||
|
|
||||||
|
d3-interpolate@3.0.1:
|
||||||
|
dependencies:
|
||||||
|
d3-color: 3.1.0
|
||||||
|
|
||||||
|
d3-selection@3.0.0: {}
|
||||||
|
|
||||||
|
d3-timer@3.0.1: {}
|
||||||
|
|
||||||
|
d3-transition@3.0.1(d3-selection@3.0.0):
|
||||||
|
dependencies:
|
||||||
|
d3-color: 3.1.0
|
||||||
|
d3-dispatch: 3.0.1
|
||||||
|
d3-ease: 3.0.1
|
||||||
|
d3-interpolate: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
d3-timer: 3.0.1
|
||||||
|
|
||||||
|
d3-zoom@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch: 3.0.1
|
||||||
|
d3-drag: 3.0.0
|
||||||
|
d3-interpolate: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||||
|
|
||||||
db0@0.2.3: {}
|
db0@0.2.3: {}
|
||||||
|
|
||||||
debug@2.6.9:
|
debug@2.6.9:
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.7"
|
"version": "1.1.0"
|
||||||
}
|
}
|
Reference in New Issue
Block a user