Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion packages/canvas/DesignCanvas/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@ import type { Node, RootNode } from '../../../types'

export type PageSchema = RootNode

export type AIHelperState = 'hidden' | 'chat' | 'loading' | 'confirm' | 'completed'
Comment thread
lichunn marked this conversation as resolved.

export interface NodeAIStatus {
state: AIHelperState
collapsed?: boolean // 面板是否收起(收起时保留原状态,重新打开可恢复)
aiContext?: any
lastAIAction?: string
aiHistory?: Array<{
timestamp: number
action: string
content: any
}>
chatContent?: string // 聊天内容
// AI采纳状态相关字段
originalNodeData?: any // AI修改前的节点数据备份
aiModifiedNodeData?: any // AI修改后的节点数据
}

export interface NodeStatus {
[key: string]: any
aiStatus?: NodeAIStatus // 现在包含了所有的AI状态,包括采纳状态
}

export interface PageState {
currentVm?: unknown
currentSchema?: { [x: string]: any; id: string }
Expand All @@ -17,7 +40,7 @@ export interface PageState {
isSaved: boolean
isLock: boolean
isBlock: boolean
nodesStatus: Record<string, any>
nodesStatus: Record<string, NodeStatus>
loading: boolean
}

Expand Down
100 changes: 99 additions & 1 deletion packages/canvas/DesignCanvas/src/api/useCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
ChangePropsOperation,
DeleteOperation,
InsertOperation,
NodeAIStatus,
NodeOperation,
PageSchema,
PageState,
Expand Down Expand Up @@ -84,6 +85,43 @@ const rootSchema = ref([
}
])

// 初始化单个节点的AI状态
const initializeNodeAIStatus = (node: object, initialStatus: Partial<NodeAIStatus> = {}) => {
Comment thread
lichunn marked this conversation as resolved.
Outdated
if (!pageState.nodesStatus[node.id]) {
pageState.nodesStatus[node.id] = {}
}

pageState.nodesStatus[node.id].aiStatus = {
state: 'hidden',
originalNodeData: deepClone(node),
aiModifiedNodeData: undefined,
aiContext: null,
lastAIAction: '',
aiHistory: [],
...initialStatus
}
Comment thread
lichunn marked this conversation as resolved.
Outdated
}

// 初始化所有现有节点的AI状态
const initializeAllNodesAIStatus = () => {
// 递归遍历 pageSchema 的 children 来初始化所有节点的AI状态
const traverseNodes = (nodes: any[]) => {
if (!nodes) return
nodes.forEach((node) => {
if (node.id && !pageState.nodesStatus[node.id]?.aiStatus) {
initializeNodeAIStatus(node)
}
if (Array.isArray(node.children) && node.children.length) {
traverseNodes(node.children)
}
})
}

if (pageState.pageSchema?.children) {
traverseNodes(pageState.pageSchema.children)
}
Comment thread
lichunn marked this conversation as resolved.
}

