From bf73f5b6592ee071bb66746e636480eb76723d4b Mon Sep 17 00:00:00 2001 From: jazzson51569 Date: Sat, 6 Jun 2026 18:20:08 +0800 Subject: [PATCH] feat: implement hierarchical prompt structure with drag-drop and keyboard indentation --- apps/desktop/src/main/ipc/prompt.ipc.ts | 6 + apps/desktop/src/preload/api/prompt.ts | 2 + .../components/layout/MainContent.tsx | 24 ++ .../components/prompt/PromptListView.tsx | 235 ++++++++++++++++-- .../desktop/src/renderer/services/database.ts | 33 +++ .../src/renderer/stores/prompt.store.ts | 7 + packages/db/src/prompt.ts | 102 +++++++- packages/db/src/schema.ts | 7 +- packages/shared/constants/ipc-channels.ts | 1 + packages/shared/types/prompt.ts | 4 + 10 files changed, 391 insertions(+), 30 deletions(-) diff --git a/apps/desktop/src/main/ipc/prompt.ipc.ts b/apps/desktop/src/main/ipc/prompt.ipc.ts index 99193119..faabc7f0 100644 --- a/apps/desktop/src/main/ipc/prompt.ipc.ts +++ b/apps/desktop/src/main/ipc/prompt.ipc.ts @@ -249,4 +249,10 @@ export function registerPromptIPC(db: PromptDB, folderDb: FolderDB, rawDb: Datab db.insertVersionDirect(version); return true; }); + + ipcMain.handle(IPC_CHANNELS.PROMPT_MOVE, async (_, promptId: string, newParentId: string | null, newOrder: number) => { + db.movePrompt(promptId, newParentId, newOrder); + syncWorkspace(); + return true; + }); } diff --git a/apps/desktop/src/preload/api/prompt.ts b/apps/desktop/src/preload/api/prompt.ts index 2977db4c..e6cd0cd0 100644 --- a/apps/desktop/src/preload/api/prompt.ts +++ b/apps/desktop/src/preload/api/prompt.ts @@ -40,4 +40,6 @@ export const promptApi = { prompts: Prompt[]; versions: PromptVersion[]; }) => ipcRenderer.invoke(IPC_CHANNELS.PROMPT_MIGRATE_IDB_BATCH, payload), + move: (promptId: string, newParentId: string | null, newOrder: number) => + ipcRenderer.invoke(IPC_CHANNELS.PROMPT_MOVE, promptId, newParentId, newOrder), }; diff --git a/apps/desktop/src/renderer/components/layout/MainContent.tsx b/apps/desktop/src/renderer/components/layout/MainContent.tsx index b4d791ff..1919b451 100644 --- a/apps/desktop/src/renderer/components/layout/MainContent.tsx +++ b/apps/desktop/src/renderer/components/layout/MainContent.tsx @@ -24,6 +24,7 @@ const PromptQuickRewriteDialog = lazy(() => import('../prompt/PromptQuickRewrite const PromptTableView = lazy(() => import('../prompt/PromptTableView').then(m => ({ default: m.PromptTableView }))); const PromptGalleryView = lazy(() => import('../prompt/PromptGalleryView').then(m => ({ default: m.PromptGalleryView }))); const PromptKanbanView = lazy(() => import('../prompt/PromptKanbanView').then(m => ({ default: m.PromptKanbanView }))); +const PromptListView = lazy(() => import('../prompt/PromptListView').then(m => ({ default: m.PromptListView }))); const AiTestModal = lazy(() => import('../prompt/AiTestModal').then(m => ({ default: m.AiTestModal }))); const PromptDetailModal = lazy(() => import('../prompt/PromptDetailModal').then(m => ({ default: m.PromptDetailModal }))); const VariableInputModal = lazy(() => import('../prompt/VariableInputModal').then(m => ({ default: m.VariableInputModal }))); @@ -367,6 +368,7 @@ function PromptSkillMainContent() { const sortOrder = usePromptStore((state) => state.sortOrder); const viewMode = usePromptStore((state) => state.viewMode); const incrementUsageCount = usePromptStore((state) => state.incrementUsageCount); + const movePrompt = usePromptStore((state) => state.movePrompt); // Resizable prompt-list pane width (#119) const promptListPaneWidth = useUIStore((state) => state.promptListPaneWidth); const setPromptListPaneWidth = useUIStore( @@ -2014,6 +2016,28 @@ function PromptSkillMainContent() { )} + {/* List view mode: hierarchical list with drag-and-drop */} + {/* 列表视图模式:分层列表支持拖拽 */} +
+ + {viewMode === 'list' && ( + + selectPrompt(id)} + onToggleFavorite={toggleFavorite} + onCopy={handleCopyPrompt} + onContextMenu={handleContextMenu} + onMovePrompt={movePrompt} + /> + + )} +
+ {/* Card view mode: two-column layout */} {/* 卡片视图模式:左右分栏 */}
void; onToggleFavorite: (id: string) => void; onCopy: (prompt: Prompt) => void; onContextMenu: (e: React.MouseEvent, prompt: Prompt) => void; + onMovePrompt: (promptId: string, newParentId: string | null, newOrder: number) => void; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; } export function PromptListView({ prompts, selectedId, + selectedIds, onSelect, onToggleFavorite, onCopy, onContextMenu, + onMovePrompt, }: PromptListViewProps) { const { t } = useTranslation(); + const [draggingId, setDraggingId] = useState(null); + const [dropTargetId, setDropTargetId] = useState(null); + const [dropPosition, setDropPosition] = useState<'before' | 'after' | 'inside' | null>(null); + const [expandedIds, setExpandedIds] = useState>(new Set()); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (!selectedId || draggingId) return; + + const selectedPrompt = prompts.find(p => p.id === selectedId); + if (!selectedPrompt) return; + + if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + + if (e.shiftKey) { + // Shift+Tab: outdent (move to parent level) + if (selectedPrompt.parentId) { + const siblings = prompts.filter(p => p.parentId === selectedPrompt.parentId); + const parentPrompt = prompts.find(p => p.id === selectedPrompt.parentId); + const newOrder = parentPrompt ? prompts.filter(p => p.parentId === parentPrompt?.parentId).length : prompts.filter(p => p.parentId === null).length; + onMovePrompt(selectedId, parentPrompt?.parentId || null, newOrder); + } + } else { + // Tab: indent (move to previous sibling's child) + const siblings = prompts.filter(p => p.parentId === selectedPrompt.parentId).sort((a, b) => (a.order || 0) - (b.order || 0)); + const currentIndex = siblings.findIndex(s => s.id === selectedId); + + if (currentIndex > 0) { + const prevSibling = siblings[currentIndex - 1]; + const childCount = prompts.filter(p => p.parentId === prevSibling.id).length; + onMovePrompt(selectedId, prevSibling.id, childCount); + } + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [selectedId, prompts, onMovePrompt, draggingId]); - // Format date - // 格式化日期 const formatDate = (dateStr: string) => { const date = new Date(dateStr); const now = new Date(); @@ -40,29 +85,169 @@ export function PromptListView({ } }; - return ( -
- {prompts.map((prompt) => ( + const toggleExpand = useCallback((promptId: string) => { + setExpandedIds(prev => { + const newSet = new Set(prev); + if (newSet.has(promptId)) { + newSet.delete(promptId); + } else { + newSet.add(promptId); + } + return newSet; + }); + }, []); + + const hasChildren = useCallback((promptId: string) => { + return prompts.some(p => p.parentId === promptId); + }, [prompts]); + + const getChildren = useCallback((parentId: string | null) => { + return prompts + .filter(p => p.parentId === parentId) + .sort((a, b) => (a.order || 0) - (b.order || 0)); + }, [prompts]); + + const handleDragStart = useCallback((e: React.DragEvent, promptId: string) => { + setDraggingId(promptId); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', promptId); + }, []); + + const handleDragEnd = useCallback(() => { + setDraggingId(null); + setDropTargetId(null); + setDropPosition(null); + }, []); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + }, []); + + const handleDragEnter = useCallback((e: React.DragEvent, promptId: string) => { + e.preventDefault(); + if (draggingId !== promptId) { + setDropTargetId(promptId); + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + const y = e.clientY - rect.top; + const height = rect.height; + + if (y < height / 3) { + setDropPosition('before'); + } else if (y > height * 2 / 3) { + setDropPosition('after'); + } else { + setDropPosition('inside'); + } + } + }, [draggingId]); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setDropTargetId(null); + setDropPosition(null); + }, []); + + const handleDrop = useCallback((e: React.DragEvent, targetPromptId: string) => { + e.preventDefault(); + if (draggingId && draggingId !== targetPromptId) { + const targetPrompt = prompts.find(p => p.id === targetPromptId); + const draggingPrompt = prompts.find(p => p.id === draggingId); + + if (targetPrompt && draggingPrompt) { + if (dropPosition === 'inside') { + const childCount = prompts.filter(p => p.parentId === targetPromptId).length; + onMovePrompt(draggingId, targetPromptId, childCount); + } else { + const newParentId = targetPrompt.parentId; + let targetOrder = targetPrompt.order || 0; + + if (dropPosition === 'after') { + targetOrder += 1; + } + + if (draggingPrompt.parentId === newParentId && draggingPrompt.order && draggingPrompt.order < targetOrder) { + targetOrder -= 1; + } + + onMovePrompt(draggingId, newParentId, targetOrder); + } + } + } + setDraggingId(null); + setDropTargetId(null); + setDropPosition(null); + }, [draggingId, dropPosition, prompts, onMovePrompt]); + + const isSelected = useCallback((promptId: string) => { + return selectedId === promptId || selectedIds.includes(promptId); + }, [selectedId, selectedIds]); + + const isDragging = useCallback((promptId: string) => { + return draggingId === promptId; + }, [draggingId]); + + const isDropTarget = useCallback((promptId: string) => { + return dropTargetId === promptId; + }, [dropTargetId]); + + const renderTreeNode = useCallback((prompt: Prompt, depth: number) => { + const hasKids = hasChildren(prompt.id); + const isExpanded = expandedIds.has(prompt.id); + + return ( +
handleDragStart(e, prompt.id)} + onDragEnd={handleDragEnd} + onDragOver={handleDragOver} + onDragEnter={(e) => handleDragEnter(e, prompt.id)} + onDragLeave={handleDragLeave} + onDrop={(e) => handleDrop(e, prompt.id)} onClick={() => onSelect(prompt.id)} onContextMenu={(e) => onContextMenu(e, prompt)} className={` flex items-center gap-3 px-3 py-2.5 border-b border-border/50 cursor-pointer - transition-colors duration-quick - ${selectedId === prompt.id + transition-colors duration-quick relative + ${isSelected(prompt.id) ? 'bg-primary/10 border-l-2 border-l-primary' - : 'hover:bg-accent/50' + : isDropTarget(prompt.id) && dropPosition === 'inside' + ? 'bg-primary/20 border-l-2 border-l-primary' + : 'hover:bg-accent/50' } + ${isDragging(prompt.id) ? 'opacity-50' : ''} + ${isDropTarget(prompt.id) && dropPosition === 'inside' ? 'ring-2 ring-primary/50 ring-inset' : ''} + ${isDropTarget(prompt.id) && dropPosition === 'before' ? 'border-t-2 border-t-primary' : ''} + ${isDropTarget(prompt.id) && dropPosition === 'after' ? 'border-b-2 border-b-primary' : ''} `} + style={{ paddingLeft: `${depth * 16 + 12}px` }} > - {/* Title and description */} - {/* 标题和描述 */} +
+ {hasKids && ( + + )} + {!hasKids && } + +
+

@@ -85,24 +270,18 @@ export function PromptListView({ )}

- {/* Usage count */} - {/* 使用次数 */}
{prompt.usageCount || 0}
- {/* Update time */} - {/* 更新时间 */}
{formatDate(prompt.updatedAt)}
- {/* Action buttons */} - {/* 操作按钮 */}
- ))} + + {hasKids && isExpanded && ( +
+ {getChildren(prompt.id).map((child) => renderTreeNode(child, depth + 1))} +
+ )} +
+ ); + }, [hasChildren, expandedIds, toggleExpand, getChildren, isSelected, isDragging, isDropTarget, dropPosition, onSelect, onContextMenu, handleDragStart, handleDragEnd, handleDragOver, handleDragEnter, handleDragLeave, handleDrop, onCopy, onToggleFavorite, t]); + + const rootNodes = getChildren(null); + + return ( +
+ {rootNodes.map((node) => renderTreeNode(node, 0))}
); -} +} \ No newline at end of file diff --git a/apps/desktop/src/renderer/services/database.ts b/apps/desktop/src/renderer/services/database.ts index 36d35d39..767d0507 100644 --- a/apps/desktop/src/renderer/services/database.ts +++ b/apps/desktop/src/renderer/services/database.ts @@ -356,6 +356,39 @@ export async function movePrompts( } } +export async function movePrompt( + promptId: string, + newParentId: string | null, + newOrder: number, +): Promise { + if (window.api?.prompt?.move) { + await window.api.prompt.move(promptId, newParentId, newOrder); + return; + } + + const database = await getDatabase(); + return new Promise((resolve, reject) => { + const transaction = database.transaction(STORES.PROMPTS, "readwrite"); + const store = transaction.objectStore(STORES.PROMPTS); + const getRequest = store.get(promptId); + + getRequest.onsuccess = () => { + const prompt = getRequest.result; + if (prompt) { + prompt.parentId = newParentId; + prompt.order = newOrder; + prompt.updatedAt = new Date().toISOString(); + const putRequest = store.put(prompt); + putRequest.onsuccess = () => resolve(); + putRequest.onerror = () => reject(putRequest.error); + } else { + resolve(); + } + }; + getRequest.onerror = () => reject(getRequest.error); + }); +} + // ==================== Version 操作 ==================== // ==================== Version Operations ==================== diff --git a/apps/desktop/src/renderer/stores/prompt.store.ts b/apps/desktop/src/renderer/stores/prompt.store.ts index 37a36624..19e1c30a 100644 --- a/apps/desktop/src/renderer/stores/prompt.store.ts +++ b/apps/desktop/src/renderer/stores/prompt.store.ts @@ -60,6 +60,7 @@ interface PromptState { setGalleryImageSize: (size: GalleryImageSize) => void; setKanbanColumns: (columns: KanbanColumns) => void; incrementUsageCount: (id: string) => Promise; + movePrompt: (promptId: string, newParentId: string | null, newOrder: number) => Promise; } export const usePromptStore = create()( @@ -228,6 +229,12 @@ export const usePromptStore = create()( })); } }, + + movePrompt: async (promptId, newParentId, newOrder) => { + await db.movePrompt(promptId, newParentId, newOrder); + await get().fetchPrompts(); + scheduleAllSaveSync("prompt:move"); + }, }), { name: "prompt-store", diff --git a/packages/db/src/prompt.ts b/packages/db/src/prompt.ts index 5b253d53..b587331f 100644 --- a/packages/db/src/prompt.ts +++ b/packages/db/src/prompt.ts @@ -24,6 +24,8 @@ interface PromptRow { variables: string | null; tags: string | null; folder_id: string | null; + parent_id: string | null; + sort_order: number; images: string | null; videos: string | null; is_favorite: number; @@ -65,9 +67,9 @@ export class PromptDB { const stmt = this.db.prepare(` INSERT INTO prompts ( id, visibility, title, description, prompt_type, system_prompt, system_prompt_en, user_prompt, - user_prompt_en, variables, tags, folder_id, images, videos, source, notes, + user_prompt_en, variables, tags, folder_id, parent_id, sort_order, images, videos, source, notes, last_ai_response, is_favorite, current_version, usage_count, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run( @@ -83,16 +85,18 @@ export class PromptDB { JSON.stringify(data.variables || []), JSON.stringify(data.tags || []), data.folderId || null, + null, + 0, JSON.stringify(data.images || []), JSON.stringify(data.videos || []), data.source || null, data.notes || null, null, 0, - 0, - 0, - now, - now, + 0, + 0, + now, + now, ); // Create initial version @@ -183,6 +187,14 @@ export class PromptDB { updates.push("folder_id = ?"); values.push(data.folderId); } + if (data.parentId !== undefined) { + updates.push("parent_id = ?"); + values.push(data.parentId); + } + if (data.order !== undefined) { + updates.push("sort_order = ?"); + values.push(data.order); + } if (data.images !== undefined) { updates.push("images = ?"); values.push(JSON.stringify(data.images)); @@ -260,6 +272,8 @@ export class PromptDB { ...(data.variables !== undefined && { variables: data.variables }), ...(data.tags !== undefined && { tags: data.tags }), ...(data.folderId !== undefined && { folderId: data.folderId }), + ...(data.parentId !== undefined && { parentId: data.parentId }), + ...(data.order !== undefined && { order: data.order }), ...(data.images !== undefined && { images: data.images }), ...(data.videos !== undefined && { videos: data.videos }), ...(data.isFavorite !== undefined && { isFavorite: data.isFavorite }), @@ -506,11 +520,11 @@ export class PromptDB { .prepare( `INSERT OR REPLACE INTO prompts ( id, visibility, title, description, prompt_type, system_prompt, system_prompt_en, user_prompt, - user_prompt_en, variables, tags, folder_id, images, videos, is_favorite, is_pinned, + user_prompt_en, variables, tags, folder_id, parent_id, sort_order, images, videos, is_favorite, is_pinned, current_version, usage_count, source, notes, last_ai_response, created_at, updated_at ) VALUES ( @id, @visibility, @title, @description, @prompt_type, @system_prompt, @system_prompt_en, @user_prompt, - @user_prompt_en, @variables, @tags, @folder_id, @images, @videos, @is_favorite, @is_pinned, + @user_prompt_en, @variables, @tags, @folder_id, @parent_id, @sort_order, @images, @videos, @is_favorite, @is_pinned, @current_version, @usage_count, @source, @notes, @last_ai_response, @created_at, @updated_at )`, ) @@ -527,6 +541,8 @@ export class PromptDB { "@variables": JSON.stringify(prompt.variables ?? []), "@tags": JSON.stringify(prompt.tags ?? []), "@folder_id": prompt.folderId ?? null, + "@parent_id": prompt.parentId ?? null, + "@sort_order": prompt.order ?? 0, "@images": JSON.stringify(prompt.images ?? []), "@videos": JSON.stringify(prompt.videos ?? []), "@is_favorite": prompt.isFavorite ? 1 : 0, @@ -669,6 +685,74 @@ export class PromptDB { txn(); } + /** + * Move prompt to a new parent or reorder within the same parent + * 移动提示词到新的父节点或在同级中重新排序 + */ + movePrompt(promptId: string, newParentId: string | null, newOrder: number): void { + const txn = this.db.transaction(() => { + // Get current parent and order + const current = this.db + .prepare("SELECT parent_id, sort_order FROM prompts WHERE id = ?") + .get(promptId) as { parent_id: string | null; sort_order: number } | undefined; + + if (!current) return; + + const oldParentId = current.parent_id; + const oldOrder = current.sort_order; + + // Update the moved prompt + this.db + .prepare("UPDATE prompts SET parent_id = ?, sort_order = ?, updated_at = ? WHERE id = ?") + .run(newParentId, newOrder, Date.now(), promptId); + + // If parent changed, adjust orders in old parent + if (oldParentId !== newParentId && oldParentId !== null) { + this.db + .prepare("UPDATE prompts SET sort_order = sort_order - 1 WHERE parent_id = ? AND sort_order > ?") + .run(oldParentId, oldOrder); + } + + // Adjust orders in new parent to make room + if (newParentId !== null) { + this.db + .prepare("UPDATE prompts SET sort_order = sort_order + 1 WHERE parent_id = ? AND sort_order >= ? AND id != ?") + .run(newParentId, newOrder, promptId); + } else { + // No parent (root level) + this.db + .prepare("UPDATE prompts SET sort_order = sort_order + 1 WHERE parent_id IS NULL AND sort_order >= ? AND id != ?") + .run(newOrder, promptId); + } + }); + + txn(); + } + + /** + * Get children of a prompt + * 获取提示词的子节点 + */ + getChildren(parentId: string | null): Prompt[] { + const stmt = this.db.prepare( + parentId === null + ? "SELECT * FROM prompts WHERE parent_id IS NULL ORDER BY sort_order" + : "SELECT * FROM prompts WHERE parent_id = ? ORDER BY sort_order" + ); + const rows = stmt.all(parentId === null ? [] : [parentId]) as PromptRow[]; + return rows.map((row) => this.rowToPrompt(row)); + } + + /** + * Get all prompts with hierarchical structure + * 获取所有提示词(包含层级结构) + */ + getAllWithHierarchy(): Prompt[] { + const stmt = this.db.prepare("SELECT * FROM prompts ORDER BY parent_id NULLS FIRST, sort_order"); + const rows = stmt.all() as PromptRow[]; + return rows.map((row) => this.rowToPrompt(row)); + } + /** * Convert database row to Prompt object @@ -689,6 +773,8 @@ export class PromptDB { variables: JSON.parse(row.variables || "[]"), tags: JSON.parse(row.tags || "[]"), folderId: row.folder_id, + parentId: row.parent_id, + order: row.sort_order, images: JSON.parse(row.images || "[]"), videos: JSON.parse(row.videos || "[]"), isFavorite: row.is_favorite === 1, diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 58d08d5c..03837a31 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -22,6 +22,8 @@ CREATE TABLE IF NOT EXISTS prompts ( variables TEXT, tags TEXT, folder_id TEXT, + parent_id TEXT, + sort_order INTEGER DEFAULT 0, images TEXT, videos TEXT, is_favorite INTEGER DEFAULT 0, @@ -33,7 +35,8 @@ CREATE TABLE IF NOT EXISTS prompts ( last_ai_response TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, - FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE SET NULL + FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE SET NULL, + FOREIGN KEY (parent_id) REFERENCES prompts(id) ON DELETE CASCADE ); -- 版本表 @@ -226,6 +229,8 @@ CREATE INDEX IF NOT EXISTS idx_folders_sort ON folders(sort_order); CREATE INDEX IF NOT EXISTS idx_prompts_folder_favorite ON prompts(folder_id, is_favorite); CREATE INDEX IF NOT EXISTS idx_prompts_folder_updated ON prompts(folder_id, updated_at DESC); +CREATE INDEX IF NOT EXISTS idx_prompts_parent ON prompts(parent_id); +CREATE INDEX IF NOT EXISTS idx_prompts_sort_order ON prompts(sort_order); -- 全文搜索 (FTS5) CREATE VIRTUAL TABLE IF NOT EXISTS prompts_fts USING fts5( diff --git a/packages/shared/constants/ipc-channels.ts b/packages/shared/constants/ipc-channels.ts index 5dbeb49d..5bd0a80d 100644 --- a/packages/shared/constants/ipc-channels.ts +++ b/packages/shared/constants/ipc-channels.ts @@ -26,6 +26,7 @@ export const IPC_CHANNELS = { * 若 SQLite 已有数据则直接返回 { imported: false }(防止覆盖)。 */ PROMPT_MIGRATE_IDB_BATCH: "prompt:migrateIdbBatch", + PROMPT_MOVE: "prompt:move", // Version VERSION_GET_ALL: "version:getAll", diff --git a/packages/shared/types/prompt.ts b/packages/shared/types/prompt.ts index b2a2b2b6..f4ff302a 100644 --- a/packages/shared/types/prompt.ts +++ b/packages/shared/types/prompt.ts @@ -21,6 +21,8 @@ export interface Prompt { variables: Variable[]; tags: string[]; folderId?: string | null; + parentId?: string | null; // Parent prompt ID for hierarchical structure + order?: number; // Sort order within the same parent images?: string[]; videos?: string[]; // Video file names for preview / 视频预览文件名 isFavorite: boolean; @@ -91,6 +93,8 @@ export interface UpdatePromptDTO { variables?: Variable[]; tags?: string[]; folderId?: string | null; + parentId?: string | null; + order?: number; images?: string[]; videos?: string[]; isFavorite?: boolean;