优化代码
This commit is contained in:
307
src/App.vue
307
src/App.vue
@@ -119,6 +119,7 @@ const hoveredNodeId = ref<string | null>(null)
|
|||||||
const focusedNodeId = ref<string | null>(null)
|
const focusedNodeId = ref<string | null>(null)
|
||||||
const draggingNodeId = ref<string | null>(null)
|
const draggingNodeId = ref<string | null>(null)
|
||||||
const previewImageUrl = ref<string | null>(null)
|
const previewImageUrl = ref<string | null>(null)
|
||||||
|
const showResetConfirm = ref(false)
|
||||||
|
|
||||||
// 画布控制状态
|
// 画布控制状态
|
||||||
const panOnDrag = ref(true)
|
const panOnDrag = ref(true)
|
||||||
@@ -248,6 +249,19 @@ watch(
|
|||||||
{ immediate: true }
|
{ 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)
|
const node = flowNodes.value.find(n => n.id === nodeId)
|
||||||
if (!node || node.data.isImageLoading) return
|
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
|
const useConfig = apiConfig.mode === 'default' ? DEFAULT_CONFIG.image : apiConfig.image
|
||||||
// 自定义模式下完全使用用户输入,不进行项目 Key 兜底
|
// 自定义模式下完全使用用户输入,不进行项目 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 data = await response.json()
|
||||||
const imageUrl = data.data[0].url
|
const imageUrl = data.data[0].url
|
||||||
|
|
||||||
updateNode(nodeId, { data: { ...node.data, imageUrl, isImageLoading: false } })
|
updateNode(nodeId, { data: { ...node.data, imageUrl, isImageLoading: false, error: null } })
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Image Generation Error:', error)
|
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'),
|
description: t('node.coreIdea'),
|
||||||
type: 'root',
|
type: 'root',
|
||||||
isExpanding: true,
|
isExpanding: true,
|
||||||
followUp: ''
|
isTitleExpanded: false,
|
||||||
|
followUp: '',
|
||||||
|
error: null
|
||||||
},
|
},
|
||||||
sourcePosition: Position.Right,
|
sourcePosition: Position.Right,
|
||||||
targetPosition: Position.Left
|
targetPosition: Position.Left
|
||||||
@@ -417,7 +492,16 @@ const expandIdea = async (param?: any, customInput?: string) => {
|
|||||||
ideaInput.value = ''
|
ideaInput.value = ''
|
||||||
} else {
|
} else {
|
||||||
const node = flowNodes.value.find(n => n.id === parentNode.id)
|
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')
|
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 data = await response.json()
|
||||||
const result = JSON.parse(data.choices[0].message.content)
|
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
|
const startY = parentNodeObj ? parentNodeObj.position.y : 300
|
||||||
|
|
||||||
processSubNodes(result.nodes, currentParentId, startX, startY)
|
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)
|
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 {
|
} finally {
|
||||||
const node = flowNodes.value.find(n => n.id === currentParentId)
|
const node = flowNodes.value.find(n => n.id === currentParentId)
|
||||||
if (node) {
|
if (node) {
|
||||||
@@ -489,7 +598,9 @@ const processSubNodes = (subNodes: any[], parentId: string, baseX: number, baseY
|
|||||||
type: 'child',
|
type: 'child',
|
||||||
followUp: '',
|
followUp: '',
|
||||||
isExpanding: false,
|
isExpanding: false,
|
||||||
isImageLoading: false
|
isImageLoading: false,
|
||||||
|
isTitleExpanded: false,
|
||||||
|
error: null
|
||||||
},
|
},
|
||||||
sourcePosition: Position.Right,
|
sourcePosition: Position.Right,
|
||||||
targetPosition: Position.Left
|
targetPosition: Position.Left
|
||||||
@@ -506,10 +617,19 @@ const processSubNodes = (subNodes: any[], parentId: string, baseX: number, baseY
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const startNewSession = () => {
|
const executeReset = () => {
|
||||||
ideaInput.value = ''
|
ideaInput.value = ''
|
||||||
setNodes([])
|
setNodes([])
|
||||||
setEdges([])
|
setEdges([])
|
||||||
|
showResetConfirm.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const startNewSession = () => {
|
||||||
|
if (flowNodes.value.length > 0) {
|
||||||
|
showResetConfirm.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
executeReset()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -527,12 +647,6 @@ const startNewSession = () => {
|
|||||||
|
|
||||||
<!-- 桌面端工具按钮组 -->
|
<!-- 桌面端工具按钮组 -->
|
||||||
<div class="hidden md:flex items-center gap-2">
|
<div class="hidden md:flex items-center gap-2">
|
||||||
<!-- 重置画布 -->
|
|
||||||
<button @click="startNewSession" class="toolbar-btn text-red-500 hover:bg-red-50 border-red-100 flex-shrink-0" :title="t('nav.reset')">
|
|
||||||
<Trash2 class="w-3.5 h-3.5 md:w-4 h-4" />
|
|
||||||
<span>{{ t('nav.reset') }}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="h-4 w-[1px] bg-slate-100 mx-1 flex-shrink-0"></div>
|
<div class="h-4 w-[1px] bg-slate-100 mx-1 flex-shrink-0"></div>
|
||||||
|
|
||||||
<!-- 布局控制 -->
|
<!-- 布局控制 -->
|
||||||
@@ -553,6 +667,14 @@ const startNewSession = () => {
|
|||||||
|
|
||||||
<div class="h-4 w-[1px] bg-slate-100 mx-1 flex-shrink-0"></div>
|
<div class="h-4 w-[1px] bg-slate-100 mx-1 flex-shrink-0"></div>
|
||||||
|
|
||||||
|
<!-- 重置画布 -->
|
||||||
|
<button @click="startNewSession" class="toolbar-btn text-red-500 hover:bg-red-50 border-red-100 flex-shrink-0" :title="t('nav.reset')">
|
||||||
|
<Trash2 class="w-3.5 h-3.5 md:w-4 h-4" />
|
||||||
|
<span>{{ t('nav.reset') }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="h-4 w-[1px] bg-slate-100 mx-1 flex-shrink-0"></div>
|
||||||
|
|
||||||
<!-- 连线颜色 -->
|
<!-- 连线颜色 -->
|
||||||
<div class="flex items-center gap-2 px-2 md:px-3 py-1.5 bg-slate-50 rounded-lg border border-slate-100 flex-shrink-0">
|
<div class="flex items-center gap-2 px-2 md:px-3 py-1.5 bg-slate-50 rounded-lg border border-slate-100 flex-shrink-0">
|
||||||
<Palette class="w-3 h-3 md:w-3.5 h-3.5 text-slate-400" />
|
<Palette class="w-3 h-3 md:w-3.5 h-3.5 text-slate-400" />
|
||||||
@@ -636,12 +758,6 @@ const startNewSession = () => {
|
|||||||
leave-to-class="transform -translate-y-4 opacity-0"
|
leave-to-class="transform -translate-y-4 opacity-0"
|
||||||
>
|
>
|
||||||
<div v-if="isToolsExpanded" class="md:hidden absolute top-[57px] left-0 right-0 bg-white/95 backdrop-blur-md border-b border-slate-200 shadow-xl z-40 py-4 px-4 flex flex-wrap gap-3 justify-center">
|
<div v-if="isToolsExpanded" class="md:hidden absolute top-[57px] left-0 right-0 bg-white/95 backdrop-blur-md border-b border-slate-200 shadow-xl z-40 py-4 px-4 flex flex-wrap gap-3 justify-center">
|
||||||
<!-- 重置画布 -->
|
|
||||||
<button @click="startNewSession(); isToolsExpanded = false" class="toolbar-btn text-red-500 hover:bg-red-50 border-red-100" :title="t('nav.reset')">
|
|
||||||
<Trash2 class="w-4 h-4" />
|
|
||||||
<span>{{ t('nav.reset') }}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- 布局控制 -->
|
<!-- 布局控制 -->
|
||||||
<button @click="fitView({ padding: 0.2, duration: 800 }); isToolsExpanded = false" class="toolbar-btn text-blue-500 hover:bg-blue-50 border-blue-100" :title="t('nav.fit')">
|
<button @click="fitView({ padding: 0.2, duration: 800 }); isToolsExpanded = false" class="toolbar-btn text-blue-500 hover:bg-blue-50 border-blue-100" :title="t('nav.fit')">
|
||||||
<Focus class="w-4 h-4" />
|
<Focus class="w-4 h-4" />
|
||||||
@@ -658,6 +774,12 @@ const startNewSession = () => {
|
|||||||
<span>{{ t('nav.center') }}</span>
|
<span>{{ t('nav.center') }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- 重置画布 -->
|
||||||
|
<button @click="startNewSession(); isToolsExpanded = false" class="toolbar-btn text-red-500 hover:bg-red-50 border-red-100" :title="t('nav.reset')">
|
||||||
|
<Trash2 class="w-4 h-4" />
|
||||||
|
<span>{{ t('nav.reset') }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- 连线颜色 -->
|
<!-- 连线颜色 -->
|
||||||
<div class="flex items-center gap-2 px-3 py-1.5 bg-slate-50 rounded-lg border border-slate-100">
|
<div class="flex items-center gap-2 px-3 py-1.5 bg-slate-50 rounded-lg border border-slate-100">
|
||||||
<Palette class="w-4 h-4 text-slate-400" />
|
<Palette class="w-4 h-4 text-slate-400" />
|
||||||
@@ -706,7 +828,12 @@ const startNewSession = () => {
|
|||||||
:pan-on-drag="panOnDrag"
|
:pan-on-drag="panOnDrag"
|
||||||
:selection-key-code="'Shift'"
|
:selection-key-code="'Shift'"
|
||||||
>
|
>
|
||||||
<Background :variant="config.backgroundVariant" pattern-color="#f2f2f2" :gap="24" :size="0.5" />
|
<Background
|
||||||
|
:variant="config.backgroundVariant"
|
||||||
|
:pattern-color="config.backgroundVariant === BackgroundVariant.Dots ? '#cbd5e1' : '#f1f5f9'"
|
||||||
|
:gap="24"
|
||||||
|
:size="config.backgroundVariant === BackgroundVariant.Dots ? 1 : 0.5"
|
||||||
|
/>
|
||||||
<Controls v-if="config.showControls" />
|
<Controls v-if="config.showControls" />
|
||||||
<MiniMap v-if="config.showMiniMap" pannable zoomable />
|
<MiniMap v-if="config.showMiniMap" pannable zoomable />
|
||||||
|
|
||||||
@@ -716,7 +843,8 @@ const startNewSession = () => {
|
|||||||
class="window-node group transition-all duration-500"
|
class="window-node group transition-all duration-500"
|
||||||
:class="{
|
:class="{
|
||||||
'opacity-40 grayscale-[0.4] blur-[0.5px] scale-[0.98] pointer-events-none': activeNodeId && !activePath.nodeIds.has(id),
|
'opacity-40 grayscale-[0.4] blur-[0.5px] scale-[0.98] pointer-events-none': activeNodeId && !activePath.nodeIds.has(id),
|
||||||
'opacity-100 grayscale-0 blur-0 scale-105 z-50 ring-2 ring-offset-4': activePath.nodeIds.has(id)
|
'opacity-100 grayscale-0 blur-0 scale-105 z-50 ring-2 ring-offset-4': activePath.nodeIds.has(id),
|
||||||
|
'!w-[450px]': data.isDetailExpanded
|
||||||
}"
|
}"
|
||||||
:style="{
|
:style="{
|
||||||
borderColor: activePath.nodeIds.has(id) ? config.edgeColor : config.edgeColor + '40',
|
borderColor: activePath.nodeIds.has(id) ? config.edgeColor : config.edgeColor + '40',
|
||||||
@@ -784,14 +912,38 @@ const startNewSession = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-start gap-2 mb-2">
|
||||||
<span class="font-bold" :style="{ color: config.edgeColor }">></span>
|
<span class="font-bold shrink-0 mt-0.5" :style="{ color: config.edgeColor }">></span>
|
||||||
<h3 class="font-black text-slate-900 tracking-tight truncate">{{ data.label }}</h3>
|
<h3
|
||||||
|
class="font-black text-slate-900 tracking-tight cursor-pointer hover:text-orange-600 transition-colors"
|
||||||
|
:class="data.isTitleExpanded ? 'whitespace-normal' : 'truncate'"
|
||||||
|
@click.stop="updateNode(id, { data: { ...data, isTitleExpanded: !data.isTitleExpanded } })"
|
||||||
|
>
|
||||||
|
{{ data.label }}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[10px] text-slate-500 leading-relaxed font-medium line-clamp-3">
|
<p class="text-[10px] text-slate-500 leading-relaxed font-medium line-clamp-3">
|
||||||
{{ data.description }}
|
{{ data.description }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- 错误反馈显示 -->
|
||||||
|
<div v-if="data.error" class="mt-3 p-2.5 bg-red-50 border border-red-100 rounded-lg animate-in fade-in slide-in-from-top-1 duration-300">
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<Shield class="w-3.5 h-3.5 text-red-500 shrink-0 mt-0.5" />
|
||||||
|
<div class="flex-grow space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-red-600 leading-tight">{{ t('common.error.title') }}</p>
|
||||||
|
<p class="text-[9px] text-red-500 leading-relaxed">{{ data.error }}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click.stop="data.imageUrl === null && data.isImageLoading === false ? generateNodeImage(id, data.label) : expandIdea({ id, data, position: flowNodes.find(n => n.id === id)?.position })"
|
||||||
|
class="p-1 hover:bg-red-100 rounded transition-colors"
|
||||||
|
:title="t('common.error.retry')"
|
||||||
|
>
|
||||||
|
<RefreshCw class="w-3 h-3 text-red-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Node Actions -->
|
<!-- Node Actions -->
|
||||||
<div class="pt-3 mt-3 border-t border-slate-50">
|
<div class="pt-3 mt-3 border-t border-slate-50">
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
@@ -803,6 +955,14 @@ const startNewSession = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
@click.stop="deepDive(id, data.label)"
|
||||||
|
class="action-btn text-orange-500 hover:bg-orange-50"
|
||||||
|
:title="t('node.deepDive')"
|
||||||
|
>
|
||||||
|
<BookOpen class="w-2.5 h-2.5" />
|
||||||
|
<span>{{ t('node.deepDive') }}</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!data.imageUrl && !data.isImageLoading"
|
v-if="!data.imageUrl && !data.isImageLoading"
|
||||||
@click.stop="generateNodeImage(id, data.label)"
|
@click.stop="generateNodeImage(id, data.label)"
|
||||||
@@ -814,6 +974,26 @@ const startNewSession = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Detailed Content Display -->
|
||||||
|
<div v-if="data.isDetailExpanded" class="mb-4 pt-4 border-t border-slate-100 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="text-[9px] font-black text-slate-400 uppercase tracking-widest">{{ t('node.deepDive') }}</span>
|
||||||
|
<button @click.stop="updateNode(id, { data: { ...data, isDetailExpanded: false } })" class="text-slate-300 hover:text-slate-500">
|
||||||
|
<X class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="data.isDeepDiving" class="flex flex-col items-center py-6">
|
||||||
|
<div class="relative mb-3">
|
||||||
|
<RefreshCw class="w-6 h-6 text-orange-400 animate-spin" />
|
||||||
|
<div class="absolute inset-0 blur-lg bg-orange-200 opacity-50 animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-[9px] font-black text-slate-300 uppercase tracking-widest animate-pulse">{{ t('common.loading') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-[11px] text-slate-600 leading-relaxed font-medium whitespace-pre-wrap max-h-[350px] overflow-y-auto custom-scrollbar pr-2 selection:bg-orange-100 nowheel">
|
||||||
|
{{ data.detailedContent }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Follow-up Input -->
|
<!-- Follow-up Input -->
|
||||||
<div class="relative group/input">
|
<div class="relative group/input">
|
||||||
<div
|
<div
|
||||||
@@ -1000,14 +1180,55 @@ const startNewSession = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 全局图片预览弹窗 -->
|
<!-- 全局图片预览弹窗 -->
|
||||||
<div v-if="previewImageUrl" class="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm p-10" @click="previewImageUrl = null">
|
<Transition name="fade">
|
||||||
<div class="relative max-w-full max-h-full rounded-lg overflow-hidden shadow-2xl animate-in fade-in zoom-in duration-300" @click.stop>
|
<div v-if="previewImageUrl" class="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm p-10" @click="previewImageUrl = null">
|
||||||
<button @click="previewImageUrl = null" class="absolute top-4 right-4 p-2 bg-black/50 hover:bg-black/70 text-white rounded-full transition-colors z-10">
|
<div class="relative max-w-full max-h-full rounded-lg overflow-hidden shadow-2xl animate-in fade-in zoom-in duration-300" @click.stop>
|
||||||
<X class="w-5 h-5" />
|
<button @click="previewImageUrl = null" class="absolute top-4 right-4 p-2 bg-black/50 hover:bg-black/70 text-white rounded-full transition-colors z-10">
|
||||||
</button>
|
<X class="w-5 h-5" />
|
||||||
<img :src="previewImageUrl" class="max-w-screen max-h-screen object-contain" />
|
</button>
|
||||||
|
<img :src="previewImageUrl" class="max-w-screen max-h-screen object-contain" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Transition>
|
||||||
|
|
||||||
|
<!-- 自定义重置确认弹窗 -->
|
||||||
|
<Transition name="fade">
|
||||||
|
<div v-if="showResetConfirm" class="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
||||||
|
<div class="absolute inset-0 bg-slate-900/40 backdrop-blur-sm" @click="showResetConfirm = false"></div>
|
||||||
|
<div class="relative bg-white rounded-2xl shadow-2xl border border-slate-100 p-6 w-full max-w-sm overflow-hidden group animate-in zoom-in duration-300">
|
||||||
|
<!-- 背景装饰 -->
|
||||||
|
<div class="absolute -top-12 -right-12 w-24 h-24 bg-orange-50 rounded-full blur-2xl group-hover:bg-orange-100 transition-colors"></div>
|
||||||
|
|
||||||
|
<div class="relative flex flex-col items-center text-center space-y-4">
|
||||||
|
<div class="w-16 h-16 bg-orange-50 rounded-2xl flex items-center justify-center text-orange-500 mb-2 ring-4 ring-orange-50/50">
|
||||||
|
<RefreshCw class="w-8 h-8 animate-spin-slow" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h3 class="text-lg font-bold text-slate-800 tracking-tight">{{ t('nav.reset') }}</h3>
|
||||||
|
<p class="text-sm text-slate-500 leading-relaxed px-4">
|
||||||
|
{{ t('common.confirmReset') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3 w-full pt-2">
|
||||||
|
<button
|
||||||
|
@click="showResetConfirm = false"
|
||||||
|
class="flex-1 px-4 py-2.5 rounded-xl border border-slate-200 text-slate-600 font-medium hover:bg-slate-50 transition-colors active:scale-95"
|
||||||
|
>
|
||||||
|
{{ t('common.cancel') || 'Cancel' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="executeReset"
|
||||||
|
class="flex-1 px-4 py-2.5 rounded-xl bg-orange-500 text-white font-medium hover:bg-orange-600 shadow-lg shadow-orange-500/30 transition-all active:scale-95"
|
||||||
|
>
|
||||||
|
{{ t('common.confirm') || 'Confirm' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部全局操作栏 -->
|
<!-- 底部全局操作栏 -->
|
||||||
@@ -1097,11 +1318,27 @@ body {
|
|||||||
@apply border-current bg-opacity-10;
|
@apply border-current bg-opacity-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
@apply bg-slate-200 rounded-full hover:bg-slate-300 transition-colors;
|
||||||
|
}
|
||||||
|
|
||||||
/* VueFlow Overrides */
|
/* VueFlow Overrides */
|
||||||
.vue-flow__node-window {
|
.vue-flow__node-window {
|
||||||
@apply p-0 border-none bg-transparent !important;
|
@apply p-0 border-none bg-transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vue-flow__node.selected {
|
||||||
|
z-index: 1000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.vue-flow__controls {
|
.vue-flow__controls {
|
||||||
@apply !bg-white !border-slate-200 !shadow-xl !rounded-lg !left-4 md:!left-6 !bottom-28 md:!bottom-6 !transition-all;
|
@apply !bg-white !border-slate-200 !shadow-xl !rounded-lg !left-4 md:!left-6 !bottom-28 md:!bottom-6 !transition-all;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,25 @@
|
|||||||
"settings": "API Settings",
|
"settings": "API Settings",
|
||||||
"default": "DEFAULT",
|
"default": "DEFAULT",
|
||||||
"custom": "CUSTOM",
|
"custom": "CUSTOM",
|
||||||
"save": "Save",
|
"save": "Save & Close",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"generating": "Generating...",
|
"generating": "Generating...",
|
||||||
"expanding": "Expanding Idea...",
|
"expanding": "Expanding Idea...",
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
"signin": "SIGN IN",
|
"signin": "SIGN IN",
|
||||||
"tools": "Tools"
|
"tools": "Tools",
|
||||||
|
"confirmReset": "Are you sure you want to reset the canvas? All current ideas will be lost.",
|
||||||
|
"error": {
|
||||||
|
"title": "Request Failed",
|
||||||
|
"cors": "Request blocked, possibly due to CORS policy or network connection issues.",
|
||||||
|
"rateLimit": "Too many requests (429), please try again later.",
|
||||||
|
"badRequest": "Bad request (400), please check your configuration.",
|
||||||
|
"serverError": "Internal server error (500), please try again later.",
|
||||||
|
"unknown": "An unknown error occurred, please check your network or API config.",
|
||||||
|
"retry": "Retry"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"title": "ThinkFlow AI",
|
"title": "ThinkFlow AI",
|
||||||
@@ -46,6 +57,7 @@
|
|||||||
"coreIdea": "Core Idea",
|
"coreIdea": "Core Idea",
|
||||||
"followUp": "Ask a follow-up...",
|
"followUp": "Ask a follow-up...",
|
||||||
"imgAction": "IMG",
|
"imgAction": "IMG",
|
||||||
|
"deepDive": "Detail",
|
||||||
"view": "View",
|
"view": "View",
|
||||||
"regenerate": "Regenerate",
|
"regenerate": "Regenerate",
|
||||||
"mainTitle": "Root Node",
|
"mainTitle": "Root Node",
|
||||||
@@ -53,11 +65,12 @@
|
|||||||
},
|
},
|
||||||
"prompts": {
|
"prompts": {
|
||||||
"system": "You are a mind-mapping assistant that helps users expand their ideas layer by layer into a thinking tree.\n\nWorkflow:\n1. User provides an initial idea (or selects an existing node).\n2. Understand user intent based on the [Thinking Context Path] (trace from root to current node).\n3. Generate 3-5 deeper or related sub-ideas.\n4. Each sub-idea includes a short name and a minimal description.\n\nResponse format must be strict JSON:\n{'{'}\n \"nodes\": [\n {'{'} \"text\": \"Sub-idea 1 Name\", \"description\": \"One-sentence description\" {'}'},\n {'{'} \"text\": \"Sub-idea 2 Name\", \"description\": \"One-sentence description\" {'}'}\n ]\n{'}'}\n\nNote: Return ONLY JSON, no explanation.",
|
"system": "You are a mind-mapping assistant that helps users expand their ideas layer by layer into a thinking tree.\n\nWorkflow:\n1. User provides an initial idea (or selects an existing node).\n2. Understand user intent based on the [Thinking Context Path] (trace from root to current node).\n3. Generate 3-5 deeper or related sub-ideas.\n4. Each sub-idea includes a short name and a minimal description.\n\nResponse format must be strict JSON:\n{'{'}\n \"nodes\": [\n {'{'} \"text\": \"Sub-idea 1 Name\", \"description\": \"One-sentence description\" {'}'},\n {'{'} \"text\": \"Sub-idea 2 Name\", \"description\": \"One-sentence description\" {'}'}\n ]\n{'}'}\n\nNote: Return ONLY JSON, no explanation.",
|
||||||
"image": "Generate an illustration style image representing the theme: {prompt}. Requirements: Simple composition, bright colors, suitable as a visual aid for a mind map. NO Chinese characters or text in the image.",
|
"image": "A beautiful book art illustration, theme: {prompt}. Style: Exquisite picture book aesthetic, warm tones, soft lighting, elegant and poetic composition that tells a story. Details: High-quality brushwork, minimalist yet decorative, suitable as a visual centerpiece for a mind map. STRICT REQUIREMENT: No text, letters, or characters in the image.",
|
||||||
"continue": "Please continue exploring",
|
"continue": "Please continue exploring",
|
||||||
"coreIdeaPrefix": "Core Idea",
|
"coreIdeaPrefix": "Core Idea",
|
||||||
"contextPath": "Thinking Context Path",
|
"contextPath": "Thinking Context Path",
|
||||||
"selectedNode": "Current Selected Node",
|
"selectedNode": "Current Selected Node",
|
||||||
"newRequirement": "User Follow-up/New Requirement"
|
"newRequirement": "User Follow-up/New Requirement",
|
||||||
|
"deepDivePrompt": "Please provide a deep and detailed analysis of 【{topic}】. Requirements:\n1. Clear structure, including background, core principles, key elements, and practical applications.\n2. Professional yet easy-to-understand language.\n3. Total length around 300-500 words.\n4. Output the body text directly, do not use JSON format, and do not include any opening or closing remarks."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,33 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"settings": "API 设置",
|
"settings": "API 设置",
|
||||||
"default": "默认模式",
|
"default": "默认接口",
|
||||||
"custom": "自定义模式",
|
"custom": "自定义接口",
|
||||||
"save": "保存",
|
"save": "保存并关闭",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
|
"confirm": "确定",
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"generating": "生成中...",
|
"generating": "生成中...",
|
||||||
"expanding": "正在展开想法...",
|
"expanding": "发散中...",
|
||||||
"active": "激活",
|
"active": "激活",
|
||||||
"signin": "登录",
|
"signin": "登录",
|
||||||
"tools": "工具箱"
|
"tools": "工具箱",
|
||||||
|
"confirmReset": "确定要重置画布吗?当前的所有想法都将丢失。",
|
||||||
|
"error": {
|
||||||
|
"title": "请求失败",
|
||||||
|
"cors": "网络请求受阻,可能是跨域(CORS)策略或网络连接问题。",
|
||||||
|
"rateLimit": "请求过于频繁(429),请稍后再试。",
|
||||||
|
"badRequest": "请求参数错误(400),请检查配置。",
|
||||||
|
"serverError": "服务器内部错误(500),请稍后再试。",
|
||||||
|
"unknown": "发生未知错误,请检查网络或 API 配置。",
|
||||||
|
"retry": "重试"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"title": "ThinkFlow AI",
|
"title": "ThinkFlow AI",
|
||||||
"subtitle": "思维导图 & AI 助手",
|
"subtitle": "思维导图 & AI 助手",
|
||||||
"placeholder": "在这里输入您的想法...",
|
"placeholder": "在这里输入您的想法...",
|
||||||
"execute": "执行发散",
|
"execute": "发散思维",
|
||||||
"reset": "重置",
|
"reset": "重置",
|
||||||
"fit": "适配",
|
"fit": "适配",
|
||||||
"layout": "布局",
|
"layout": "布局",
|
||||||
@@ -46,6 +57,7 @@
|
|||||||
"coreIdea": "核心想法",
|
"coreIdea": "核心想法",
|
||||||
"followUp": "输入后续问题...",
|
"followUp": "输入后续问题...",
|
||||||
"imgAction": "生图",
|
"imgAction": "生图",
|
||||||
|
"deepDive": "详情",
|
||||||
"view": "查看",
|
"view": "查看",
|
||||||
"regenerate": "重新生成",
|
"regenerate": "重新生成",
|
||||||
"mainTitle": "主节点",
|
"mainTitle": "主节点",
|
||||||
@@ -53,11 +65,12 @@
|
|||||||
},
|
},
|
||||||
"prompts": {
|
"prompts": {
|
||||||
"system": "你是一个思维发散助手,帮助用户将想法逐层展开,构建思维树。\n\n工作流程:\n1. 用户给出一个初始想法(或选择一个已有节点继续追问)。\n2. 你需要根据【思考上下文路径】(即从根节点到当前节点的思考链路)来理解用户的意图。\n3. 生成 3-5 个更深层或相关维度的子想法。\n4. 每个子想法包含简短名称和极简描述。\n\n返回格式必须为严格 JSON:\n{'{'}\n \"nodes\": [\n {'{'} \"text\": \"子想法1名称\", \"description\": \"一句话描述\" {'}'},\n {'{'} \"text\": \"子想法2名称\", \"description\": \"一句话描述\" {'}'}\n ]\n{'}'}\n\n注意:只返回 JSON,不附加解释。",
|
"system": "你是一个思维发散助手,帮助用户将想法逐层展开,构建思维树。\n\n工作流程:\n1. 用户给出一个初始想法(或选择一个已有节点继续追问)。\n2. 你需要根据【思考上下文路径】(即从根节点到当前节点的思考链路)来理解用户的意图。\n3. 生成 3-5 个更深层或相关维度的子想法。\n4. 每个子想法包含简短名称和极简描述。\n\n返回格式必须为严格 JSON:\n{'{'}\n \"nodes\": [\n {'{'} \"text\": \"子想法1名称\", \"description\": \"一句话描述\" {'}'},\n {'{'} \"text\": \"子想法2名称\", \"description\": \"一句话描述\" {'}'}\n ]\n{'}'}\n\n注意:只返回 JSON,不附加解释。",
|
||||||
"image": "生成一张插画风格的图片,表现主题:{prompt}。要求:构图简洁,色彩明快,适合作为思维导图的视觉辅助,图片中严禁出现任何中文字符。",
|
"image": "这是一张精美的艺术插画,主题是:{prompt}。风格要求:具有精致的绘本感,构图优美且富有诗意,像是在讲述一个故事。细节要求:高质量的笔触,极简主义与装饰艺术结合,适合作为思维导图的视觉核心。严禁:图片中不得出现任何文字、字母或字符。",
|
||||||
"continue": "请继续深入发散",
|
"continue": "请继续深入发散",
|
||||||
"coreIdeaPrefix": "核心想法",
|
"coreIdeaPrefix": "核心想法",
|
||||||
"contextPath": "思考上下文路径",
|
"contextPath": "思考上下文路径",
|
||||||
"selectedNode": "当前选择节点",
|
"selectedNode": "当前选择节点",
|
||||||
"newRequirement": "用户追问/新要求"
|
"newRequirement": "用户追问/新要求",
|
||||||
|
"deepDivePrompt": "请针对【{topic}】提供一个深度且详细的解析。要求:\n1. 结构清晰,包含背景、核心原理、关键要素和实际应用。\n2. 语言专业且易懂。\n3. 总字数控制在 300-500 字左右。\n4. 直接输出正文内容,不要包含 JSON 格式,也不要包含任何开场白或结束语。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user