优化代码,增加注释
This commit is contained in:
755
src/composables/useThinkFlow.ts
Normal file
755
src/composables/useThinkFlow.ts
Normal file
@@ -0,0 +1,755 @@
|
||||
/**
|
||||
* ThinkFlow 核心业务 composable
|
||||
* - 统一管理:画布状态(节点/边)、交互状态、API 调用、错误处理与导出能力
|
||||
* - 对外提供:页面与组件可直接调用的状态与动作(expand / deepDive / image / summary 等)
|
||||
*/
|
||||
|
||||
import { computed, reactive, ref, watch, type Ref } from 'vue'
|
||||
import { MarkerType, Position, useVueFlow } from '@vue-flow/core'
|
||||
import { BackgroundVariant } from '@vue-flow/background'
|
||||
|
||||
/**
|
||||
* i18n 翻译函数类型(等价于 vue-i18n 的 t)
|
||||
*/
|
||||
type Translate = (key: string, params?: any) => string
|
||||
|
||||
/**
|
||||
* 创建 ThinkFlow 的业务上下文。
|
||||
* @param t 国际化翻译函数
|
||||
* @param locale 当前语言(用于持久化语言选择)
|
||||
*/
|
||||
export function useThinkFlow({ t, locale }: { t: Translate; locale: Ref<string> }) {
|
||||
/**
|
||||
* 默认模式下的 API Key(当前不使用环境变量注入)。
|
||||
* - 如需鉴权,请通过 Settings 的 Custom 模式填写 apiKey
|
||||
*/
|
||||
const API_KEY = ''
|
||||
|
||||
/**
|
||||
* API 配置(支持默认/自定义两种模式)
|
||||
* - 自定义模式写入 localStorage,刷新后仍保留
|
||||
*/
|
||||
const apiConfig = reactive({
|
||||
mode: localStorage.getItem('api_mode') || 'default',
|
||||
chat: {
|
||||
baseUrl: localStorage.getItem('chat_baseUrl') || '',
|
||||
model: localStorage.getItem('chat_model') || '',
|
||||
apiKey: localStorage.getItem('chat_apiKey') || ''
|
||||
},
|
||||
image: {
|
||||
baseUrl: localStorage.getItem('image_baseUrl') || '',
|
||||
model: localStorage.getItem('image_model') || '',
|
||||
apiKey: localStorage.getItem('image_apiKey') || ''
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 默认接口配置(当用户选择默认模式时使用)
|
||||
* - apiKey 允许为空:会回退到 API_KEY(环境变量)
|
||||
*/
|
||||
const DEFAULT_CONFIG = {
|
||||
chat: {
|
||||
baseUrl: 'https://thinkflow.lz-t.top/chat/completions',
|
||||
model: 'glm-4-flash',
|
||||
apiKey: ''
|
||||
},
|
||||
image: {
|
||||
baseUrl: 'https://thinkflow.lz-t.top/images/generations',
|
||||
model: 'cogview-3-flash',
|
||||
apiKey: ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言选择持久化(与 i18n/index.ts 中的初始化配合)
|
||||
*/
|
||||
watch(
|
||||
() => locale.value,
|
||||
newVal => {
|
||||
localStorage.setItem('language', newVal)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* API 配置持久化:任何字段变化都会更新 localStorage
|
||||
*/
|
||||
watch(
|
||||
() => apiConfig,
|
||||
newVal => {
|
||||
localStorage.setItem('api_mode', newVal.mode)
|
||||
localStorage.setItem('chat_baseUrl', newVal.chat.baseUrl)
|
||||
localStorage.setItem('chat_model', newVal.chat.model)
|
||||
localStorage.setItem('chat_apiKey', newVal.chat.apiKey)
|
||||
localStorage.setItem('image_baseUrl', newVal.image.baseUrl)
|
||||
localStorage.setItem('image_model', newVal.image.model)
|
||||
localStorage.setItem('image_apiKey', newVal.image.apiKey)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
/**
|
||||
* 设置弹窗开关(由顶部导航触发)
|
||||
*/
|
||||
const showSettings = ref(false)
|
||||
|
||||
/**
|
||||
* VueFlow 实例能力集合:节点/边增删改与视图控制
|
||||
*/
|
||||
const {
|
||||
addNodes,
|
||||
addEdges,
|
||||
setNodes,
|
||||
setEdges,
|
||||
nodes: flowNodes,
|
||||
edges: flowEdges,
|
||||
updateNode,
|
||||
fitView,
|
||||
onNodeDragStart,
|
||||
onNodeDragStop
|
||||
} = useVueFlow()
|
||||
|
||||
/**
|
||||
* 全局输入与对话状态
|
||||
*/
|
||||
const ideaInput = ref('')
|
||||
const isLoading = ref(false)
|
||||
const previewImageUrl = ref<string | null>(null)
|
||||
const showResetConfirm = ref(false)
|
||||
const showSummaryModal = ref(false)
|
||||
const isSummarizing = ref(false)
|
||||
const summaryContent = ref('')
|
||||
|
||||
const draggingNodeId = ref<string | null>(null)
|
||||
|
||||
/**
|
||||
* 交互:按住 Space 启用“抓手拖拽画布”
|
||||
* - isSpacePressed 用于在 UI 层展示手型光标
|
||||
* - panOnDrag 控制 VueFlow 的拖拽行为(按 Space 时总是允许拖拽画布)
|
||||
*/
|
||||
const panOnDrag = ref(true)
|
||||
const isSpacePressed = ref(false)
|
||||
|
||||
window.addEventListener('keydown', e => {
|
||||
if (e.code === 'Space' && !['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)) {
|
||||
isSpacePressed.value = true
|
||||
panOnDrag.value = true
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener('keyup', e => {
|
||||
if (e.code === 'Space') {
|
||||
isSpacePressed.value = false
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 用拖拽节点作为“当前激活节点”的一种来源,便于高亮路径/聚焦
|
||||
*/
|
||||
onNodeDragStart(e => {
|
||||
draggingNodeId.value = e.node.id
|
||||
})
|
||||
|
||||
onNodeDragStop(() => {
|
||||
draggingNodeId.value = null
|
||||
})
|
||||
|
||||
/**
|
||||
* 当前激活节点 id
|
||||
* 优先级:正在展开的节点 > 选中的节点 > 正在拖拽的节点
|
||||
*/
|
||||
const activeNodeId = computed(() => {
|
||||
const expandingNode = flowNodes.value.find(n => n.data.isExpanding)
|
||||
const selectedNode = flowNodes.value.find(n => n.selected)
|
||||
return expandingNode?.id || selectedNode?.id || draggingNodeId.value
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取某节点所有后代节点 id(用于激活路径计算)
|
||||
*/
|
||||
const getDescendantIds = (nodeId: string, ids: Set<string> = new Set()): Set<string> => {
|
||||
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<string>()
|
||||
const edgeIds = new Set<string>()
|
||||
|
||||
if (!activeNodeId.value) return { nodeIds, edgeIds }
|
||||
|
||||
const targetId = activeNodeId.value
|
||||
nodeIds.add(targetId)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
const descendantIds = getDescendantIds(targetId)
|
||||
descendantIds.forEach(id => nodeIds.add(id))
|
||||
|
||||
flowEdges.value.forEach(edge => {
|
||||
if (nodeIds.has(edge.source) && nodeIds.has(edge.target)) {
|
||||
edgeIds.add(edge.id)
|
||||
}
|
||||
})
|
||||
|
||||
return { nodeIds, edgeIds }
|
||||
})
|
||||
|
||||
const config = reactive({
|
||||
edgeColor: '#fed7aa',
|
||||
edgeStyle: 'smoothstep',
|
||||
backgroundVariant: BackgroundVariant.Lines,
|
||||
showControls: true,
|
||||
showMiniMap: true
|
||||
})
|
||||
|
||||
const lastAppliedStatus = ref('')
|
||||
|
||||
/**
|
||||
* 根据激活路径与配置,动态更新边的样式(高亮/透明度/动画)
|
||||
* - 通过 lastAppliedStatus 避免无效重复 setEdges
|
||||
*/
|
||||
watch(
|
||||
[() => activeNodeId.value, () => config.edgeColor, () => flowEdges.value.length, () => flowNodes.value.some(n => n.data.isExpanding)],
|
||||
([, newColor, , 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 => {
|
||||
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 }
|
||||
)
|
||||
|
||||
/**
|
||||
* 将网络/接口异常转换为用户可读的错误文案
|
||||
*/
|
||||
const getErrorMessage = (error: any) => {
|
||||
if (error.name === 'TypeError' && error.message === 'Failed to fetch') {
|
||||
return t('common.error.cors')
|
||||
}
|
||||
if (error.status === 429) return t('common.error.rateLimit')
|
||||
if (error.status === 400) return t('common.error.badRequest')
|
||||
if (error.status >= 500) return t('common.error.serverError')
|
||||
return error.message || t('common.error.unknown')
|
||||
}
|
||||
|
||||
/**
|
||||
* 视图:将根节点居中显示
|
||||
*/
|
||||
const centerRoot = () => {
|
||||
const rootNode = flowNodes.value.find(n => n.data.type === 'root')
|
||||
if (rootNode) {
|
||||
fitView({ nodes: [rootNode.id], padding: 2, duration: 800 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 布局:从根节点开始按“横向树形”重新排布节点位置
|
||||
* - 主要用于整理节点过多导致的视觉拥挤
|
||||
*/
|
||||
const resetLayout = () => {
|
||||
const rootNode = flowNodes.value.find(n => n.data.type === 'root')
|
||||
if (!rootNode) return
|
||||
|
||||
const visited = new Set<string>()
|
||||
const layoutNode = (nodeId: string, x: number, y: number) => {
|
||||
if (visited.has(nodeId)) return
|
||||
visited.add(nodeId)
|
||||
|
||||
const node = flowNodes.value.find(n => n.id === nodeId)
|
||||
if (node) {
|
||||
node.position = { x, y }
|
||||
|
||||
const childEdges = flowEdges.value.filter(e => e.source === nodeId)
|
||||
childEdges.forEach((edge, index) => {
|
||||
const offsetX = 450
|
||||
const totalHeight = (childEdges.length - 1) * 280
|
||||
const startY = y - totalHeight / 2
|
||||
const offsetY = index * 280
|
||||
|
||||
layoutNode(edge.target, x + offsetX, startY + offsetY)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
layoutNode(rootNode.id, 50, 300)
|
||||
|
||||
setTimeout(() => {
|
||||
fitView({ padding: 0.2, duration: 800 })
|
||||
}, 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出:将当前树形结构导出为 Markdown
|
||||
* - 以 root 为标题
|
||||
* - 子节点按缩进列表输出
|
||||
* - deepDive 生成的详细内容以引用块输出
|
||||
*/
|
||||
const exportMarkdown = () => {
|
||||
if (flowNodes.value.length === 0) return
|
||||
|
||||
const rootNode = flowNodes.value.find(n => n.data.type === 'root')
|
||||
if (!rootNode) return
|
||||
|
||||
let markdown = `# ${rootNode.data.label}\n\n`
|
||||
|
||||
const buildMarkdown = (parentId: string, level: number) => {
|
||||
const children = flowEdges.value
|
||||
.filter(e => e.source === parentId)
|
||||
.map(e => flowNodes.value.find(n => n.id === e.target))
|
||||
.filter(n => n !== undefined)
|
||||
|
||||
children.forEach(child => {
|
||||
const indent = ' '.repeat(level - 1)
|
||||
markdown += `${indent}- ${child!.data.label}\n`
|
||||
if (child!.data.detailedContent) {
|
||||
const detailIndent = ' '.repeat(level)
|
||||
markdown += `${detailIndent}> ${child!.data.detailedContent.replace(/\n/g, `\n${detailIndent}> `)}\n`
|
||||
}
|
||||
buildMarkdown(child!.id, level + 1)
|
||||
})
|
||||
}
|
||||
|
||||
buildMarkdown(rootNode.id, 1)
|
||||
|
||||
const blob = new Blob([markdown], { type: 'text/markdown' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.download = `thinkflow-${rootNode.data.label}-${Date.now()}.md`
|
||||
link.href = url
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* 总结:基于当前所有节点信息生成一段总结文本
|
||||
* - 结果展示在 SummaryModal
|
||||
*/
|
||||
const generateSummary = async () => {
|
||||
if (flowNodes.value.length === 0) return
|
||||
|
||||
showSummaryModal.value = true
|
||||
isSummarizing.value = true
|
||||
summaryContent.value = ''
|
||||
|
||||
const nodesInfo = flowNodes.value.map(n => ({
|
||||
label: n.data.label,
|
||||
description: n.data.description,
|
||||
type: n.data.type
|
||||
}))
|
||||
|
||||
const useConfig = apiConfig.mode === 'default' ? DEFAULT_CONFIG.chat : apiConfig.chat
|
||||
const finalApiKey = apiConfig.mode === 'default' ? useConfig.apiKey || API_KEY : useConfig.apiKey
|
||||
|
||||
try {
|
||||
const response = await fetch(useConfig.baseUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${finalApiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: useConfig.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: t('prompts.summaryPrompt', {
|
||||
nodes: JSON.stringify(nodesInfo, null, 2)
|
||||
})
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error('Summary request failed')
|
||||
|
||||
const data = await response.json()
|
||||
summaryContent.value = data.choices[0].message.content
|
||||
} catch (error) {
|
||||
console.error('Summary Generation Error:', error)
|
||||
summaryContent.value = t('common.error.unknown')
|
||||
} finally {
|
||||
isSummarizing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片:为指定节点生成配图
|
||||
* - 节点会进入 isImageLoading 状态
|
||||
* - 成功后写入 imageUrl,用于节点卡片与预览弹窗展示
|
||||
*/
|
||||
const generateNodeImage = async (nodeId: string, prompt: string) => {
|
||||
const node = flowNodes.value.find(n => n.id === nodeId)
|
||||
if (!node || node.data.isImageLoading) return
|
||||
|
||||
updateNode(nodeId, ((n: any) => ({ ...n, selected: true, zIndex: 1000 })) as any)
|
||||
|
||||
updateNode(nodeId, { data: { ...node.data, isImageLoading: true, error: null } })
|
||||
|
||||
const useConfig = apiConfig.mode === 'default' ? DEFAULT_CONFIG.image : apiConfig.image
|
||||
const finalApiKey = apiConfig.mode === 'default' ? useConfig.apiKey || API_KEY : useConfig.apiKey
|
||||
|
||||
try {
|
||||
const response = await fetch(useConfig.baseUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${finalApiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: useConfig.model,
|
||||
prompt: t('prompts.image', { prompt })
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error: any = new Error('Image request failed')
|
||||
error.status = response.status
|
||||
throw error
|
||||
}
|
||||
const data = await response.json()
|
||||
const imageUrl = data.data[0].url
|
||||
|
||||
updateNode(nodeId, { data: { ...node.data, imageUrl, isImageLoading: false, error: null } })
|
||||
} catch (error: any) {
|
||||
console.error('Image Generation Error:', error)
|
||||
updateNode(nodeId, { data: { ...node.data, isImageLoading: false, error: getErrorMessage(error) } })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 深挖:针对某个节点生成更详细的解释/拓展内容
|
||||
* - 若已有 detailedContent 且未展开,则直接展开(避免重复请求)
|
||||
*/
|
||||
const deepDive = async (nodeId: string, topic: string) => {
|
||||
const node = flowNodes.value.find(n => n.id === nodeId)
|
||||
if (!node) return
|
||||
|
||||
updateNode(nodeId, ((n: any) => ({ ...n, selected: true, zIndex: 1000 })) as any)
|
||||
|
||||
if (node.data.detailedContent && !node.data.isDetailExpanded) {
|
||||
updateNode(nodeId, { data: { ...node.data, isDetailExpanded: true } })
|
||||
return
|
||||
}
|
||||
|
||||
if (node.data.isDeepDiving) return
|
||||
|
||||
updateNode(nodeId, { data: { ...node.data, isDeepDiving: true, isDetailExpanded: true, error: null } })
|
||||
|
||||
const useConfig = apiConfig.mode === 'default' ? DEFAULT_CONFIG.chat : apiConfig.chat
|
||||
const finalApiKey = apiConfig.mode === 'default' ? useConfig.apiKey || API_KEY : useConfig.apiKey
|
||||
|
||||
try {
|
||||
const response = await fetch(useConfig.baseUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${finalApiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: useConfig.model,
|
||||
messages: [{ role: 'user', content: t('prompts.deepDivePrompt', { topic }) }]
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error: any = new Error('Deep dive request failed')
|
||||
error.status = response.status
|
||||
throw error
|
||||
}
|
||||
const data = await response.json()
|
||||
const content = data.choices[0].message.content
|
||||
|
||||
updateNode(nodeId, { data: { ...node.data, detailedContent: content, isDeepDiving: false, error: null } })
|
||||
} catch (error: any) {
|
||||
console.error('Deep Dive Error:', error)
|
||||
updateNode(nodeId, { data: { ...node.data, isDeepDiving: false, error: getErrorMessage(error) } })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成从根到指定节点的“上下文路径”文本,用于二次扩展时给模型更明确的上下文
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 将模型返回的子节点数组写入画布,并连边到 parentId
|
||||
*/
|
||||
const processSubNodes = (subNodes: any[], parentId: string, baseX: number, baseY: number) => {
|
||||
subNodes.forEach((item: any, index: number) => {
|
||||
const childId = `node-${Date.now()}-${index}`
|
||||
const offsetX = 450
|
||||
const offsetY = (index - (subNodes.length - 1) / 2) * 280
|
||||
|
||||
addNodes({
|
||||
id: childId,
|
||||
type: 'window',
|
||||
position: { x: baseX + offsetX, y: baseY + offsetY },
|
||||
data: {
|
||||
label: item.text,
|
||||
description: item.description,
|
||||
type: 'child',
|
||||
followUp: '',
|
||||
isExpanding: false,
|
||||
isImageLoading: false,
|
||||
isTitleExpanded: false,
|
||||
error: null
|
||||
},
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left
|
||||
})
|
||||
|
||||
addEdges({
|
||||
id: `e-${parentId}-${childId}`,
|
||||
source: parentId,
|
||||
target: childId,
|
||||
animated: true,
|
||||
style: { stroke: config.edgeColor, strokeWidth: 2 },
|
||||
markerEnd: MarkerType.ArrowClosed
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成/扩展节点
|
||||
* - 无 parentNode:创建 root 节点并生成第一层子节点
|
||||
* - 有 parentNode:基于选中节点生成下一层子节点(支持 followUp 作为追加需求)
|
||||
*
|
||||
* 接口约定:
|
||||
* - chat completion 返回 message.content 为 JSON 字符串
|
||||
* - response_format: { type: 'json_object' } 用于提高 JSON 输出稳定性
|
||||
*/
|
||||
const expandIdea = async (param?: any, customInput?: string) => {
|
||||
const parentNode = param && param.id ? param : undefined
|
||||
const text = customInput || (parentNode ? parentNode.data.label : ideaInput.value)
|
||||
|
||||
if (!text || (parentNode ? parentNode.data.isExpanding : isLoading.value)) return
|
||||
|
||||
let currentParentId = parentNode?.id
|
||||
|
||||
if (!parentNode) {
|
||||
isLoading.value = true
|
||||
setNodes([])
|
||||
setEdges([])
|
||||
|
||||
const rootId = 'root-' + Date.now()
|
||||
currentParentId = rootId
|
||||
|
||||
addNodes({
|
||||
id: rootId,
|
||||
type: 'window',
|
||||
position: { x: 50, y: 300 },
|
||||
data: {
|
||||
label: text,
|
||||
description: t('node.coreIdea'),
|
||||
type: 'root',
|
||||
isExpanding: true,
|
||||
isTitleExpanded: false,
|
||||
followUp: '',
|
||||
error: null
|
||||
},
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left
|
||||
})
|
||||
|
||||
ideaInput.value = ''
|
||||
} else {
|
||||
const node = flowNodes.value.find(n => n.id === parentNode.id)
|
||||
if (node) {
|
||||
updateNode(parentNode.id, {
|
||||
data: {
|
||||
...node.data,
|
||||
isExpanding: true,
|
||||
isDetailExpanded: false,
|
||||
error: null
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const systemPrompt = t('prompts.system')
|
||||
|
||||
let userMessage = ''
|
||||
if (parentNode) {
|
||||
const path = findPathToNode(parentNode.id)
|
||||
userMessage = `[${t('prompts.contextPath')}]: ${path.join(' -> ')}\n[${t('prompts.selectedNode')}]: ${parentNode.data.label}\n[${t('prompts.newRequirement')}]: ${customInput || t('prompts.continue')}`
|
||||
} else {
|
||||
userMessage = `${t('prompts.coreIdeaPrefix')}: ${text}`
|
||||
}
|
||||
|
||||
const useConfig = apiConfig.mode === 'default' ? DEFAULT_CONFIG.chat : apiConfig.chat
|
||||
const finalApiKey = apiConfig.mode === 'default' ? useConfig.apiKey || API_KEY : useConfig.apiKey
|
||||
|
||||
try {
|
||||
const response = await fetch(useConfig.baseUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${finalApiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: useConfig.model,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userMessage }
|
||||
],
|
||||
response_format: { type: 'json_object' },
|
||||
temperature: 0.8
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error: any = new Error('AI request failed')
|
||||
error.status = response.status
|
||||
throw error
|
||||
}
|
||||
const data = await response.json()
|
||||
const result = JSON.parse(data.choices[0].message.content)
|
||||
|
||||
const parentNodeObj = flowNodes.value.find(n => n.id === currentParentId)
|
||||
const startX = parentNodeObj ? parentNodeObj.position.x : 50
|
||||
const startY = parentNodeObj ? parentNodeObj.position.y : 300
|
||||
|
||||
processSubNodes(result.nodes, currentParentId, startX, startY)
|
||||
|
||||
if (!parentNode) {
|
||||
setTimeout(() => {
|
||||
const childEdges = flowEdges.value.filter(e => e.source === currentParentId)
|
||||
const childIds = childEdges.map(e => e.target)
|
||||
|
||||
const nodesToFit = [currentParentId, ...childIds.slice(0, 3)]
|
||||
|
||||
fitView({
|
||||
nodes: nodesToFit,
|
||||
padding: 0.25,
|
||||
duration: 1000
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Expansion Error:', error)
|
||||
const node = flowNodes.value.find(n => n.id === currentParentId)
|
||||
if (node) {
|
||||
updateNode(currentParentId, { data: { ...node.data, error: getErrorMessage(error) } })
|
||||
}
|
||||
} finally {
|
||||
const node = flowNodes.value.find(n => n.id === currentParentId)
|
||||
if (node) {
|
||||
node.data.isExpanding = false
|
||||
}
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即清空当前画布与输入,并关闭确认弹窗
|
||||
*/
|
||||
const executeReset = () => {
|
||||
ideaInput.value = ''
|
||||
setNodes([])
|
||||
setEdges([])
|
||||
showResetConfirm.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 新会话入口
|
||||
* - 若当前已有节点:弹出二次确认
|
||||
* - 若为空:直接清空(等价 executeReset)
|
||||
*/
|
||||
const startNewSession = () => {
|
||||
if (flowNodes.value.length > 0) {
|
||||
showResetConfirm.value = true
|
||||
return
|
||||
}
|
||||
executeReset()
|
||||
}
|
||||
|
||||
return {
|
||||
apiConfig,
|
||||
DEFAULT_CONFIG,
|
||||
showSettings,
|
||||
ideaInput,
|
||||
isLoading,
|
||||
previewImageUrl,
|
||||
showResetConfirm,
|
||||
showSummaryModal,
|
||||
isSummarizing,
|
||||
summaryContent,
|
||||
panOnDrag,
|
||||
isSpacePressed,
|
||||
config,
|
||||
flowNodes,
|
||||
flowEdges,
|
||||
activeNodeId,
|
||||
activePath,
|
||||
updateNode,
|
||||
fitView,
|
||||
resetLayout,
|
||||
centerRoot,
|
||||
startNewSession,
|
||||
executeReset,
|
||||
generateSummary,
|
||||
exportMarkdown,
|
||||
generateNodeImage,
|
||||
deepDive,
|
||||
expandIdea
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user