Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions apps/desktop/src/main/database/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type { Database } from "@prompthub/db";
export { SCHEMA_TABLES, SCHEMA_INDEXES, SCHEMA } from "@prompthub/db";
export { PromptDB } from "@prompthub/db";
export { PromptRelationDB } from "@prompthub/db";
export { PromptOutputFormatDB } from "@prompthub/db";
export { FolderDB } from "@prompthub/db";
export { SkillDB } from "@prompthub/db";
export { RuleDB } from "@prompthub/db";
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/ipc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ const REBINDABLE_DB_CHANNELS = [
IPC_CHANNELS.PROMPT_RELATION_LIST,
IPC_CHANNELS.PROMPT_RELATION_UPDATE,
IPC_CHANNELS.PROMPT_RELATION_DELETE,
IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_CREATE,
IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_LIST,
IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_UPDATE,
IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_DELETE,
IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_REORDER,
IPC_CHANNELS.VERSION_GET_ALL,
IPC_CHANNELS.VERSION_CREATE,
IPC_CHANNELS.VERSION_ROLLBACK,
Expand Down
38 changes: 37 additions & 1 deletion apps/desktop/src/main/ipc/prompt.ipc.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { ipcMain } from 'electron';
import { IPC_CHANNELS } from '@prompthub/shared/constants';
import { PromptDB } from '../database/prompt';
import { PromptRelationDB } from '../database';
import { PromptOutputFormatDB, PromptRelationDB } from '../database';
import { FolderDB } from '../database/folder';
import type Database from '../database/sqlite';
import type {
CreateOutputFormatItemDTO,
CreatePromptRelationDTO,
CreatePromptDTO,
Folder,
OutputFormatItemQuery,
Prompt,
PromptRelationQuery,
PromptVersion,
SearchQuery,
UpdateOutputFormatItemDTO,
UpdatePromptRelationDTO,
UpdatePromptDTO,
} from '@prompthub/shared/types';
Expand All @@ -23,6 +26,7 @@ import { syncPromptWorkspaceFromDatabase } from "../services/prompt-workspace";
*/
export function registerPromptIPC(db: PromptDB, folderDb: FolderDB, rawDb: Database.Database): void {
const relationDb = new PromptRelationDB(rawDb);
const outputFormatDb = new PromptOutputFormatDB(rawDb);
const syncWorkspace = () => {
syncPromptWorkspaceFromDatabase(db, folderDb);
};
Expand Down Expand Up @@ -306,4 +310,36 @@ export function registerPromptIPC(db: PromptDB, folderDb: FolderDB, rawDb: Datab
}
return deleted;
});

ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_CREATE, async (_, data: CreateOutputFormatItemDTO) => {
const item = outputFormatDb.create(data);
syncWorkspace();
return item;
});

ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_LIST, async (_, query?: OutputFormatItemQuery) => {
return outputFormatDb.list(query);
});

ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_UPDATE, async (_, id: string, data: UpdateOutputFormatItemDTO) => {
const item = outputFormatDb.update(id, data);
if (item) {
syncWorkspace();
}
return item;
});

ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_DELETE, async (_, id: string) => {
const deleted = outputFormatDb.delete(id);
if (deleted) {
syncWorkspace();
}
return deleted;
});

ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_REORDER, async (_, sourcePromptId: string, itemId: string, newSortOrder: number) => {
outputFormatDb.reorder(sourcePromptId, itemId, newSortOrder);
syncWorkspace();
return true;
});
Comment on lines +313 to +344

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

IPC 处理器缺少输入校验,违反编码规范。

