样式优化
This commit is contained in:
7
package-lock.json
generated
7
package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"@vue-flow/core": "^1.48.1",
|
||||
"@vue-flow/node-resizer": "^1.5.0",
|
||||
"axios": "^1.6.7",
|
||||
"html-to-image": "^1.11.13",
|
||||
"lucide-vue-next": "^0.322.0",
|
||||
"vue": "^3.4.15"
|
||||
},
|
||||
@@ -2029,6 +2030,12 @@
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-image": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
|
||||
"integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@vue-flow/core": "^1.48.1",
|
||||
"@vue-flow/node-resizer": "^1.5.0",
|
||||
"axios": "^1.6.7",
|
||||
"html-to-image": "^1.11.13",
|
||||
"lucide-vue-next": "^0.322.0",
|
||||
"vue": "^3.4.15"
|
||||
},
|
||||
|
||||
389
src/App.vue
389
src/App.vue
@@ -1,9 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, h } from 'vue'
|
||||
import { Sparkles, Search, Folder, Monitor, BookOpen, Code, Sun, Globe, LogIn, Plus, MousePointer2, Zap, ArrowRight, RefreshCw, Image as ImageIcon } from 'lucide-vue-next'
|
||||
import { ref, reactive, h, watch, computed } from 'vue'
|
||||
import {
|
||||
Sparkles,
|
||||
Search,
|
||||
Folder,
|
||||
Monitor,
|
||||
BookOpen,
|
||||
Code,
|
||||
Sun,
|
||||
Globe,
|
||||
LogIn,
|
||||
Plus,
|
||||
MousePointer2,
|
||||
Zap,
|
||||
ArrowRight,
|
||||
RefreshCw,
|
||||
Image as ImageIcon,
|
||||
Download,
|
||||
Settings,
|
||||
Palette,
|
||||
Grid3X3,
|
||||
Trash2
|
||||
} from 'lucide-vue-next'
|
||||
import { VueFlow, useVueFlow, Position, MarkerType } from '@vue-flow/core'
|
||||
import { Background, BackgroundVariant } from '@vue-flow/background'
|
||||
import { Controls } from '@vue-flow/controls'
|
||||
import { toPng } from 'html-to-image'
|
||||
|
||||
// 导入 VueFlow 样式
|
||||
import '@vue-flow/core/dist/style.css'
|
||||
@@ -13,11 +35,62 @@ 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 } = useVueFlow()
|
||||
const { addNodes, addEdges, onConnect, setNodes, setEdges, nodes: flowNodes, edges: flowEdges, updateNode, fitView } = useVueFlow()
|
||||
|
||||
// 状态管理
|
||||
const ideaInput = ref('')
|
||||
const isLoading = ref(false)
|
||||
const hoveredNodeId = ref<string | null>(null)
|
||||
const focusedNodeId = ref<string | null>(null)
|
||||
|
||||
// 计算当前是否有节点处于“活跃”状态(被聚焦、悬停或正在生成)
|
||||
const activeNodeId = computed(() => {
|
||||
const expandingNode = flowNodes.value.find(n => n.data.isExpanding)
|
||||
return expandingNode?.id || focusedNodeId.value || hoveredNodeId.value
|
||||
})
|
||||
|
||||
// 画布配置
|
||||
const config = reactive({
|
||||
edgeColor: '#fed7aa',
|
||||
edgeStyle: 'smoothstep',
|
||||
backgroundVariant: BackgroundVariant.Lines,
|
||||
showControls: true
|
||||
})
|
||||
|
||||
// 监听配置变化更新现有连线
|
||||
watch(
|
||||
() => config.edgeColor,
|
||||
newColor => {
|
||||
setEdges(
|
||||
flowEdges.value.map(edge => ({
|
||||
...edge,
|
||||
style: { ...edge.style, stroke: newColor }
|
||||
}))
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 导出为图片
|
||||
*/
|
||||
const exportImage = async () => {
|
||||
const el = document.querySelector('.vue-flow') as HTMLElement
|
||||
if (!el) return
|
||||
|
||||
try {
|
||||
const dataUrl = await toPng(el, {
|
||||
backgroundColor: '#ffffff',
|
||||
quality: 1,
|
||||
pixelRatio: 2
|
||||
})
|
||||
const link = document.createElement('a')
|
||||
link.download = `thinkflow-${Date.now()}.png`
|
||||
link.href = dataUrl
|
||||
link.click()
|
||||
} catch (err) {
|
||||
console.error('Export failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用智谱AI生成图片
|
||||
@@ -55,13 +128,18 @@ const generateNodeImage = async (nodeId: string, prompt: string) => {
|
||||
/**
|
||||
* 调用智谱AI生成思维发散节点
|
||||
*/
|
||||
const expandIdea = async (param?: any) => {
|
||||
const expandIdea = async (param?: any, customInput?: string) => {
|
||||
// 判断是点击了节点上的按钮还是主输入框
|
||||
const parentNode = param && param.id ? param : undefined
|
||||
const text = parentNode ? parentNode.data.label : ideaInput.value
|
||||
const text = customInput || (parentNode ? parentNode.data.label : ideaInput.value)
|
||||
|
||||
if (!text || isLoading.value) return
|
||||
isLoading.value = true
|
||||
if (!text || (parentNode ? parentNode.data.isExpanding : isLoading.value)) return
|
||||
|
||||
if (parentNode) {
|
||||
updateNode(parentNode.id, { data: { ...parentNode.data, isExpanding: true } })
|
||||
} else {
|
||||
isLoading.value = true
|
||||
}
|
||||
|
||||
// 如果是第一次生成,清空画布
|
||||
if (!parentNode) {
|
||||
@@ -69,9 +147,45 @@ const expandIdea = async (param?: any) => {
|
||||
setEdges([])
|
||||
}
|
||||
|
||||
const systemPrompt = `你是一个思维发散专家。给定一个想法,请将其发散出 3-5 个更深层或相关维度的子想法。
|
||||
每个子想法包含:简短的名称 (text) 和 极简的描述 (description)。
|
||||
返回格式必须为 JSON: { "nodes": [ { "text": "...", "description": "..." } ] }`
|
||||
const systemPrompt = `你是一个思维发散助手,帮助用户将想法逐层展开,构建思维树。
|
||||
|
||||
工作流程:
|
||||
1. 用户给出一个初始想法(或选择一个已有节点继续追问)。
|
||||
2. 你根据当前想法和已有对话历史,生成 3-5 个更深层或相关维度的子想法。
|
||||
3. 每个子想法包含简短名称和极简描述。
|
||||
4. 如果用户的问题明显是针对某个已有节点的追问,请结合上下文做针对性发散。
|
||||
|
||||
返回格式必须为严格 JSON:
|
||||
{
|
||||
"nodes": [
|
||||
{ "text": "子想法1名称", "description": "一句话描述" },
|
||||
{ "text": "子想法2名称", "description": "一句话描述" }
|
||||
]
|
||||
}
|
||||
|
||||
示例:
|
||||
用户:"年夜饭推荐"
|
||||
你返回:{
|
||||
"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
|
||||
|
||||
try {
|
||||
const response = await fetch('https://open.bigmodel.cn/api/paas/v4/chat/completions', {
|
||||
@@ -84,7 +198,7 @@ const expandIdea = async (param?: any) => {
|
||||
model: 'glm-4-flash',
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: text }
|
||||
{ role: 'user', content: userMessage }
|
||||
],
|
||||
response_format: { type: 'json_object' },
|
||||
temperature: 0.8
|
||||
@@ -106,7 +220,9 @@ const expandIdea = async (param?: any) => {
|
||||
id: rootId,
|
||||
type: 'window',
|
||||
position: { x: startX, y: startY },
|
||||
data: { label: text, description: '核心想法', type: 'root' }
|
||||
data: { label: text, description: '核心想法', type: 'root' },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left
|
||||
})
|
||||
|
||||
// 为后续子节点计算位置
|
||||
@@ -117,21 +233,27 @@ const expandIdea = async (param?: any) => {
|
||||
} catch (error) {
|
||||
console.error('Expansion Error:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
if (parentNode) {
|
||||
updateNode(parentNode.id, { data: { ...parentNode.data, isExpanding: false, followUp: '' } })
|
||||
} else {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processSubNodes = (subNodes: any[], parentId: string, baseX: number, baseY: number) => {
|
||||
subNodes.forEach((item: any, index: number) => {
|
||||
const childId = `node-${Date.now()}-${index}`
|
||||
const offsetX = 400
|
||||
const offsetY = (index - (subNodes.length - 1) / 2) * 250
|
||||
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' }
|
||||
data: { label: item.text, description: item.description, type: 'child' },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left
|
||||
})
|
||||
|
||||
addEdges({
|
||||
@@ -139,7 +261,7 @@ const processSubNodes = (subNodes: any[], parentId: string, baseX: number, baseY
|
||||
source: parentId,
|
||||
target: childId,
|
||||
animated: true,
|
||||
style: { stroke: '#fed7aa', strokeWidth: 2 },
|
||||
style: { stroke: config.edgeColor, strokeWidth: 2 },
|
||||
markerEnd: MarkerType.ArrowClosed
|
||||
})
|
||||
})
|
||||
@@ -154,42 +276,57 @@ const startNewSession = () => {
|
||||
|
||||
<template>
|
||||
<div class="h-screen w-screen bg-white font-mono text-slate-800 relative overflow-hidden flex flex-col selection:bg-orange-100">
|
||||
<!-- 顶部导航栏 -->
|
||||
<nav class="flex-none bg-white/80 backdrop-blur-md border-b border-slate-200 px-6 py-2 flex items-center justify-between shadow-sm z-50">
|
||||
<!-- 顶部导航栏 (工具栏) -->
|
||||
<nav class="flex-none bg-white/80 backdrop-blur-md border-b border-slate-200 px-6 py-3 flex items-center justify-between shadow-sm z-50">
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="flex items-center gap-2 mr-4">
|
||||
<div class="w-3 h-3 bg-orange-500 rounded-sm rotate-45"></div>
|
||||
<span class="font-black text-slate-900 tracking-tighter text-lg">ThinkFlow</span>
|
||||
</div>
|
||||
|
||||
<div class="h-6 w-[1px] bg-slate-200 mx-2"></div>
|
||||
|
||||
<!-- 工具按钮组 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2.5 h-2.5 bg-emerald-400 rounded-full animate-pulse"></div>
|
||||
<span class="text-slate-400">ready</span>
|
||||
<span class="text-slate-300 mx-1">~/</span>
|
||||
<span class="font-bold text-slate-900 tracking-tight">ThinkFlow AI</span>
|
||||
<span class="w-1.5 h-5 bg-orange-200 ml-1"></span>
|
||||
<!-- 重置画布 -->
|
||||
<button @click="startNewSession" class="toolbar-btn text-red-500 hover:bg-red-50 border-red-100" title="Reset Canvas">
|
||||
<Trash2 class="w-4 h-4" />
|
||||
<span>RESET</span>
|
||||
</button>
|
||||
|
||||
<div class="h-4 w-[1px] bg-slate-100 mx-1"></div>
|
||||
|
||||
<!-- 连线颜色 -->
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 bg-slate-50 rounded-lg border border-slate-100">
|
||||
<Palette class="w-3.5 h-3.5 text-slate-400" />
|
||||
<input type="color" v-model="config.edgeColor" class="w-4 h-4 rounded cursor-pointer bg-transparent border-none" />
|
||||
<span class="text-[10px] font-bold text-slate-500 uppercase">Edge</span>
|
||||
</div>
|
||||
|
||||
<!-- 背景样式 -->
|
||||
<select v-model="config.backgroundVariant" class="toolbar-select">
|
||||
<option :value="BackgroundVariant.Lines">LINES</option>
|
||||
<option :value="BackgroundVariant.Dots">DOTS</option>
|
||||
</select>
|
||||
|
||||
<div class="h-4 w-[1px] bg-slate-100 mx-1"></div>
|
||||
|
||||
<!-- 导出图片 -->
|
||||
<button @click="exportImage" class="toolbar-btn text-emerald-600 hover:bg-emerald-50 border-emerald-100">
|
||||
<Download class="w-4 h-4" />
|
||||
<span>EXPORT</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:flex items-center gap-2">
|
||||
<button class="nav-btn group">
|
||||
<span class="text-orange-500 font-bold">$</span>
|
||||
<span class="text-slate-600">ai</span>
|
||||
<span class="text-slate-300 ml-1">--search</span>
|
||||
</button>
|
||||
<button class="nav-btn">
|
||||
<span class="text-emerald-500 font-bold">$</span>
|
||||
<span class="text-slate-600">cd</span>
|
||||
<span class="text-slate-300 ml-1">/categories</span>
|
||||
</button>
|
||||
<button class="nav-btn">
|
||||
<span class="text-blue-500 font-bold">$</span>
|
||||
<span class="text-slate-600">watch</span>
|
||||
<span class="font-bold ml-1">stats</span>
|
||||
</button>
|
||||
<div class="h-4 w-[1px] bg-slate-200 mx-2"></div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="p-2 hover:bg-slate-100 rounded-md transition-colors text-slate-400 font-bold text-xs flex items-center gap-1">
|
||||
<Globe class="w-3.5 h-3.5" /> ZH
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-1.5 bg-emerald-50 text-emerald-600 rounded-md text-xs font-bold border border-emerald-100 hover:bg-emerald-100 transition-all"
|
||||
class="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-xl text-xs font-black tracking-widest hover:bg-slate-800 transition-all shadow-lg shadow-slate-200"
|
||||
>
|
||||
<span class="text-emerald-500">$</span> 登录 <LogIn class="w-3.5 h-3.5 ml-1" />
|
||||
SIGN IN <LogIn class="w-3.5 h-3.5 ml-1" />
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -197,20 +334,32 @@ const startNewSession = () => {
|
||||
<!-- 主内容区:VueFlow 画布 -->
|
||||
<div class="flex-grow relative">
|
||||
<VueFlow :default-edge-options="{ type: 'smoothstep' }" :fit-view-on-init="true" class="bg-white">
|
||||
<Background :variant="BackgroundVariant.Lines" pattern-color="#f2f2f2" :gap="24" :size="0.5" />
|
||||
<Controls />
|
||||
<Background :variant="config.backgroundVariant" pattern-color="#f2f2f2" :gap="24" :size="0.5" />
|
||||
<Controls v-if="config.showControls" />
|
||||
|
||||
<!-- 自定义节点插槽 -->
|
||||
<template #node-window="{ id, data }">
|
||||
<div class="window-node group">
|
||||
<div
|
||||
class="window-node group transition-all duration-500"
|
||||
:class="{
|
||||
'opacity-20 grayscale-[0.5] blur-[0.5px] scale-[0.98]': activeNodeId && activeNodeId !== id,
|
||||
'opacity-100 grayscale-0 blur-0 scale-105 z-50': activeNodeId === id
|
||||
}"
|
||||
:style="{
|
||||
borderColor: data.isExpanding || activeNodeId === id ? config.edgeColor : '',
|
||||
boxShadow: activeNodeId === id ? `0 20px 50px -12px ${config.edgeColor}40` : ''
|
||||
}"
|
||||
@mouseenter="hoveredNodeId = id"
|
||||
@mouseleave="hoveredNodeId = null"
|
||||
>
|
||||
<!-- Window Header -->
|
||||
<div class="window-header">
|
||||
<div class="window-header" :style="{ backgroundColor: data.isExpanding || activeNodeId === id ? config.edgeColor + '10' : '' }">
|
||||
<div class="flex gap-1.5">
|
||||
<div class="w-2 h-2 rounded-full bg-red-400/80"></div>
|
||||
<div class="w-2 h-2 rounded-full" :style="{ backgroundColor: data.isExpanding || activeNodeId === id ? config.edgeColor : '#f87171' }"></div>
|
||||
<div class="w-2 h-2 rounded-full bg-amber-400/80"></div>
|
||||
<div class="w-2 h-2 rounded-full bg-emerald-400/80"></div>
|
||||
</div>
|
||||
<span class="window-title">
|
||||
<span class="window-title" :style="{ color: data.isExpanding || activeNodeId === id ? config.edgeColor : '' }">
|
||||
{{ data.type === 'root' ? 'main.ts' : 'module.tsx' }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -238,7 +387,7 @@ const startNewSession = () => {
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-orange-500 font-bold">></span>
|
||||
<span class="font-bold" :style="{ color: config.edgeColor }">></span>
|
||||
<h3 class="font-black text-slate-900 tracking-tight truncate">{{ data.label }}</h3>
|
||||
</div>
|
||||
<p class="text-[10px] text-slate-500 leading-relaxed font-medium line-clamp-3">
|
||||
@@ -246,29 +395,51 @@ const startNewSession = () => {
|
||||
</p>
|
||||
|
||||
<!-- Node Actions -->
|
||||
<div class="pt-3 mt-3 border-t border-slate-50 flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-1.5 shrink-0">
|
||||
<div class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></div>
|
||||
<span class="text-[9px] font-bold text-slate-400 uppercase tracking-tighter">active</span>
|
||||
<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 gap-1.5 shrink-0">
|
||||
<div class="w-1.5 h-1.5 rounded-full animate-pulse" :style="{ backgroundColor: data.isExpanding ? config.edgeColor : '#34d399' }"></div>
|
||||
<span class="text-[9px] font-bold text-slate-400 uppercase tracking-tighter">{{ data.isExpanding ? 'Expanding' : 'active' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
v-if="!data.imageUrl && !data.isImageLoading"
|
||||
@click.stop="generateNodeImage(id, data.label)"
|
||||
class="action-btn text-blue-500 hover:bg-blue-50"
|
||||
>
|
||||
<ImageIcon class="w-2.5 h-2.5" />
|
||||
<span>IMG</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 overflow-hidden">
|
||||
<button
|
||||
v-if="!data.imageUrl && !data.isImageLoading"
|
||||
@click.stop="generateNodeImage(id, data.label)"
|
||||
class="action-btn text-blue-500 hover:bg-blue-50"
|
||||
<!-- Follow-up Input -->
|
||||
<div class="relative group/input">
|
||||
<div
|
||||
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 : '' }"
|
||||
>
|
||||
<ImageIcon class="w-2.5 h-2.5" />
|
||||
<span>IMG</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click.stop="expandIdea({ id, data, position: flowNodes.find(n => n.id === id)?.position })"
|
||||
class="action-btn text-orange-500 hover:bg-orange-50"
|
||||
>
|
||||
<ArrowRight class="w-2.5 h-2.5" />
|
||||
<span>EXPAND</span>
|
||||
</button>
|
||||
<span class="font-black text-[10px] select-none" :style="{ color: config.edgeColor }">$</span>
|
||||
<input
|
||||
v-model="data.followUp"
|
||||
@focus="focusedNodeId = id"
|
||||
@blur="focusedNodeId = null"
|
||||
@keyup.enter="expandIdea({ id, data, position: flowNodes.find(n => n.id === id)?.position }, data.followUp)"
|
||||
placeholder="Ask a follow-up..."
|
||||
class="bg-transparent border-none outline-none text-[10px] font-bold text-slate-700 flex-grow placeholder:text-slate-300"
|
||||
:disabled="data.isExpanding"
|
||||
/>
|
||||
<button
|
||||
@click.stop="expandIdea({ id, data, position: flowNodes.find(n => n.id === id)?.position }, data.followUp)"
|
||||
:disabled="!data.followUp?.trim() || data.isExpanding"
|
||||
class="transition-all transform active:scale-90"
|
||||
:style="{ color: data.followUp?.trim() ? config.edgeColor : '#cbd5e1' }"
|
||||
>
|
||||
<RefreshCw v-if="data.isExpanding" class="w-3.5 h-3.5 animate-spin" />
|
||||
<ArrowRight v-else class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -288,36 +459,50 @@ const startNewSession = () => {
|
||||
</div>
|
||||
|
||||
<!-- 底部全局操作栏 -->
|
||||
<div class="fixed bottom-10 left-1/2 -translate-x-1/2 z-50 flex items-center gap-4">
|
||||
<!-- 核心输入框 -->
|
||||
<div class="fixed bottom-12 left-1/2 -translate-x-1/2 z-50 flex flex-col items-center gap-4">
|
||||
<!-- 路径提示 (参考图片风格) -->
|
||||
<div
|
||||
class="bg-slate-900 rounded-2xl p-2 pl-6 flex items-center gap-4 shadow-2xl shadow-slate-300 border border-slate-800 w-[600px] group transition-all focus-within:ring-2 focus-within:ring-orange-500/20"
|
||||
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-orange-500 font-black text-xl select-none">></span>
|
||||
<input
|
||||
v-model="ideaInput"
|
||||
@keyup.enter="expandIdea"
|
||||
placeholder="Input your idea to expand..."
|
||||
class="bg-transparent border-none outline-none text-white placeholder:text-slate-500 flex-grow font-bold tracking-tight"
|
||||
/>
|
||||
<button
|
||||
@click="expandIdea"
|
||||
:disabled="isLoading"
|
||||
class="p-3 bg-orange-500 hover:bg-orange-600 text-white rounded-xl transition-all active:scale-90 disabled:opacity-50"
|
||||
>
|
||||
<Zap v-if="!isLoading" class="w-5 h-5" />
|
||||
<RefreshCw v-else class="w-5 h-5 animate-spin" />
|
||||
</button>
|
||||
<span class="text-emerald-500 font-bold">$</span>
|
||||
<span class="text-slate-400 text-xs font-bold">pwd:</span>
|
||||
<span class="text-slate-300 text-xs">~ /</span>
|
||||
<span class="text-slate-600 text-xs font-bold">think-flow</span>
|
||||
</div>
|
||||
|
||||
<!-- 清空按钮 -->
|
||||
<button
|
||||
@click="startNewSession"
|
||||
class="bg-white text-slate-900 p-4 rounded-2xl shadow-2xl border border-slate-200 hover:bg-slate-50 transition-all active:scale-95 group"
|
||||
title="Clear Canvas"
|
||||
>
|
||||
<Plus class="w-6 h-6 group-hover:rotate-90 transition-transform text-slate-400" />
|
||||
</button>
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- 核心输入框容器 -->
|
||||
<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"
|
||||
>
|
||||
<div class="flex items-center gap-3 pl-4 flex-grow">
|
||||
<span class="text-orange-500 font-black text-lg select-none">$</span>
|
||||
<input
|
||||
v-model="ideaInput"
|
||||
@keyup.enter="expandIdea"
|
||||
placeholder="Type your idea and press Enter..."
|
||||
class="bg-transparent border-none outline-none text-slate-800 placeholder:text-slate-300 flex-grow font-bold tracking-tight text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 pr-1">
|
||||
<button
|
||||
@click="expandIdea"
|
||||
:disabled="isLoading || !ideaInput.trim()"
|
||||
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"
|
||||
>
|
||||
<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 class="flex items-center gap-2">
|
||||
<!-- 这里可以根据需要放一些其他快捷按钮 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -335,6 +520,18 @@ body {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
@apply flex items-center gap-2 px-3 py-1.5 bg-white border border-slate-200 rounded-lg text-[10px] font-black tracking-widest transition-all active:scale-95 uppercase;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
@apply border-current shadow-sm;
|
||||
}
|
||||
|
||||
.toolbar-select {
|
||||
@apply px-3 py-1.5 bg-slate-50 border border-slate-100 rounded-lg text-[10px] font-black tracking-widest text-slate-500 outline-none cursor-pointer hover:border-slate-200 transition-all uppercase;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
@apply flex items-center gap-1 px-3 py-1 bg-white border border-slate-200 rounded-md text-xs font-medium text-slate-600 hover:border-slate-300 hover:shadow-sm transition-all;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user