diff --git a/src/App.vue b/src/App.vue index fe056ec..0eccaa1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -20,7 +20,11 @@ import { Settings, Palette, Grid3X3, - Trash2 + Trash2, + X, + Maximize2, + Terminal, + ChevronRight } from 'lucide-vue-next' import { VueFlow, useVueFlow, Position, MarkerType, Handle } from '@vue-flow/core' import { Background, BackgroundVariant } from '@vue-flow/background' @@ -35,18 +39,80 @@ import '@vue-flow/core/dist/theme-default.css' const API_KEY = import.meta.env.VITE_ZHIPU_AI_API_KEY // VueFlow 实例 -const { addNodes, addEdges, onConnect, setNodes, setEdges, nodes: flowNodes, edges: flowEdges, updateNode, fitView } = useVueFlow() +const { addNodes, addEdges, onConnect, setNodes, setEdges, nodes: flowNodes, edges: flowEdges, updateNode, fitView, onNodeDragStart, onNodeDragStop } = useVueFlow() // 状态管理 const ideaInput = ref('') const isLoading = ref(false) const hoveredNodeId = ref(null) const focusedNodeId = ref(null) +const draggingNodeId = ref(null) +const previewImageUrl = ref(null) -// 计算当前是否有节点处于“活跃”状态(被聚焦、悬停或正在生成) +// 拖拽监听 +onNodeDragStart(e => { + draggingNodeId.value = e.node.id +}) + +onNodeDragStop(() => { + draggingNodeId.value = null +}) + +// 计算当前是否有节点处于“活跃”状态(被选中、聚焦、拖拽或正在生成) const activeNodeId = computed(() => { const expandingNode = flowNodes.value.find(n => n.data.isExpanding) - return expandingNode?.id || focusedNodeId.value || hoveredNodeId.value + const selectedNode = flowNodes.value.find(n => n.selected) + return expandingNode?.id || selectedNode?.id || draggingNodeId.value || focusedNodeId.value +}) + +/** + * 递归获取所有子节点 ID + */ +const getDescendantIds = (nodeId: string, ids: Set = new Set()): Set => { + flowEdges.value.forEach(edge => { + if (edge.source === nodeId) { + ids.add(edge.target) + getDescendantIds(edge.target, ids) + } + }) + return ids +} + +// 计算当前活跃节点的相关路径(向上追溯到根,向下包含所有子孙) +const activePath = computed(() => { + const nodeIds = new Set() + const edgeIds = new Set() + + if (!activeNodeId.value) return { nodeIds, edgeIds } + + const targetId = activeNodeId.value + nodeIds.add(targetId) + + // 1. 向上追溯到根节点 + let currentId = targetId + while (currentId) { + const edge = flowEdges.value.find(e => e.target === currentId) + if (edge) { + edgeIds.add(edge.id) + nodeIds.add(edge.source) + currentId = edge.source + } else { + break + } + } + + // 2. 向下包含所有子孙节点和相关连线 + const descendantIds = getDescendantIds(targetId) + descendantIds.forEach(id => nodeIds.add(id)) + + // 3. 收集子孙节点之间的连线 + flowEdges.value.forEach(edge => { + if (nodeIds.has(edge.source) && nodeIds.has(edge.target)) { + edgeIds.add(edge.id) + } + }) + + return { nodeIds, edgeIds } }) // 画布配置 @@ -57,17 +123,39 @@ const config = reactive({ showControls: true }) -// 监听配置变化更新现有连线 +const lastAppliedStatus = ref('') + +// 监听 activePath 和配置变化,动态更新连线状态 watch( - () => config.edgeColor, - newColor => { + [() => activeNodeId.value, () => config.edgeColor, () => flowEdges.value.length, () => flowNodes.value.some(n => n.data.isExpanding)], + ([newNodeId, newColor, newLength, anyExpanding]) => { + const { edgeIds } = activePath.value + const edgeIdsStr = Array.from(edgeIds).sort().join(',') + + // 状态标识:包含高亮边、颜色、以及是否有节点在发散(影响动画) + const currentStatus = `${edgeIdsStr}-${newColor}-${anyExpanding}` + if (lastAppliedStatus.value === currentStatus) return + lastAppliedStatus.value = currentStatus + setEdges( - flowEdges.value.map(edge => ({ - ...edge, - style: { ...edge.style, stroke: newColor } - })) + flowEdges.value.map(edge => { + const isHighlighted = edgeIds.has(edge.id) + const isExpanding = !!flowNodes.value.find(n => n.id === edge.source)?.data.isExpanding + + return { + ...edge, + animated: isHighlighted || isExpanding, + style: { + ...edge.style, + stroke: isHighlighted ? newColor : `${newColor}33`, + strokeWidth: isHighlighted ? 3 : 2, + transition: 'all 0.3s ease' + } + } + }) ) - } + }, + { immediate: true } ) /** @@ -125,6 +213,27 @@ const generateNodeImage = async (nodeId: string, prompt: string) => { } } +/** + * 递归获取从根节点到当前节点的路径 + */ +const findPathToNode = (nodeId: string): string[] => { + const path: string[] = [] + let currentId = nodeId + + while (currentId) { + const node = flowNodes.value.find(n => n.id === currentId) + if (node) { + path.unshift(`${node.data.label} (${node.data.description})`) + // 查找连入该节点的边 + const edge = flowEdges.value.find(e => e.target === currentId) + currentId = edge ? edge.source : '' + } else { + break + } + } + return path +} + /** * 调用智谱AI生成思维发散节点 */ @@ -135,8 +244,10 @@ const expandIdea = async (param?: any, customInput?: string) => { if (!text || (parentNode ? parentNode.data.isExpanding : isLoading.value)) return + // 记录父节点加载状态 if (parentNode) { - updateNode(parentNode.id, { data: { ...parentNode.data, isExpanding: true } }) + const node = flowNodes.value.find(n => n.id === parentNode.id) + if (node) node.data.isExpanding = true } else { isLoading.value = true } @@ -151,9 +262,9 @@ const expandIdea = async (param?: any, customInput?: string) => { 工作流程: 1. 用户给出一个初始想法(或选择一个已有节点继续追问)。 -2. 你根据当前想法和已有对话历史,生成 3-5 个更深层或相关维度的子想法。 -3. 每个子想法包含简短名称和极简描述。 -4. 如果用户的问题明显是针对某个已有节点的追问,请结合上下文做针对性发散。 +2. 你需要根据【思考上下文路径】(即从根节点到当前节点的思考链路)来理解用户的意图。 +3. 生成 3-5 个更深层或相关维度的子想法。 +4. 每个子想法包含简短名称和极简描述。 返回格式必须为严格 JSON: { @@ -163,29 +274,15 @@ const expandIdea = async (param?: any, customInput?: string) => { ] } -示例: -用户:"年夜饭推荐" -你返回:{ - "nodes": [ - { "text": "传统年菜", "description": "饺子、鱼、年糕等经典菜谱" }, - { "text": "创新年菜", "description": "融合中西风格的新式年夜饭" }, - { "text": "素食年夜饭", "description": "适合素食者的丰盛菜单" }, - { "text": "快手年夜饭", "description": "省时省力又显丰盛的方案" } - ] -} - -当用户追问时(例如用户说:"详细说说传统年菜"),你结合上下文发散出更细的子节点: -{ - "nodes": [ - { "text": "北方饺子", "description": "多种馅料与蘸料搭配" }, - { "text": "清蒸鱼", "description": "寓意年年有余的做法与选鱼技巧" }, - { "text": "年糕甜品", "description": "不同地区的甜味或咸味年糕" } - ] -} - 注意:只返回 JSON,不附加解释。` - const userMessage = parentNode && customInput ? `核心想法: ${parentNode.data.label}\n用户追问: ${customInput}` : text + let userMessage = '' + if (parentNode) { + const path = findPathToNode(parentNode.id) + userMessage = `[思考上下文路径]: ${path.join(' -> ')}\n[当前选择节点]: ${parentNode.data.label}\n[用户追问/新要求]: ${customInput || '请继续深入发散'}` + } else { + userMessage = `核心想法: ${text}` + } try { const response = await fetch('https://open.bigmodel.cn/api/paas/v4/chat/completions', { @@ -220,7 +317,13 @@ const expandIdea = async (param?: any, customInput?: string) => { id: rootId, type: 'window', position: { x: startX, y: startY }, - data: { label: text, description: '核心想法', type: 'root' }, + data: { + label: text, + description: '核心想法', + type: 'root', + isExpanding: false, + followUp: '' + }, sourcePosition: Position.Right, targetPosition: Position.Left }) @@ -234,7 +337,11 @@ const expandIdea = async (param?: any, customInput?: string) => { console.error('Expansion Error:', error) } finally { if (parentNode) { - updateNode(parentNode.id, { data: { ...parentNode.data, isExpanding: false, followUp: '' } }) + const node = flowNodes.value.find(n => n.id === parentNode.id) + if (node) { + node.data.isExpanding = false + node.data.followUp = '' + } } else { isLoading.value = false } @@ -345,16 +452,17 @@ const startNewSession = () => { -