新增的五个输出格式 IPC 处理器均未验证输入参数。根据 apps/desktop/src/main/ipc/** 编码规范:"All IPC handlers must validate input types and reject malformed payloads before processing" 以及 "IPC handlers must catch errors and return structured error responses, never crash the main process silently"。

具体问题:

  • CREATE:未校验 data 是否为对象、sourcePromptId 是否为非空字符串、targetPromptId 是否为 string|nullsortOrder 是否为非负数。
  • REORDER:未校验 sourcePromptId/itemId 为非空字符串,未校验 newSortOrder 为有限非负数(对比同文件中 assertPromptMoveInputnewOrder 的校验)。
  • UPDATE/DELETE:未校验 id 为非空字符串。
  • 所有处理器均无 try/catch,DB 层异常直接传播为 IPC rejection。

As per coding guidelines: apps/desktop/src/main/ipc/** — "All IPC handlers must validate input types and reject malformed payloads before processing" and "IPC handlers must catch errors and return structured error responses, never crash the main process silently".

🛡️ 建议添加输入校验函数
+ const assertOutputFormatId = (id: string) => {
+   if (typeof id !== 'string' || id.trim().length === 0) {
+     throw new Error('Output format item id is required');
+   }
+ };
+
+ const assertOutputFormatCreateInput = (data: CreateOutputFormatItemDTO) => {
+   if (!data || typeof data !== 'object') {
+     throw new Error('Output format data is required');
+   }
+   if (typeof data.sourcePromptId !== 'string' || data.sourcePromptId.trim().length === 0) {
+     throw new Error('Source prompt id is required');
+   }
+   if (
+     data.targetPromptId !== null &&
+     (typeof data.targetPromptId !== 'string' || data.targetPromptId.trim().length === 0)
+   ) {
+     throw new Error('Target prompt id must be null or a non-empty string');
+   }
+   if (
+     data.sortOrder !== undefined &&
+     (!Number.isFinite(data.sortOrder) || data.sortOrder < 0)
+   ) {
+     throw new Error('Sort order must be a non-negative number');
+   }
+ };
+
+ const assertOutputFormatReorderInput = (
+   sourcePromptId: string,
+   itemId: string,
+   newSortOrder: number,
+ ) => {
+   if (typeof sourcePromptId !== 'string' || sourcePromptId.trim().length === 0) {
+     throw new Error('Source prompt id is required');
+   }
+   if (typeof itemId !== 'string' || itemId.trim().length === 0) {
+     throw new Error('Item id is required');
+   }
+   if (!Number.isFinite(newSortOrder) || newSortOrder < 0) {
+     throw new Error('Sort order must be a non-negative number');
+   }
+ };

  ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_CREATE, async (_, data: CreateOutputFormatItemDTO) => {
+   assertOutputFormatCreateInput(data);
    const item = outputFormatDb.create(data);
    syncWorkspace();
    return item;
  });

  ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_LIST, async (_, query?: OutputFormatItemQuery) => {
+   if (query !== undefined && (typeof query !== 'object' || query === null)) {
+     throw new Error('Query must be an object or undefined');
+   }
    return outputFormatDb.list(query);
  });

  ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_UPDATE, async (_, id: string, data: UpdateOutputFormatItemDTO) => {
+   assertOutputFormatId(id);
+   if (!data || typeof data !== 'object') {
+     throw new Error('Update data is required');
+   }
+   if (
+     data.sortOrder !== undefined &&
+     (!Number.isFinite(data.sortOrder) || data.sortOrder < 0)
+   ) {
+     throw new Error('Sort order must be a non-negative number');
+   }
    const item = outputFormatDb.update(id, data);
    if (item) {
      syncWorkspace();
    }
    return item;
  });

  ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_DELETE, async (_, id: string) => {
+   assertOutputFormatId(id);
    const deleted = outputFormatDb.delete(id);
    if (deleted) {
      syncWorkspace();
    }
    return deleted;
  });

  ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_REORDER, async (_, sourcePromptId: string, itemId: string, newSortOrder: number) => {
+   assertOutputFormatReorderInput(sourcePromptId, itemId, newSortOrder);
    outputFormatDb.reorder(sourcePromptId, itemId, newSortOrder);
    syncWorkspace();
    return true;
  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_CREATE, async (_, data: CreateOutputFormatItemDTO) => {
const item = outputFormatDb.create(data);
syncWorkspace();
return item;
});
ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_LIST, async (_, query?: OutputFormatItemQuery) => {
return outputFormatDb.list(query);
});
ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_UPDATE, async (_, id: string, data: UpdateOutputFormatItemDTO) => {
const item = outputFormatDb.update(id, data);
if (item) {
syncWorkspace();
}
return item;
});
ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_DELETE, async (_, id: string) => {
const deleted = outputFormatDb.delete(id);
if (deleted) {
syncWorkspace();
}
return deleted;
});
ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_REORDER, async (_, sourcePromptId: string, itemId: string, newSortOrder: number) => {
outputFormatDb.reorder(sourcePromptId, itemId, newSortOrder);
syncWorkspace();
return true;
});
const assertOutputFormatId = (id: string) => {
if (typeof id !== 'string' || id.trim().length === 0) {
throw new Error('Output format item id is required');
}
};
const assertOutputFormatCreateInput = (data: CreateOutputFormatItemDTO) => {
if (!data || typeof data !== 'object') {
throw new Error('Output format data is required');
}
if (typeof data.sourcePromptId !== 'string' || data.sourcePromptId.trim().length === 0) {
throw new Error('Source prompt id is required');
}
if (
data.targetPromptId !== null &&
(typeof data.targetPromptId !== 'string' || data.targetPromptId.trim().length === 0)
) {
throw new Error('Target prompt id must be null or a non-empty string');
}
if (
data.sortOrder !== undefined &&
(!Number.isFinite(data.sortOrder) || data.sortOrder < 0)
) {
throw new Error('Sort order must be a non-negative number');
}
};
const assertOutputFormatReorderInput = (
sourcePromptId: string,
itemId: string,
newSortOrder: number,
) => {
if (typeof sourcePromptId !== 'string' || sourcePromptId.trim().length === 0) {
throw new Error('Source prompt id is required');
}
if (typeof itemId !== 'string' || itemId.trim().length === 0) {
throw new Error('Item id is required');
}
if (!Number.isFinite(newSortOrder) || newSortOrder < 0) {
throw new Error('Sort order must be a non-negative number');
}
};
ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_CREATE, async (_, data: CreateOutputFormatItemDTO) => {
assertOutputFormatCreateInput(data);
const item = outputFormatDb.create(data);
syncWorkspace();
return item;
});
ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_LIST, async (_, query?: OutputFormatItemQuery) => {
if (query !== undefined && (typeof query !== 'object' || query === null)) {
throw new Error('Query must be an object or undefined');
}
return outputFormatDb.list(query);
});
ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_UPDATE, async (_, id: string, data: UpdateOutputFormatItemDTO) => {
assertOutputFormatId(id);
if (!data || typeof data !== 'object') {
throw new Error('Update data is required');
}
if (
data.sortOrder !== undefined &&
(!Number.isFinite(data.sortOrder) || data.sortOrder < 0)
) {
throw new Error('Sort order must be a non-negative number');
}
const item = outputFormatDb.update(id, data);
if (item) {
syncWorkspace();
}
return item;
});
ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_DELETE, async (_, id: string) => {
assertOutputFormatId(id);
const deleted = outputFormatDb.delete(id);
if (deleted) {
syncWorkspace();
}
return deleted;
});
ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_REORDER, async (_, sourcePromptId: string, itemId: string, newSortOrder: number) => {
assertOutputFormatReorderInput(sourcePromptId, itemId, newSortOrder);
outputFormatDb.reorder(sourcePromptId, itemId, newSortOrder);
syncWorkspace();
return true;
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/ipc/prompt.ipc.ts` around lines 313 - 344, The new
output-format IPC handlers in prompt.ipc.ts need upfront validation and error
handling before touching outputFormatDb. Add input checks in the
PROMPT_OUTPUT_FORMAT_CREATE/UPDATE/DELETE/REORDER handlers (and reuse the same
style as assertPromptMoveInput) to verify ids are non-empty strings, payloads
are objects, and sort orders are finite non-negative numbers, then reject
malformed requests early. Wrap each ipcMain.handle callback in try/catch so DB
exceptions are converted into structured error responses instead of propagating
silently; keep the syncWorkspace calls only after successful mutations.

Source: Coding guidelines

}
13 changes: 13 additions & 0 deletions apps/desktop/src/preload/api/prompt.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { ipcRenderer } from "electron";
import { IPC_CHANNELS } from "@prompthub/shared/constants/ipc-channels";
import type {
CreateOutputFormatItemDTO,
CreatePromptRelationDTO,
CreatePromptDTO,
Folder,
OutputFormatItemQuery,
Prompt,
PromptRelationQuery,
PromptVersion,
SearchQuery,
UpdateOutputFormatItemDTO,
UpdatePromptRelationDTO,
UpdatePromptDTO,
} from "@prompthub/shared/types";
Expand Down Expand Up @@ -53,4 +56,14 @@ export const promptApi = {
ipcRenderer.invoke(IPC_CHANNELS.PROMPT_RELATION_UPDATE, id, data),
deleteRelation: (id: string) =>
ipcRenderer.invoke(IPC_CHANNELS.PROMPT_RELATION_DELETE, id),
createOutputFormat: (data: CreateOutputFormatItemDTO) =>
ipcRenderer.invoke(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_CREATE, data),
listOutputFormat: (query?: OutputFormatItemQuery) =>
ipcRenderer.invoke(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_LIST, query),
updateOutputFormat: (id: string, data: UpdateOutputFormatItemDTO) =>
ipcRenderer.invoke(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_UPDATE, id, data),
deleteOutputFormat: (id: string) =>
ipcRenderer.invoke(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_DELETE, id),
reorderOutputFormat: (sourcePromptId: string, itemId: string, newSortOrder: number) =>
ipcRenderer.invoke(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_REORDER, sourcePromptId, itemId, newSortOrder),
};
145 changes: 142 additions & 3 deletions apps/desktop/src/renderer/components/layout/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
import { resolvePromptMarkdownHref } from '../prompt/prompt-markdown-url';
import { PromptQuickRewriteTrigger } from '../prompt/PromptQuickRewriteTrigger';
import { PromptRelationshipPanel } from '../prompt/PromptRelationshipPanel';
import { PromptOutputFormatPanel } from '../prompt/PromptOutputFormatPanel';
import { PromptAiResponsePanel } from '../prompt/PromptAiResponsePanel';
import { PromptDetailMetadata } from '../prompt/PromptDetailMetadata';
import { PromptViewContainers } from '../prompt/PromptViewContainers';
Expand Down Expand Up @@ -699,10 +700,14 @@ function PromptSkillMainContent() {
const selectedIds = usePromptStore((state) => state.selectedIds);
const lastSelectedId = usePromptStore((state) => state.lastSelectedId);
const relations = usePromptStore((state) => state.relations ?? []);
const outputFormatItems = usePromptStore((state) => state.outputFormatItems ?? []);
const selectPrompt = usePromptStore((state) => state.selectPrompt);
const setSelectedIds = usePromptStore((state) => state.setSelectedIds);
const createPrompt = usePromptStore((state) => state.createPrompt);
const createRelation = usePromptStore((state) => state.createRelation);
const createOutputFormatItem = usePromptStore((state) => state.createOutputFormatItem);
const deleteOutputFormatItem = usePromptStore((state) => state.deleteOutputFormatItem);
const reorderOutputFormatItem = usePromptStore((state) => state.reorderOutputFormatItem);
const toggleFavorite = usePromptStore((state) => state.toggleFavorite);
const togglePinned = usePromptStore((state) => state.togglePinned);
const deletePrompt = usePromptStore((state) => state.deletePrompt);
Expand Down Expand Up @@ -735,6 +740,10 @@ function PromptSkillMainContent() {
// 用于列表/画廊视图复制时的变量弹窗
const [isCopyVariableModalOpen, setIsCopyVariableModalOpen] = useState(false);
const [copyPrompt, setCopyPrompt] = useState<Prompt | null>(null);
const [copyPromptQueue, setCopyPromptQueue] = useState<Prompt[]>([]);
const [copyPromptResults, setCopyPromptResults] = useState<string[]>([]);
const [copyPromptQueueIndex, setCopyPromptQueueIndex] = useState(-1);
const [copyPromptSourceId, setCopyPromptSourceId] = useState<string | null>(null);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; prompt: Prompt } | null>(null);
const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; prompt: Prompt | null }>({ isOpen: false, prompt: null });
Expand Down Expand Up @@ -1480,6 +1489,9 @@ function PromptSkillMainContent() {
selectedPromptRelations.length +
(selectedParentPrompt ? 1 : 0) +
selectedChildPrompts.length;
const selectedOutputFormatCount = selectedPrompt
? outputFormatItems.filter((item) => item.sourcePromptId === selectedPrompt.id).length
: 0;

// Auto-select prompt language based on UI language (if English version exists)
// 根据界面语言自动选择 Prompt 语言(如果有英文版本)
Expand All @@ -1503,6 +1515,7 @@ function PromptSkillMainContent() {
const [isDetailInlineEditing, setIsDetailInlineEditing] = useState(false);
const [isDetailInlineSaving, setIsDetailInlineSaving] = useState(false);
const [isDetailRelationshipsOpen, setIsDetailRelationshipsOpen] = useState(false);
const [isDetailOutputFormatOpen, setIsDetailOutputFormatOpen] = useState(false);
const [detailInlineActiveField, setDetailInlineActiveField] =
useState<DetailInlineEditField>('title');
const [detailInlineDraft, setDetailInlineDraft] = useState<DetailInlineEditDraft>({
Expand Down Expand Up @@ -1550,6 +1563,7 @@ function PromptSkillMainContent() {
setIsDetailInlineEditing(false);
setIsDetailInlineSaving(false);
setIsDetailRelationshipsOpen(false);
setIsDetailOutputFormatOpen(false);
setDetailInlineActiveField('title');
setInlineAiTestImages([]);
}, [selectedPrompt?.id]);
Expand Down Expand Up @@ -1729,22 +1743,111 @@ function PromptSkillMainContent() {
[cancelDetailInlineEdit, saveDetailInlineEdit],
);

const getOutputFormatPromptQueue = useCallback(
(prompt: Prompt): Prompt[] => {
const configuredItems = outputFormatItems
.filter((item) => item.sourcePromptId === prompt.id)
.sort((left, right) => {
const byOrder = left.sortOrder - right.sortOrder;
if (byOrder !== 0) return byOrder;
return left.createdAt.localeCompare(right.createdAt);
});

if (configuredItems.length === 0) {
return [prompt];
}

const queue = configuredItems
.map((item) => {
if (!item.targetPromptId) {
return prompt;
}
return promptById.get(item.targetPromptId) ?? null;
})
.filter((item): item is Prompt => item !== null);

return queue.length > 0 ? queue : [prompt];
},
[outputFormatItems, promptById],
);

// Handle copying prompt - check for variables first
// 处理复制 Prompt - 先检查是否有变量
const handleCopyPrompt = async (prompt: Prompt) => {
const resolvedPrompt = resolvePromptContentByLanguage(prompt, showEnglish);
const queue = getOutputFormatPromptQueue(prompt);

if (queue.length > 1) {
setCopyPromptQueue(queue);
setCopyPromptResults(new Array(queue.length).fill(""));
setCopyPromptQueueIndex(0);
setCopyPromptSourceId(prompt.id);
return;
}

const resolvedPrompt = resolvePromptContentByLanguage(queue[0], showEnglish);

if (hasUserDefinedPromptVariables(undefined, resolvedPrompt.userPrompt)) {
// 有变量,打开弹窗让用户填写
setCopyPrompt(prompt);
setCopyPrompt(queue[0]);
setIsCopyVariableModalOpen(true);
} else {
await copyTextToClipboard(buildPromptCopyText(resolvedPrompt));
await incrementUsageCount(prompt.id);
await incrementUsageCount(queue[0].id);
showToast(t('toast.copied'), 'success', showCopyNotification);
}
};

useEffect(() => {
if (copyPromptQueue.length === 0 || copyPromptQueueIndex < 0) {
return;
}

if (copyPromptQueueIndex >= copyPromptQueue.length) {
const finalText = copyPromptResults.filter((text) => text.trim()).join("\n\n");
void copyTextToClipboard(finalText).then(async () => {
if (copyPromptSourceId) {
await incrementUsageCount(copyPromptSourceId);
}
triggerCopied();
showToast(t('toast.copied'), 'success', showCopyNotification);
}).finally(() => {
setIsCopyVariableModalOpen(false);
setCopyPrompt(null);
setCopyPromptQueue([]);
setCopyPromptResults([]);
setCopyPromptQueueIndex(-1);
setCopyPromptSourceId(null);
});
return;
}

const currentPrompt = copyPromptQueue[copyPromptQueueIndex];
const resolvedPrompt = resolvePromptContentByLanguage(currentPrompt, showEnglish);
if (hasUserDefinedPromptVariables(undefined, resolvedPrompt.userPrompt)) {
setCopyPrompt(currentPrompt);
setIsCopyVariableModalOpen(true);
return;
}

setCopyPromptResults((currentResults) => {
const nextResults = [...currentResults];
nextResults[copyPromptQueueIndex] = buildPromptCopyText(resolvedPrompt);
return nextResults;
});
setCopyPromptQueueIndex((currentIndex) => currentIndex + 1);
}, [
copyPromptQueue,
copyPromptQueueIndex,
copyPromptResults,
copyPromptSourceId,
incrementUsageCount,
showCopyNotification,
showEnglish,
showToast,
t,
triggerCopied,
]);

const handleDuplicatePrompt = useCallback(async (prompt: Prompt) => {
const duplicatedPrompt = await createPrompt({
title: `${prompt.title} (${t('prompt.duplicateSuffix', 'Duplicate')})`,
Expand Down Expand Up @@ -2530,8 +2633,11 @@ function PromptSkillMainContent() {
childPrompts={selectedChildPrompts}
folderOptions={detailFolderOptions}
relationshipCount={selectedRelationshipCount}
outputFormatCount={selectedOutputFormatCount}
isRelatedPromptsOpen={isDetailRelationshipsOpen}
isOutputFormatOpen={isDetailOutputFormatOpen}
isRelatedPromptsDisabled={isDetailInlineEditing}
isOutputFormatDisabled={isDetailInlineEditing}
t={t}
onMoveToFolder={(prompt, folderId) => {
void handleMovePrompt(prompt, folderId);
Expand All @@ -2540,6 +2646,9 @@ function PromptSkillMainContent() {
onToggleRelatedPrompts={() =>
setIsDetailRelationshipsOpen((open) => !open)
}
onToggleOutputFormat={() =>
setIsDetailOutputFormatOpen((open) => !open)
}
/>

{isDetailRelationshipsOpen && (
Expand All @@ -2556,6 +2665,20 @@ function PromptSkillMainContent() {
/>
)}

{isDetailOutputFormatOpen && (
<PromptOutputFormatPanel
currentPrompt={selectedPrompt}
prompts={prompts}
outputFormatItems={outputFormatItems}
onCreateOutputFormatItem={createOutputFormatItem}
onDeleteOutputFormatItem={deleteOutputFormatItem}
onReorderOutputFormatItem={reorderOutputFormatItem}
onSelectPrompt={selectPrompt}
disabled={isDetailInlineEditing}
className="mb-4"
/>
)}

{/* Images */}
{/* 图片 */}
{selectedPrompt.images && selectedPrompt.images.length > 0 && (
Expand Down Expand Up @@ -3036,12 +3159,28 @@ function PromptSkillMainContent() {
onClose={() => {
setIsCopyVariableModalOpen(false);
setCopyPrompt(null);
setCopyPromptQueue([]);
setCopyPromptResults([]);
setCopyPromptQueueIndex(-1);
setCopyPromptSourceId(null);
}}
promptId={copyPrompt.id}
systemPrompt={undefined}
userPrompt={resolvePromptContentByLanguage(copyPrompt, showEnglish).userPrompt}
mode="copy"
onCopy={async (text) => {
if (copyPromptQueue.length > 0 && copyPromptQueueIndex >= 0) {
setCopyPromptResults((currentResults) => {
const nextResults = [...currentResults];
nextResults[copyPromptQueueIndex] = text;
return nextResults;
});
setIsCopyVariableModalOpen(false);
setCopyPrompt(null);
setCopyPromptQueueIndex((currentIndex) => currentIndex + 1);
return;
}

await copyTextToClipboard(text);
await incrementUsageCount(copyPrompt.id);
triggerCopied();
Expand Down
Loading
Loading