Update App.vue
This commit is contained in:
284
src/App.vue
284
src/App.vue
@@ -20,7 +20,11 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
Palette,
|
Palette,
|
||||||
Grid3X3,
|
Grid3X3,
|
||||||
Trash2
|
Trash2,
|
||||||
|
X,
|
||||||
|
Maximize2,
|
||||||
|
Terminal,
|
||||||
|
ChevronRight
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { VueFlow, useVueFlow, Position, MarkerType, Handle } from '@vue-flow/core'
|
import { VueFlow, useVueFlow, Position, MarkerType, Handle } from '@vue-flow/core'
|
||||||
import { Background, BackgroundVariant } from '@vue-flow/background'
|
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
|
const API_KEY = import.meta.env.VITE_ZHIPU_AI_API_KEY
|
||||||
|
|
||||||
// VueFlow 实例
|
// 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 ideaInput = ref('')
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const hoveredNodeId = ref<string | null>(null)
|
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 previewImageUrl = ref<string | null>(null)
|
||||||
|
|
||||||
// 计算当前是否有节点处于“活跃”状态(被聚焦、悬停或正在生成)
|
// 拖拽监听
|
||||||
|
onNodeDragStart(e => {
|
||||||
|
draggingNodeId.value = e.node.id
|
||||||
|
})
|
||||||
|
|
||||||
|
onNodeDragStop(() => {
|
||||||
|
draggingNodeId.value = null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算当前是否有节点处于“活跃”状态(被选中、聚焦、拖拽或正在生成)
|
||||||
const activeNodeId = computed(() => {
|
const activeNodeId = computed(() => {
|
||||||
const expandingNode = flowNodes.value.find(n => n.data.isExpanding)
|
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<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)
|
||||||
|
|
||||||
|
// 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
|
showControls: true
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听配置变化更新现有连线
|
const lastAppliedStatus = ref('')
|
||||||
|
|
||||||
|
// 监听 activePath 和配置变化,动态更新连线状态
|
||||||
watch(
|
watch(
|
||||||
() => config.edgeColor,
|
[() => activeNodeId.value, () => config.edgeColor, () => flowEdges.value.length, () => flowNodes.value.some(n => n.data.isExpanding)],
|
||||||
newColor => {
|
([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(
|
setEdges(
|
||||||
flowEdges.value.map(edge => ({
|
flowEdges.value.map(edge => {
|
||||||
...edge,
|
const isHighlighted = edgeIds.has(edge.id)
|
||||||
style: { ...edge.style, stroke: newColor }
|
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生成思维发散节点
|
* 调用智谱AI生成思维发散节点
|
||||||
*/
|
*/
|
||||||
@@ -135,8 +244,10 @@ const expandIdea = async (param?: any, customInput?: string) => {
|
|||||||
|
|
||||||
if (!text || (parentNode ? parentNode.data.isExpanding : isLoading.value)) return
|
if (!text || (parentNode ? parentNode.data.isExpanding : isLoading.value)) return
|
||||||
|
|
||||||
|
// 记录父节点加载状态
|
||||||
if (parentNode) {
|
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 {
|
} else {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
}
|
}
|
||||||
@@ -151,9 +262,9 @@ const expandIdea = async (param?: any, customInput?: string) => {
|
|||||||
|
|
||||||
工作流程:
|
工作流程:
|
||||||
1. 用户给出一个初始想法(或选择一个已有节点继续追问)。
|
1. 用户给出一个初始想法(或选择一个已有节点继续追问)。
|
||||||
2. 你根据当前想法和已有对话历史,生成 3-5 个更深层或相关维度的子想法。
|
2. 你需要根据【思考上下文路径】(即从根节点到当前节点的思考链路)来理解用户的意图。
|
||||||
3. 每个子想法包含简短名称和极简描述。
|
3. 生成 3-5 个更深层或相关维度的子想法。
|
||||||
4. 如果用户的问题明显是针对某个已有节点的追问,请结合上下文做针对性发散。
|
4. 每个子想法包含简短名称和极简描述。
|
||||||
|
|
||||||
返回格式必须为严格 JSON:
|
返回格式必须为严格 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,不附加解释。`
|
注意:只返回 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 {
|
try {
|
||||||
const response = await fetch('https://open.bigmodel.cn/api/paas/v4/chat/completions', {
|
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,
|
id: rootId,
|
||||||
type: 'window',
|
type: 'window',
|
||||||
position: { x: startX, y: startY },
|
position: { x: startX, y: startY },
|
||||||
data: { label: text, description: '核心想法', type: 'root' },
|
data: {
|
||||||
|
label: text,
|
||||||
|
description: '核心想法',
|
||||||
|
type: 'root',
|
||||||
|
isExpanding: false,
|
||||||
|
followUp: ''
|
||||||
|
},
|
||||||
sourcePosition: Position.Right,
|
sourcePosition: Position.Right,
|
||||||
targetPosition: Position.Left
|
targetPosition: Position.Left
|
||||||
})
|
})
|
||||||
@@ -234,7 +337,11 @@ const expandIdea = async (param?: any, customInput?: string) => {
|
|||||||
console.error('Expansion Error:', error)
|
console.error('Expansion Error:', error)
|
||||||
} finally {
|
} finally {
|
||||||
if (parentNode) {
|
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 {
|
} else {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -345,16 +452,17 @@ const startNewSession = () => {
|
|||||||
<Controls v-if="config.showControls" />
|
<Controls v-if="config.showControls" />
|
||||||
|
|
||||||
<!-- 自定义节点插槽 -->
|
<!-- 自定义节点插槽 -->
|
||||||
<template #node-window="{ id, data }">
|
<template #node-window="{ id, data, selected }">
|
||||||
<div
|
<div
|
||||||
class="window-node group transition-all duration-500"
|
class="window-node group transition-all duration-500"
|
||||||
:class="{
|
:class="{
|
||||||
'opacity-20 grayscale-[0.5] blur-[0.5px] scale-[0.98]': activeNodeId && activeNodeId !== id,
|
'opacity-10 grayscale-[0.8] blur-[1px] scale-[0.95] pointer-events-none': activeNodeId && !activePath.nodeIds.has(id),
|
||||||
'opacity-100 grayscale-0 blur-0 scale-105 z-50': activeNodeId === id
|
'opacity-100 grayscale-0 blur-0 scale-105 z-50 ring-2 ring-offset-4': activePath.nodeIds.has(id)
|
||||||
}"
|
}"
|
||||||
:style="{
|
:style="{
|
||||||
borderColor: activeNodeId === id ? config.edgeColor : config.edgeColor + '40',
|
borderColor: activePath.nodeIds.has(id) ? config.edgeColor : config.edgeColor + '40',
|
||||||
boxShadow: activeNodeId === id ? `0 20px 50px -12px ${config.edgeColor}40` : ''
|
boxShadow: activeNodeId === id ? `0 20px 50px -12px ${config.edgeColor}40` : '',
|
||||||
|
'--tw-ring-color': selected ? config.edgeColor + '40' : 'transparent'
|
||||||
}"
|
}"
|
||||||
@mouseenter="hoveredNodeId = id"
|
@mouseenter="hoveredNodeId = id"
|
||||||
@mouseleave="hoveredNodeId = null"
|
@mouseleave="hoveredNodeId = null"
|
||||||
@@ -364,34 +472,54 @@ const startNewSession = () => {
|
|||||||
<Handle type="source" :position="Position.Right" class="!bg-transparent !border-none" />
|
<Handle type="source" :position="Position.Right" class="!bg-transparent !border-none" />
|
||||||
|
|
||||||
<!-- Window Header -->
|
<!-- Window Header -->
|
||||||
<div class="window-header" :style="{ backgroundColor: activeNodeId === id ? config.edgeColor + '15' : config.edgeColor + '05' }">
|
<div class="window-header" :style="{ backgroundColor: activePath.nodeIds.has(id) ? config.edgeColor + '15' : config.edgeColor + '05' }">
|
||||||
<div class="flex gap-1.5">
|
<div class="flex gap-1.5">
|
||||||
<div class="w-2 h-2 rounded-full" :style="{ backgroundColor: config.edgeColor }"></div>
|
<div class="w-2 h-2 rounded-full" :style="{ backgroundColor: activePath.nodeIds.has(id) ? config.edgeColor : config.edgeColor + '40' }"></div>
|
||||||
<div class="w-2 h-2 rounded-full bg-slate-200"></div>
|
<div class="w-2 h-2 rounded-full bg-slate-200"></div>
|
||||||
<div class="w-2 h-2 rounded-full bg-slate-200"></div>
|
<div class="w-2 h-2 rounded-full bg-slate-200"></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="window-title" :style="{ color: config.edgeColor }">
|
<span class="window-title" :style="{ color: activePath.nodeIds.has(id) ? config.edgeColor : '' }">
|
||||||
{{ data.type === 'root' ? 'main.ts' : 'module.tsx' }}
|
{{ data.type === 'root' ? 'main.ts' : 'module.tsx' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading Overlay for Node -->
|
||||||
|
<div
|
||||||
|
v-if="data.isExpanding"
|
||||||
|
class="absolute inset-0 z-20 flex flex-col items-center justify-center bg-white/60 backdrop-blur-[2px] rounded-2xl transition-all duration-300"
|
||||||
|
>
|
||||||
|
<div class="relative">
|
||||||
|
<RefreshCw class="w-8 h-8 text-slate-900 animate-spin mb-3" :style="{ color: config.edgeColor }" />
|
||||||
|
<div class="absolute inset-0 blur-xl opacity-20 animate-pulse" :style="{ backgroundColor: config.edgeColor }"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] font-black tracking-widest uppercase text-slate-500">Expanding Idea...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Window Content -->
|
<!-- Window Content -->
|
||||||
<div class="window-content">
|
<div class="window-content">
|
||||||
<!-- Image Display -->
|
<!-- Image Display -->
|
||||||
<div
|
<div
|
||||||
v-if="data.imageUrl || data.isImageLoading"
|
v-if="data.imageUrl || data.isImageLoading"
|
||||||
class="mb-4 rounded-lg overflow-hidden bg-slate-50 border border-slate-100 aspect-video flex items-center justify-center relative group/img"
|
class="mb-4 rounded-lg overflow-hidden bg-slate-50 border border-slate-100 aspect-video flex items-center justify-center relative group/img cursor-pointer"
|
||||||
|
@click.stop="data.imageUrl ? (previewImageUrl = data.imageUrl) : null"
|
||||||
>
|
>
|
||||||
<img v-if="data.imageUrl" :src="data.imageUrl" class="w-full h-full object-cover" />
|
<img v-if="data.imageUrl" :src="data.imageUrl" class="w-full h-full object-cover" />
|
||||||
<div v-if="data.isImageLoading" class="absolute inset-0 flex flex-col items-center justify-center bg-white/80 backdrop-blur-sm">
|
<div v-if="data.isImageLoading" class="absolute inset-0 flex flex-col items-center justify-center bg-white/80 backdrop-blur-sm cursor-default">
|
||||||
<RefreshCw class="w-6 h-6 text-orange-500 animate-spin mb-2" />
|
<RefreshCw class="w-6 h-6 text-orange-500 animate-spin mb-2" />
|
||||||
<span class="text-[8px] font-bold text-slate-400 uppercase">Generating...</span>
|
<span class="text-[8px] font-bold text-slate-400 uppercase">Generating...</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="data.imageUrl"
|
v-if="data.imageUrl"
|
||||||
class="absolute inset-0 bg-black/40 opacity-0 group-hover/img:opacity-100 transition-opacity flex items-center justify-center"
|
class="absolute inset-0 bg-black/40 opacity-0 group-hover/img:opacity-100 transition-opacity flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
<button @click="generateNodeImage(id, data.label)" class="p-2 bg-white/20 hover:bg-white/40 rounded-full backdrop-blur-md transition-all">
|
<button class="p-2 bg-white/20 hover:bg-white/40 rounded-full backdrop-blur-md transition-all" title="View">
|
||||||
|
<Maximize2 class="w-4 h-4 text-white" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click.stop="generateNodeImage(id, data.label)"
|
||||||
|
class="p-2 bg-white/20 hover:bg-white/40 rounded-full backdrop-blur-md transition-all"
|
||||||
|
title="Regenerate"
|
||||||
|
>
|
||||||
<RefreshCw class="w-4 h-4 text-white" />
|
<RefreshCw class="w-4 h-4 text-white" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -431,7 +559,8 @@ const startNewSession = () => {
|
|||||||
class="flex items-center gap-2 bg-slate-50 rounded-lg px-2.5 py-2 border border-slate-100 focus-within:bg-white transition-all"
|
class="flex items-center gap-2 bg-slate-50 rounded-lg px-2.5 py-2 border border-slate-100 focus-within:bg-white transition-all"
|
||||||
:style="{ borderColor: data.followUp || focusedNodeId === id ? config.edgeColor : '' }"
|
:style="{ borderColor: data.followUp || focusedNodeId === id ? config.edgeColor : '' }"
|
||||||
>
|
>
|
||||||
<span class="font-black text-[10px] select-none" :style="{ color: config.edgeColor }">$</span>
|
<ChevronRight v-if="!data.followUp" class="w-3 h-3 text-slate-400" />
|
||||||
|
<Terminal v-else class="w-3 h-3" :style="{ color: config.edgeColor }" />
|
||||||
<input
|
<input
|
||||||
v-model="data.followUp"
|
v-model="data.followUp"
|
||||||
@focus="focusedNodeId = id"
|
@focus="focusedNodeId = id"
|
||||||
@@ -461,6 +590,16 @@ const startNewSession = () => {
|
|||||||
<!-- 浮动 UI 层 -->
|
<!-- 浮动 UI 层 -->
|
||||||
<div class="absolute inset-0 pointer-events-none z-10 p-12"></div>
|
<div class="absolute inset-0 pointer-events-none z-10 p-12"></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">
|
||||||
|
<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>
|
||||||
|
<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">
|
||||||
|
<X class="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<img :src="previewImageUrl" class="max-w-screen max-h-screen object-contain" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 加载状态遮罩 -->
|
<!-- 加载状态遮罩 -->
|
||||||
<div v-if="isLoading" class="absolute inset-0 z-[100] flex items-center justify-center bg-white/5 backdrop-blur-[1px] pointer-events-none">
|
<div v-if="isLoading" class="absolute inset-0 z-[100] flex items-center justify-center bg-white/5 backdrop-blur-[1px] pointer-events-none">
|
||||||
<div class="bg-white/90 p-4 rounded-full shadow-2xl border border-orange-100 animate-bounce">
|
<div class="bg-white/90 p-4 rounded-full shadow-2xl border border-orange-100 animate-bounce">
|
||||||
@@ -475,7 +614,7 @@ const startNewSession = () => {
|
|||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 px-4 py-1.5 bg-white/90 backdrop-blur-md border border-slate-200 rounded-lg shadow-sm self-start ml-2 mb-[-8px] z-10 scale-90 origin-bottom-left"
|
class="flex items-center gap-2 px-4 py-1.5 bg-white/90 backdrop-blur-md border border-slate-200 rounded-lg shadow-sm self-start ml-2 mb-[-8px] z-10 scale-90 origin-bottom-left"
|
||||||
>
|
>
|
||||||
<span class="text-emerald-500 font-bold">$</span>
|
<Sparkles class="w-3 h-3 text-emerald-500" />
|
||||||
<span class="text-slate-400 text-xs font-bold">pwd:</span>
|
<span class="text-slate-400 text-xs font-bold">pwd:</span>
|
||||||
<span class="text-slate-300 text-xs">~ /</span>
|
<span class="text-slate-300 text-xs">~ /</span>
|
||||||
<span class="text-slate-600 text-xs font-bold">think-flow</span>
|
<span class="text-slate-600 text-xs font-bold">think-flow</span>
|
||||||
@@ -484,29 +623,24 @@ const startNewSession = () => {
|
|||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<!-- 核心输入框容器 -->
|
<!-- 核心输入框容器 -->
|
||||||
<div
|
<div
|
||||||
class="bg-white rounded-2xl p-1.5 flex items-center gap-3 shadow-2xl shadow-slate-200 border border-slate-200 w-[640px] group transition-all focus-within:ring-4 focus-within:ring-orange-500/5 focus-within:border-orange-200"
|
class="flex-grow flex items-center gap-4 bg-slate-50 border border-slate-200 rounded-2xl px-5 py-3 focus-within:bg-white focus-within:shadow-xl focus-within:shadow-slate-100 transition-all"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-3 pl-4 flex-grow">
|
<Terminal class="w-5 h-5 text-slate-400" />
|
||||||
<span class="text-orange-500 font-black text-lg select-none">$</span>
|
<input
|
||||||
<input
|
v-model="ideaInput"
|
||||||
v-model="ideaInput"
|
placeholder="Enter your thought here..."
|
||||||
@keyup.enter="expandIdea"
|
class="flex-grow bg-transparent border-none outline-none text-sm font-bold text-slate-700 placeholder:text-slate-300"
|
||||||
placeholder="Type your idea and press Enter..."
|
@keyup.enter="expandIdea"
|
||||||
class="bg-transparent border-none outline-none text-slate-800 placeholder:text-slate-300 flex-grow font-bold tracking-tight text-sm"
|
/>
|
||||||
/>
|
<button
|
||||||
</div>
|
@click="expandIdea"
|
||||||
|
:disabled="isLoading || !ideaInput.trim()"
|
||||||
<div class="flex items-center gap-1 pr-1">
|
class="flex items-center gap-2 px-4 py-2.5 bg-slate-900 hover:bg-slate-800 text-white rounded-xl transition-all active:scale-95 disabled:opacity-20 disabled:grayscale disabled:cursor-not-allowed group/btn"
|
||||||
<button
|
>
|
||||||
@click="expandIdea"
|
<span class="text-[10px] font-black tracking-widest uppercase mr-1">Execute</span>
|
||||||
:disabled="isLoading || !ideaInput.trim()"
|
<Zap v-if="!isLoading" class="w-4 h-4 text-orange-400 group-hover/btn:scale-110 transition-transform" />
|
||||||
class="flex items-center gap-2 px-4 py-2.5 bg-slate-900 hover:bg-slate-800 text-white rounded-xl transition-all active:scale-95 disabled:opacity-20 disabled:grayscale disabled:cursor-not-allowed group/btn"
|
<RefreshCw v-else class="w-4 h-4 animate-spin" />
|
||||||
>
|
</button>
|
||||||
<span class="text-[10px] font-black tracking-widest uppercase mr-1">Execute</span>
|
|
||||||
<Zap v-if="!isLoading" class="w-4 h-4 text-orange-400 group-hover/btn:scale-110 transition-transform" />
|
|
||||||
<RefreshCw v-else class="w-4 h-4 animate-spin" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部按钮组 (仅保留核心功能) -->
|
<!-- 底部按钮组 (仅保留核心功能) -->
|
||||||
|
|||||||
Reference in New Issue
Block a user