const handleTinyGridColumnsSlots = (node: Node) => {
const columns = Array.isArray(node.props?.columns) ? node.props.columns : []
for (const columnItem of columns) {
Expand Down Expand Up @@ -182,6 +220,8 @@ const resetCanvasState = async (state: Partial<PageState> = {}) => {
Object.assign(pageState, defaultPageState, state)

nodesMap.value.clear()
// 切换页面时清空所有节点的AI状态,避免旧页面的AI状态残留
pageState.nodesStatus = {}
Comment thread
lichunn marked this conversation as resolved.
Outdated

if (pageState.pageSchema) {
if (!pageState.pageSchema.children) {
Expand All @@ -200,6 +240,9 @@ const resetCanvasState = async (state: Partial<PageState> = {}) => {
nodesMap.value.set(0, { node: rootSchema.value, parent: pageState.pageSchema })

generateNodesMap(pageState.pageSchema.children, pageState.pageSchema)

// 初始化所有节点的AI状态
initializeAllNodesAIStatus()
}

const diffPatch = jsonDiffPatchInstance.diff(previousSchema, pageState.pageSchema)
Expand All @@ -208,6 +251,42 @@ const resetCanvasState = async (state: Partial<PageState> = {}) => {
publish({ topic: 'schemaImport', data: { current: pageState.pageSchema, previous: previousSchema, diffPatch } })
}

// 更新页面schema,保留AI状态(不清空nodesStatus)
const updatePageSchema = (newPageSchema: any) => {
const previousSchema = JSON.parse(JSON.stringify(pageState.pageSchema))

pageState.pageSchema = newPageSchema

if (!newPageSchema.children) {
newPageSchema.children = []
}

rootSchema.value = [
{
id: 0,
componentName: 'div',
props: newPageSchema.props || {},
children: newPageSchema.children
}
]

// 重建 nodesMap
nodesMap.value.clear()
nodesMap.value.set(0, { node: rootSchema.value, parent: newPageSchema })
generateNodesMap(newPageSchema.children, newPageSchema)

// 为新增的节点初始化AI状态(已存在的不覆盖)
nodesMap.value.forEach(({ node }) => {
if (node.id && !pageState.nodesStatus[node.id]?.aiStatus) {
initializeNodeAIStatus(node)
}
})
Comment thread
lichunn marked this conversation as resolved.
Outdated
Comment thread
lichunn marked this conversation as resolved.
Outdated

const diffPatch = jsonDiffPatchInstance.diff(previousSchema, newPageSchema)

publish({ topic: 'schemaImport', data: { current: newPageSchema, previous: previousSchema, diffPatch } })
}
Comment thread
lichunn marked this conversation as resolved.

// 页面重置画布数据
const resetPageCanvasState = (state: Partial<PageState> = {}) => {
state.isBlock = false
Expand Down Expand Up @@ -383,10 +462,28 @@ const operationTypeMap = {

setNode(newNodeData, parentNode)

// 初始化新节点的AI状态
if (newNodeData.id) {
initializeNodeAIStatus(newNodeData)
}

// 6. 如果新节点有子节点,递归构建 nodeMap
if (Array.isArray(newNodeData?.children) && newNodeData.children.length > 0) {
const newNode = getNode(newNodeData.id)
generateNodesMap(newNodeData.children, newNode)

// 递归初始化所有子节点的AI状态
const initChildrenAIStatus = (children: Node[]) => {
children.forEach((child) => {
if (child.id) {
initializeNodeAIStatus(child)
}
if (Array.isArray(child?.children) && child.children.length > 0) {
initChildrenAIStatus(child.children)
}
})
}
initChildrenAIStatus(newNodeData.children)
Comment thread
lichunn marked this conversation as resolved.
Comment thread
lichunn marked this conversation as resolved.
}

// 7. 返回插入结果
Expand Down Expand Up @@ -684,6 +781,7 @@ export default function () {
exportSchema,
getSchema,
getNodePath,
updateSchema
updateSchema,
updatePageSchema
}
}
Binary file added packages/canvas/container/assets/loading.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
115 changes: 115 additions & 0 deletions packages/canvas/container/src/components/AIConfirmDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<template>
<div class="ai-confirm-dialog">
<div class="ai-confirm-header">
<div class="header-left">
<icon-successful class="icon-successful"></icon-successful>
<span class="header-title">AI操作已完成,您可选择采纳或放弃</span>
</div>
</div>
<div class="ai-confirm-actions-row">
<div class="actions-right">
<svg-icon name="refresh" class="refresh-icon" @click="handleRefresh" title="重新生成"></svg-icon>
<tiny-button class="actions-btn" @click="handleCancel" round>放弃</tiny-button>
<tiny-button class="actions-btn" @click="handleConfirm" type="primary" round> 采纳</tiny-button>
</div>
</div>
</div>
Comment thread
lichunn marked this conversation as resolved.
</template>

<script>
import { TinyButton } from '@opentiny/vue'
import { IconSuccessful } from '@opentiny/vue-icon'

export default {
components: {
IconSuccessful: IconSuccessful(),
TinyButton
},
emits: ['confirm', 'cancel', 'close', 'refresh'],

setup(props, { emit }) {
const handleConfirm = () => {
emit('confirm')
}

const handleCancel = () => {
emit('cancel')
}

const handleRefresh = () => {
emit('refresh')
}

return {
handleConfirm,
handleCancel,
handleRefresh
}
}
}
</script>

<style lang="less" scoped>
.ai-confirm-dialog {
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
overflow: hidden;
display: flex;
flex-direction: column;
padding: 16px 20px;
}

/* 第一行:标题行 */
.ai-confirm-header {
display: flex;
align-items: center;
margin-bottom: 12px;

.header-left {
display: flex;
align-items: center;
}

.icon-successful {
font-size: 20px;
fill: #5cb300;
margin-right: 8px;
}

.header-title {
font-size: 14px;
font-weight: 500;
color: #191919;
margin-right: 16px;
}

.close-icon {
cursor: pointer;
font-size: 16px;
color: #000;
Comment thread
lichunn marked this conversation as resolved.
}
}

/* 第二行:操作行 */
.ai-confirm-actions-row {
display: flex;
justify-content: right;
align-items: center;

.actions-right {
display: flex;
gap: 12px;
align-items: center;
.refresh-icon {
cursor: pointer;
color: #000;
font-size: 18px;
}
.actions-btn {
min-width: 68px;
height: 28px;
}
}
}
</style>
Loading
Loading