feat: add personal canvas workflows
This commit is contained in:
@@ -42,11 +42,55 @@
|
||||
</div>
|
||||
|
||||
<!-- My workflows | 我的工作流 -->
|
||||
<div v-else class="empty-state">
|
||||
<n-icon :size="36" class="text-gray-500">
|
||||
<FolderOpenOutline />
|
||||
</n-icon>
|
||||
<p class="text-gray-500 text-sm mt-2">暂无自定义工作流</p>
|
||||
<div v-else class="my-workflows">
|
||||
<div class="my-toolbar">
|
||||
<button class="save-current-btn" @click="$emit('save-current')" title="保存当前工作流">
|
||||
<n-icon :size="15"><BookmarkOutline /></n-icon>
|
||||
<span>保存当前</span>
|
||||
</button>
|
||||
<button class="refresh-btn" @click="$emit('refresh-workflows')" title="刷新我的工作流">
|
||||
<n-icon :size="16"><RefreshOutline /></n-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingMyWorkflows" class="empty-state">
|
||||
<n-icon :size="30" class="text-gray-500">
|
||||
<RefreshOutline />
|
||||
</n-icon>
|
||||
<p class="text-gray-500 text-sm mt-2">正在加载...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="myWorkflows.length" class="workflow-grid">
|
||||
<div
|
||||
v-for="workflow in myWorkflows"
|
||||
:key="workflow.id"
|
||||
class="workflow-card my-workflow-card"
|
||||
@click="handleAddWorkflow(workflow)"
|
||||
>
|
||||
<button
|
||||
class="delete-workflow-btn"
|
||||
title="删除工作流"
|
||||
@click.stop="$emit('delete-workflow', workflow)"
|
||||
>
|
||||
<n-icon :size="13"><TrashOutline /></n-icon>
|
||||
</button>
|
||||
<div class="card-cover">
|
||||
<img v-if="workflow.thumbnail" :src="workflow.thumbnail" :alt="workflow.name" class="cover-img" />
|
||||
<n-icon v-else :size="34" class="cover-icon">
|
||||
<BookmarkOutline />
|
||||
</n-icon>
|
||||
</div>
|
||||
<div class="card-title">{{ workflow.name }}</div>
|
||||
<div class="card-meta">{{ formatWorkflowMeta(workflow) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<n-icon :size="36" class="text-gray-500">
|
||||
<FolderOpenOutline />
|
||||
</n-icon>
|
||||
<p class="text-gray-500 text-sm mt-2">暂无自定义工作流</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,15 +113,23 @@ import {
|
||||
BookOutline,
|
||||
PersonOutline,
|
||||
CartOutline,
|
||||
ChatbubbleOutline
|
||||
ChatbubbleOutline,
|
||||
BookmarkOutline,
|
||||
RefreshOutline,
|
||||
TrashOutline
|
||||
} from '@vicons/ionicons5'
|
||||
import { WORKFLOW_TEMPLATES } from '../config/workflows'
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean
|
||||
show: Boolean,
|
||||
myWorkflows: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
loadingMyWorkflows: Boolean
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:show', 'add-workflow'])
|
||||
const emit = defineEmits(['update:show', 'add-workflow', 'save-current', 'delete-workflow', 'refresh-workflows'])
|
||||
|
||||
// Active tab | 当前标签
|
||||
const activeTab = ref('public')
|
||||
@@ -113,6 +165,11 @@ const handleAddWorkflow = (workflow) => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const formatWorkflowMeta = (workflow) => {
|
||||
const count = workflow.workflowData?.nodes?.length || 0
|
||||
return `${count} 个节点`
|
||||
}
|
||||
|
||||
// Handle click outside | 点击外部关闭
|
||||
const handleClickOutside = () => {
|
||||
visible.value = false
|
||||
@@ -216,6 +273,53 @@ const vClickOutside = {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.my-workflows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.my-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.save-current-btn,
|
||||
.refresh-btn,
|
||||
.delete-workflow-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.save-current-btn {
|
||||
gap: 6px;
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.save-current-btn:hover,
|
||||
.refresh-btn:hover,
|
||||
.delete-workflow-btn:hover {
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* Workflow grid | 工作流网格 */
|
||||
.workflow-grid {
|
||||
display: grid;
|
||||
@@ -229,6 +333,10 @@ const vClickOutside = {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.my-workflow-card {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.workflow-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
@@ -269,6 +377,31 @@ const vClickOutside = {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
margin-top: 3px;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.delete-workflow-btn {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 6px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 7px;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.my-workflow-card:hover .delete-workflow-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Empty state | 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
|
||||
160
web/canvas-app/src/stores/workflows.js
Normal file
160
web/canvas-app/src/stores/workflows.js
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Personal workflow store | 个人工作流状态管理
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_SKG_API_BASE || '/api'
|
||||
const apiUrl = (path) => `${API_BASE}${path.startsWith('/') ? '' : '/'}${path}`
|
||||
|
||||
export const myWorkflows = ref([])
|
||||
export const workflowSyncStatus = ref('idle')
|
||||
export const workflowSyncError = ref('')
|
||||
|
||||
const requestJson = async (path, init = {}) => {
|
||||
const response = await fetch(apiUrl(path), {
|
||||
...init,
|
||||
headers: {
|
||||
...(init.body instanceof FormData ? {} : { 'Content-Type': 'application/json' }),
|
||||
...(init.headers || {})
|
||||
}
|
||||
})
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '')
|
||||
throw new Error(text || `${path} ${response.status}`)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
const secondsToDate = (value) => {
|
||||
if (value instanceof Date) return value
|
||||
const num = Number(value || 0)
|
||||
return new Date(num > 100000000000 ? num : num * 1000)
|
||||
}
|
||||
|
||||
const dateToSeconds = (value) => {
|
||||
if (value instanceof Date) return value.getTime() / 1000
|
||||
const parsed = new Date(value)
|
||||
return Number.isFinite(parsed.getTime()) ? parsed.getTime() / 1000 : Date.now() / 1000
|
||||
}
|
||||
|
||||
const workflowFromApi = (item) => ({
|
||||
id: item.id,
|
||||
name: item.name || '未命名工作流',
|
||||
description: item.description || '',
|
||||
thumbnail: item.thumbnail || '',
|
||||
workflowData: item.workflow_data || {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { x: 100, y: 50, zoom: 0.8 }
|
||||
},
|
||||
ownerId: item.owner_id || '',
|
||||
ownerName: item.owner_name || '',
|
||||
version: item.version || 1,
|
||||
createdAt: secondsToDate(item.created_at),
|
||||
updatedAt: secondsToDate(item.updated_at)
|
||||
})
|
||||
|
||||
const runtimeKeys = [
|
||||
'base64',
|
||||
'maskData',
|
||||
'loading',
|
||||
'error',
|
||||
'taskId',
|
||||
'progress',
|
||||
'status',
|
||||
'thumbnail',
|
||||
'outputContent',
|
||||
'executed',
|
||||
'revisedPrompt',
|
||||
'generatedImages',
|
||||
'generatedVideo'
|
||||
]
|
||||
|
||||
const cleanNodeForWorkflow = (node) => {
|
||||
const data = { ...(node.data || {}) }
|
||||
for (const key of runtimeKeys) delete data[key]
|
||||
|
||||
if (node.type === 'image') {
|
||||
data.url = ''
|
||||
}
|
||||
if (node.type === 'video') {
|
||||
data.url = ''
|
||||
data.duration = 0
|
||||
}
|
||||
if (node.type === 'llmConfig') {
|
||||
data.outputContent = ''
|
||||
}
|
||||
|
||||
delete data.createdAt
|
||||
delete data.updatedAt
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
position: {
|
||||
x: Number(node.position?.x || 0),
|
||||
y: Number(node.position?.y || 0)
|
||||
},
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
const cleanEdgeForWorkflow = (edge) => ({
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
sourceHandle: edge.sourceHandle || 'right',
|
||||
targetHandle: edge.targetHandle || 'left',
|
||||
type: edge.type,
|
||||
data: edge.data || {}
|
||||
})
|
||||
|
||||
export const cleanCanvasForWorkflow = (canvasData) => ({
|
||||
nodes: (canvasData?.nodes || []).map(cleanNodeForWorkflow),
|
||||
edges: (canvasData?.edges || []).map(cleanEdgeForWorkflow),
|
||||
viewport: canvasData?.viewport || { x: 100, y: 50, zoom: 0.8 }
|
||||
})
|
||||
|
||||
export const loadMyWorkflows = async () => {
|
||||
try {
|
||||
workflowSyncStatus.value = 'syncing'
|
||||
const response = await requestJson('/canvas-workflows')
|
||||
myWorkflows.value = (response.items || []).map(workflowFromApi)
|
||||
workflowSyncStatus.value = 'synced'
|
||||
workflowSyncError.value = ''
|
||||
return myWorkflows.value
|
||||
} catch (err) {
|
||||
workflowSyncStatus.value = 'error'
|
||||
workflowSyncError.value = err.message || '工作流加载失败'
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export const saveMyWorkflow = async ({ name, description = '', canvasData, sourceProjectId = '' }) => {
|
||||
const now = new Date()
|
||||
const payload = {
|
||||
name: (name || '').trim() || '未命名工作流',
|
||||
description,
|
||||
thumbnail: '',
|
||||
workflow_data: cleanCanvasForWorkflow(canvasData),
|
||||
created_at: dateToSeconds(now),
|
||||
updated_at: dateToSeconds(now),
|
||||
source: 'canvas',
|
||||
source_project_id: sourceProjectId
|
||||
}
|
||||
|
||||
workflowSyncStatus.value = 'syncing'
|
||||
const response = await requestJson('/canvas-workflows', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
const item = workflowFromApi(response.item)
|
||||
myWorkflows.value = [item, ...myWorkflows.value.filter(workflow => workflow.id !== item.id)]
|
||||
workflowSyncStatus.value = 'synced'
|
||||
workflowSyncError.value = ''
|
||||
return item
|
||||
}
|
||||
|
||||
export const deleteMyWorkflow = async (id) => {
|
||||
await requestJson(`/canvas-workflows/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
||||
myWorkflows.value = myWorkflows.value.filter(workflow => workflow.id !== id)
|
||||
}
|
||||
@@ -240,11 +240,28 @@
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- Save Workflow Modal | 保存工作流弹窗 -->
|
||||
<n-modal v-model:show="showSaveWorkflowModal" preset="dialog" title="保存当前工作流">
|
||||
<n-input v-model:value="workflowNameValue" placeholder="请输入工作流名称" />
|
||||
<template #action>
|
||||
<n-button @click="showSaveWorkflowModal = false">取消</n-button>
|
||||
<n-button type="primary" :loading="isSavingWorkflow" @click="confirmSaveWorkflow">保存</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- Download Modal | 下载弹窗 -->
|
||||
<DownloadModal v-model:show="showDownloadModal" />
|
||||
|
||||
<!-- Workflow Panel | 工作流面板 -->
|
||||
<WorkflowPanel v-model:show="showWorkflowPanel" @add-workflow="handleAddWorkflow" />
|
||||
<WorkflowPanel
|
||||
v-model:show="showWorkflowPanel"
|
||||
:my-workflows="myWorkflows"
|
||||
:loading-my-workflows="loadingMyWorkflows"
|
||||
@add-workflow="handleAddWorkflow"
|
||||
@save-current="openSaveWorkflowModal"
|
||||
@delete-workflow="handleDeleteWorkflow"
|
||||
@refresh-workflows="refreshMyWorkflows"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -285,6 +302,7 @@ import { loadAllModels } from '../stores/models'
|
||||
import { useChat, useWorkflowOrchestrator } from '../hooks'
|
||||
import { useModelStore } from '../stores/pinia'
|
||||
import { projects, initProjectsStore, updateProject, renameProject, currentProject } from '../stores/projects'
|
||||
import { myWorkflows, workflowSyncStatus, loadMyWorkflows, saveMyWorkflow, deleteMyWorkflow } from '../stores/workflows'
|
||||
|
||||
// API Settings component | API 设置组件
|
||||
import ApiSettings from '../components/ApiSettings.vue'
|
||||
@@ -397,7 +415,10 @@ const showRenameModal = ref(false)
|
||||
const showDeleteModal = ref(false)
|
||||
const showDownloadModal = ref(false)
|
||||
const showWorkflowPanel = ref(false)
|
||||
const showSaveWorkflowModal = ref(false)
|
||||
const renameValue = ref('')
|
||||
const workflowNameValue = ref('')
|
||||
const isSavingWorkflow = ref(false)
|
||||
|
||||
// Check if has downloadable assets | 检查是否有可下载素材
|
||||
const hasDownloadableAssets = computed(() => {
|
||||
@@ -413,6 +434,8 @@ const projectName = computed(() => {
|
||||
return project?.name || '未命名项目'
|
||||
})
|
||||
|
||||
const loadingMyWorkflows = computed(() => workflowSyncStatus.value === 'syncing')
|
||||
|
||||
// Project dropdown options | 项目下拉选项
|
||||
const projectOptions = [
|
||||
{ label: '重命名', key: 'rename' },
|
||||
@@ -472,42 +495,55 @@ const addNewNode = async (type) => {
|
||||
showNodeMenu.value = false
|
||||
}
|
||||
|
||||
// Handle add workflow from panel | 处理从面板添加工作流
|
||||
const handleAddWorkflow = ({ workflow, options }) => {
|
||||
// Calculate viewport center position | 计算视口中心位置
|
||||
const viewportCenterX = -viewport.value.x / viewport.value.zoom + (window.innerWidth / 2) / viewport.value.zoom
|
||||
const viewportCenterY = -viewport.value.y / viewport.value.zoom + (window.innerHeight / 2) / viewport.value.zoom
|
||||
const viewportCenterPosition = () => ({
|
||||
x: -viewport.value.x / viewport.value.zoom + (window.innerWidth / 2) / viewport.value.zoom,
|
||||
y: -viewport.value.y / viewport.value.zoom + (window.innerHeight / 2) / viewport.value.zoom
|
||||
})
|
||||
|
||||
// Create nodes from workflow template | 从工作流模板创建节点
|
||||
const startPosition = { x: viewportCenterX - 300, y: viewportCenterY - 200 }
|
||||
const { nodes: newNodes, edges: newEdges } = workflow.createNodes(startPosition, options)
|
||||
const insertWorkflowNodes = (workflowNodes, workflowEdges, workflowName, { reposition = false } = {}) => {
|
||||
const sourceNodes = (workflowNodes || []).filter(node => node?.type)
|
||||
if (!sourceNodes.length) {
|
||||
window.$message?.warning('这个工作流没有可插入的节点')
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate viewport center position | 计算视口中心位置
|
||||
const center = viewportCenterPosition()
|
||||
const startPosition = { x: center.x - 300, y: center.y - 200 }
|
||||
const minX = Math.min(...sourceNodes.map(node => Number(node.position?.x || 0)))
|
||||
const minY = Math.min(...sourceNodes.map(node => Number(node.position?.y || 0)))
|
||||
|
||||
// Start batch operation manually | 手动开始批量操作
|
||||
startBatchOperation()
|
||||
|
||||
// Add nodes to canvas in batch | 批量将节点添加到画布
|
||||
const nodeSpecs = newNodes.map(node => ({
|
||||
const nodeSpecs = sourceNodes.map(node => ({
|
||||
type: node.type,
|
||||
position: node.position,
|
||||
position: reposition
|
||||
? {
|
||||
x: startPosition.x + Number(node.position?.x || 0) - minX,
|
||||
y: startPosition.y + Number(node.position?.y || 0) - minY
|
||||
}
|
||||
: node.position,
|
||||
data: node.data
|
||||
}))
|
||||
const nodeIds = addNodes(nodeSpecs, false)
|
||||
|
||||
// Map old node IDs to new IDs | 映射旧节点ID到新ID
|
||||
const idMap = {}
|
||||
newNodes.forEach((node, index) => {
|
||||
idMap[node.id] = nodeIds[index]
|
||||
sourceNodes.forEach((node, index) => {
|
||||
if (node.id) idMap[node.id] = nodeIds[index]
|
||||
})
|
||||
|
||||
// Add edges to canvas in batch | 批量将边添加到画布
|
||||
const edgeSpecs = newEdges.map(edge => ({
|
||||
const edgeSpecs = (workflowEdges || []).map(edge => ({
|
||||
source: idMap[edge.source] || edge.source,
|
||||
target: idMap[edge.target] || edge.target,
|
||||
sourceHandle: edge.sourceHandle || 'right',
|
||||
targetHandle: edge.targetHandle || 'left',
|
||||
type: edge.type,
|
||||
data: edge.data
|
||||
}))
|
||||
})).filter(edge => edge.source && edge.target)
|
||||
|
||||
// Add edges (autoBatch=false to use manual batch) | 添加边(autoBatch=false 以使用手动批量)
|
||||
addEdges(edgeSpecs, false)
|
||||
@@ -523,7 +559,76 @@ const handleAddWorkflow = ({ workflow, options }) => {
|
||||
})
|
||||
}, 100)
|
||||
|
||||
window.$message?.success(`已添加工作流: ${workflow.name}`)
|
||||
window.$message?.success(`已添加工作流: ${workflowName}`)
|
||||
}
|
||||
|
||||
// Handle add workflow from panel | 处理从面板添加工作流
|
||||
const handleAddWorkflow = ({ workflow, options }) => {
|
||||
if (typeof workflow?.createNodes === 'function') {
|
||||
const center = viewportCenterPosition()
|
||||
const startPosition = { x: center.x - 300, y: center.y - 200 }
|
||||
const { nodes: newNodes, edges: newEdges } = workflow.createNodes(startPosition, options)
|
||||
insertWorkflowNodes(newNodes, newEdges, workflow.name, { reposition: false })
|
||||
return
|
||||
}
|
||||
|
||||
const workflowData = workflow?.workflowData || workflow?.workflow_data
|
||||
insertWorkflowNodes(workflowData?.nodes, workflowData?.edges, workflow?.name || '我的工作流', { reposition: true })
|
||||
}
|
||||
|
||||
const refreshMyWorkflows = async () => {
|
||||
try {
|
||||
await loadMyWorkflows()
|
||||
} catch (err) {
|
||||
window.$message?.error(err.message || '工作流加载失败')
|
||||
}
|
||||
}
|
||||
|
||||
const openSaveWorkflowModal = () => {
|
||||
if (!nodes.value.length) {
|
||||
window.$message?.warning('当前画布没有可保存的节点')
|
||||
return
|
||||
}
|
||||
workflowNameValue.value = `${projectName.value} 工作流`
|
||||
showSaveWorkflowModal.value = true
|
||||
}
|
||||
|
||||
const confirmSaveWorkflow = async () => {
|
||||
const name = workflowNameValue.value.trim()
|
||||
if (!name) {
|
||||
window.$message?.warning('请填写工作流名称')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isSavingWorkflow.value = true
|
||||
await saveMyWorkflow({
|
||||
name,
|
||||
canvasData: {
|
||||
nodes: nodes.value,
|
||||
edges: edges.value,
|
||||
viewport: viewport.value || canvasViewport.value
|
||||
},
|
||||
sourceProjectId: String(route.params.id || '')
|
||||
})
|
||||
showSaveWorkflowModal.value = false
|
||||
window.$message?.success('已保存到我的工作流')
|
||||
} catch (err) {
|
||||
window.$message?.error(err.message || '工作流保存失败')
|
||||
} finally {
|
||||
isSavingWorkflow.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteWorkflow = async (workflow) => {
|
||||
if (!workflow?.id) return
|
||||
if (!window.confirm(`确定删除工作流「${workflow.name}」吗?`)) return
|
||||
try {
|
||||
await deleteMyWorkflow(workflow.id)
|
||||
window.$message?.success('已删除工作流')
|
||||
} catch (err) {
|
||||
window.$message?.error(err.message || '工作流删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle connection | 处理连接
|
||||
@@ -841,6 +946,12 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch(showWorkflowPanel, (visible) => {
|
||||
if (visible && workflowSyncStatus.value === 'idle') {
|
||||
refreshMyWorkflows()
|
||||
}
|
||||
})
|
||||
|
||||
// Initialize | 初始化
|
||||
onMounted(async () => {
|
||||
checkMobile()
|
||||
@@ -851,6 +962,7 @@ onMounted(async () => {
|
||||
|
||||
// Load project data | 加载项目数据
|
||||
loadProjectById(route.params.id)
|
||||
refreshMyWorkflows()
|
||||
|
||||
// Check for initial prompt from home page | 检查来自首页的初始提示词
|
||||
const initialPrompt = sessionStorage.getItem('ai-canvas-initial-prompt')
|
||||
|
||||
Reference in New Issue
Block a user