diff --git a/src/App.vue b/src/App.vue index 8d686c0..263fe7b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -119,6 +119,7 @@ const hoveredNodeId = ref(null) const focusedNodeId = ref(null) const draggingNodeId = ref(null) const previewImageUrl = ref(null) +const showResetConfirm = ref(false) // 画布控制状态 const panOnDrag = ref(true) @@ -248,6 +249,19 @@ watch( { 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') +} + /** * 聚焦到根节点 */ @@ -327,7 +341,10 @@ const generateNodeImage = async (nodeId: string, prompt: string) => { const node = flowNodes.value.find(n => n.id === nodeId) if (!node || node.data.isImageLoading) return - updateNode(nodeId, { data: { ...node.data, isImageLoading: true } }) + // 激活节点 + updateNode(nodeId, { selected: true, zIndex: 1000 }) + + updateNode(nodeId, { data: { ...node.data, isImageLoading: true, error: null } }) const useConfig = apiConfig.mode === 'default' ? DEFAULT_CONFIG.image : apiConfig.image // 自定义模式下完全使用用户输入,不进行项目 Key 兜底 @@ -346,14 +363,70 @@ const generateNodeImage = async (nodeId: string, prompt: string) => { }) }) - if (!response.ok) throw new Error('Image request failed') + 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 } }) - } catch (error) { + 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 } }) + updateNode(nodeId, { data: { ...node.data, isImageLoading: false, error: getErrorMessage(error) } }) + } +} + +/** + * 深度解析节点内容 + */ +const deepDive = async (nodeId: string, topic: string) => { + const node = flowNodes.value.find(n => n.id === nodeId) + if (!node) return + + // 激活节点并置顶 + updateNode(nodeId, { selected: true, zIndex: 1000 }) + + // 如果已经有内容且当前是收起状态,则直接展开 + 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) } }) } } @@ -408,7 +481,9 @@ const expandIdea = async (param?: any, customInput?: string) => { description: t('node.coreIdea'), type: 'root', isExpanding: true, - followUp: '' + isTitleExpanded: false, + followUp: '', + error: null }, sourcePosition: Position.Right, targetPosition: Position.Left @@ -417,7 +492,16 @@ const expandIdea = async (param?: any, customInput?: string) => { ideaInput.value = '' } else { const node = flowNodes.value.find(n => n.id === parentNode.id) - if (node) node.data.isExpanding = true + if (node) { + updateNode(parentNode.id, { + data: { + ...node.data, + isExpanding: true, + isDetailExpanded: false, // 开始发散时隐藏详情 + error: null + } + }) + } } const systemPrompt = t('prompts.system') @@ -452,7 +536,11 @@ const expandIdea = async (param?: any, customInput?: string) => { }) }) - if (!response.ok) throw new Error('AI request failed') + 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) @@ -462,8 +550,29 @@ const expandIdea = async (param?: any, customInput?: string) => { const startY = parentNodeObj ? parentNodeObj.position.y : 300 processSubNodes(result.nodes, currentParentId, startX, startY) - } catch (error) { + + // 首次输入后,优化缩放比例:展示根节点和大约3个二级节点 + if (!parentNode) { + setTimeout(() => { + const childEdges = flowEdges.value.filter(e => e.source === currentParentId) + const childIds = childEdges.map(e => e.target) + + // 选取前3个二级节点作为缩放参考,这样可以保证缩放比例适中(约看到3个二级的大小) + 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) { @@ -489,7 +598,9 @@ const processSubNodes = (subNodes: any[], parentId: string, baseX: number, baseY type: 'child', followUp: '', isExpanding: false, - isImageLoading: false + isImageLoading: false, + isTitleExpanded: false, + error: null }, sourcePosition: Position.Right, targetPosition: Position.Left @@ -506,10 +617,19 @@ const processSubNodes = (subNodes: any[], parentId: string, baseX: number, baseY }) } -const startNewSession = () => { +const executeReset = () => { ideaInput.value = '' setNodes([]) setEdges([]) + showResetConfirm.value = false +} + +const startNewSession = () => { + if (flowNodes.value.length > 0) { + showResetConfirm.value = true + return + } + executeReset() } @@ -527,12 +647,6 @@ const startNewSession = () => {