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
10 changes: 10 additions & 0 deletions .changeset/copy-prompt-button.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@promptx/desktop": patch
---

feat: 资源编辑器预览模式添加复制提示词按钮

- 在资源编辑器的预览标签页中,将原本的"保存文件"按钮替换为"复制提示词"按钮
- 点击按钮可将预览的完整提示词内容复制到剪贴板,并显示成功提示
- 修复预览内容区域无法滚动的问题,通过在多层 flex 容器中添加 min-h-0 解决
- 新增中英文翻译:copyPrompt(复制提示词)、copySuccess(复制成功提示)
13 changes: 12 additions & 1 deletion apps/desktop/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,9 @@
"saveResourceInfo": "Save Resource Info",
"saveFile": "Save File",
"close": "Close",
"saving": "Saving..."
"saving": "Saving...",
"copyPrompt": "Copy Prompt",
"copySuccess": "Prompt copied to clipboard"
},
"messages": {
"loadFilesFailed": "Failed to load file list",
Expand All @@ -191,6 +193,15 @@
"loading": "Loading...",
"loadingFileContent": "Loading file content..."
},
"tabs": {
"editor": "Editor",
"preview": "Preview"
},
"preview": {
"loading": "Loading preview...",
"empty": "No preview content",
"failed": "Failed to load preview"
},
"readOnly": "⚠️ This resource is in read-only mode ({{source}})"
}
},
Expand Down
13 changes: 12 additions & 1 deletion apps/desktop/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,9 @@
"saveResourceInfo": "保存资源信息",
"saveFile": "保存文件",
"close": "关闭",
"saving": "保存中..."
"saving": "保存中...",
"copyPrompt": "复制提示词",
"copySuccess": "提示词已复制到剪贴板"
},
"messages": {
"loadFilesFailed": "加载文件列表失败",
Expand All @@ -191,6 +193,15 @@
"loading": "加载中...",
"loadingFileContent": "正在加载文件内容..."
},
"tabs": {
"editor": "编辑器",
"preview": "预览"
},
"preview": {
"loading": "正在加载预览...",
"empty": "暂无预览内容",
"failed": "预览加载失败"
},
"readOnly": "⚠️ 此资源为只读模式({{source}})"
}
},
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/main/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@
"mismatchToolExpectedRole": "Type mismatch: selected 'tool' but the archive contains a role resource",
"validationFailed": "Failed to validate resource type"
}
},
"preview": {
"onlyRoleSupported": "Only role resources support preview",
"resolveFailed": "Failed to resolve resource",
"failed": "Failed to generate preview",
"empty": "Preview content is empty"
}
},
"errors": {
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/main/i18n/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@
"mismatchToolExpectedRole": "类型不匹配:选择了工具资源但压缩包中是角色资源",
"validationFailed": "验证资源类型失败"
}
},
"preview": {
"onlyRoleSupported": "仅支持角色资源预览",
"resolveFailed": "资源解析失败",
"failed": "预览生成失败",
"empty": "预览内容为空"
}
},
"errors": {
Expand Down
48 changes: 48 additions & 0 deletions apps/desktop/src/main/windows/ResourceListWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,54 @@ export class ResourceListWindow {
return { success: false, message: error?.message || t('resources.importFailed') }
}
})

// 预览完整提示词(DPML -> Prompt)
ipcMain.handle('resources:previewPrompt', async (_evt, payload: {
id: string
type: 'role' | 'tool'
source: string
}) => {
try {
const { id, type, source } = payload || {}

if (!id || !type) {
return { success: false, message: t('resources.missingParams') }
}

// 只支持角色类型的预览
if (type !== 'role') {
return { success: false, message: t('resources.preview.onlyRoleSupported') }
}

// 使用 CLI 执行 action 命令(与 MCP action 工具相同的方式)
const core = await import('@promptx/core')
const coreExports = (core as any).default || core
const cli = (coreExports as any).cli || (coreExports as any).pouch?.cli

if (!cli || !cli.execute) {
return { success: false, message: 'CLI not available in @promptx/core' }
}

// 执行 action 命令获取渲染后的提示词
const result = await cli.execute('action', [id])

// result 包含渲染后的完整提示词
if (result && typeof result === 'string') {
return { success: true, prompt: result }
} else if (result && result.output) {
return { success: true, prompt: result.output }
} else if (result && result.content) {
return { success: true, prompt: result.content }
} else {
// 尝试将结果转为字符串
const promptText = String(result || '')
return { success: true, prompt: promptText || t('resources.preview.empty') }
}
} catch (error: any) {
console.error('Failed to preview prompt:', error)
return { success: false, message: error?.message || t('resources.preview.failed') }
}
})
}

show(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { useState, useEffect } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { toast } from "sonner"
import { useTranslation } from "react-i18next"
import { Eye, FileCode, Copy } from "lucide-react"

type ResourceItem = {
id: string
Expand Down Expand Up @@ -34,6 +36,11 @@ export default function ResourceEditor({ isOpen, onClose, editingItem, onResourc
const [editingDescription, setEditingDescription] = useState<string>("")
const [resourceInfoChanged, setResourceInfoChanged] = useState(false)

// 预览状态
const [activeTab, setActiveTab] = useState<"editor" | "preview">("editor")
const [previewContent, setPreviewContent] = useState<string>("")
const [previewLoading, setPreviewLoading] = useState(false)

// 当编辑项改变时,初始化状态
useEffect(() => {
if (editingItem && isOpen) {
Expand Down Expand Up @@ -155,6 +162,40 @@ export default function ResourceEditor({ isOpen, onClose, editingItem, onResourc
}
}

const loadPreview = async () => {
if (!editingItem || editingItem.type !== 'role') return

setPreviewLoading(true)
setPreviewContent("")

try {
const result = await window.electronAPI?.invoke("resources:previewPrompt", {
id: editingItem.id,
type: editingItem.type,
source: editingItem.source ?? "user"
})

if (result?.success) {
setPreviewContent(result.prompt || "")
} else {
setPreviewContent(`⚠️ ${result?.message || t("resources.editor.preview.failed")}`)
}
} catch (error: any) {
console.error("Failed to load preview:", error)
setPreviewContent(`⚠️ ${error?.message || t("resources.editor.preview.failed")}`)
} finally {
setPreviewLoading(false)
}
}

// 切换到预览标签时加载预览
const handleTabChange = (tab: string) => {
setActiveTab(tab as "editor" | "preview")
if (tab === "preview" && editingItem?.type === "role") {
loadPreview()
}
}

const handleClose = () => {
setFileList([])
setSelectedFile(null)
Expand All @@ -165,6 +206,8 @@ export default function ResourceEditor({ isOpen, onClose, editingItem, onResourc
setEditingName("")
setEditingDescription("")
setResourceInfoChanged(false)
setActiveTab("editor")
setPreviewContent("")
onClose()
}

Expand Down Expand Up @@ -224,7 +267,7 @@ export default function ResourceEditor({ isOpen, onClose, editingItem, onResourc
</div>

{/* 弹窗内容 */}
<div className="flex border-b flex-1 overflow-hidden">
<div className="flex border-b flex-1 min-h-0">
{/* 左侧文件列表 */}
<div className="w-1/3 border-r bg-gray-50 p-4 overflow-y-auto">
<h3 className="font-medium mb-3">{t("resources.editor.fileList")}</h3>
Expand Down Expand Up @@ -261,45 +304,89 @@ export default function ResourceEditor({ isOpen, onClose, editingItem, onResourc
</div>
</div>

{/* 右侧编辑器 */}
<div className="flex-1 flex flex-col">
{/* 编辑器头部 */}
<div className="p-3 border-b bg-white flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">
{selectedFile ? t("resources.editor.editFile", { file: selectedFile }) : t("resources.editor.selectFile")}
</span>
{selectedFile && (
<Button
onClick={handleSaveFile}
disabled={editorLoading || (editingItem?.source ?? "user") !== "user"}
size="sm"
className="text-white"
>
{editorLoading ? t("resources.editor.buttons.saving") : t("resources.editor.buttons.saveFile")}
</Button>
)}
</div>
{/* 右侧内容区域 */}
<div className="flex-1 flex flex-col min-h-0">
<Tabs value={activeTab} onValueChange={handleTabChange} className="flex-1 flex flex-col min-h-0 overflow-hidden">
{/* 标签页头部 */}
<div className="p-3 border-b bg-white flex items-center justify-between">
<TabsList className="h-8">
<TabsTrigger value="editor" className="text-sm px-3 py-1">
<FileCode className="h-4 w-4 mr-1" />
{t("resources.editor.tabs.editor")}
</TabsTrigger>
{editingItem?.type === "role" && (
<TabsTrigger value="preview" className="text-sm px-3 py-1">
<Eye className="h-4 w-4 mr-1" />
{t("resources.editor.tabs.preview")}
</TabsTrigger>
)}
</TabsList>

{/* 编辑器内容 */}
<div className="flex-1 p-4 overflow-hidden">
{fileContentLoading ? (
<div className="flex items-center justify-center h-full">
<p className="text-gray-500">{t("resources.editor.messages.loadingFileContent")}</p>
</div>
) : selectedFile ? (
<textarea
value={fileContent}
onChange={e => setFileContent(e.target.value)}
className="w-full h-full resize-none border rounded-md p-3 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={t("resources.editor.fileContent")}
disabled={(editingItem?.source ?? "user") !== "user"}
/>
) : (
<div className="flex items-center justify-center h-full">
<p className="text-gray-500">{t("resources.editor.selectFilePrompt")}</p>
{activeTab === "editor" && selectedFile && (
<Button
onClick={handleSaveFile}
disabled={editorLoading || (editingItem?.source ?? "user") !== "user"}
size="sm"
className="text-white"
>
{editorLoading ? t("resources.editor.buttons.saving") : t("resources.editor.buttons.saveFile")}
</Button>
)}
{activeTab === "preview" && previewContent && (
<Button
onClick={() => {
navigator.clipboard.writeText(previewContent)
toast.success(t("resources.editor.buttons.copySuccess"))
}}
size="sm"
className="text-white"
>
<Copy className="h-4 w-4 mr-1" />
{t("resources.editor.buttons.copyPrompt")}
</Button>
)}
</div>

{/* 编辑器标签页 */}
<TabsContent value="editor" className="flex-1 m-0 overflow-hidden">
<div className="h-full p-4">
{fileContentLoading ? (
<div className="flex items-center justify-center h-full">
<p className="text-gray-500">{t("resources.editor.messages.loadingFileContent")}</p>
</div>
) : selectedFile ? (
<textarea
value={fileContent}
onChange={e => setFileContent(e.target.value)}
className="w-full h-full resize-none border rounded-md p-3 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={t("resources.editor.fileContent")}
disabled={(editingItem?.source ?? "user") !== "user"}
/>
) : (
<div className="flex items-center justify-center h-full">
<p className="text-gray-500">{t("resources.editor.selectFilePrompt")}</p>
</div>
)}
</div>
)}
</div>
</TabsContent>

{/* 预览标签页 */}
<TabsContent value="preview" className="flex-1 m-0 min-h-0 overflow-y-auto p-4">
{previewLoading ? (
<div className="flex items-center justify-center h-full">
<p className="text-gray-500">{t("resources.editor.preview.loading")}</p>
</div>
) : previewContent ? (
<pre className="whitespace-pre-wrap font-mono text-sm bg-gray-50 p-4 rounded-md border">
{previewContent}
</pre>
) : (
<div className="flex items-center justify-center h-full">
<p className="text-gray-500">{t("resources.editor.preview.empty")}</p>
</div>
)}
</TabsContent>
</Tabs>
</div>
</div>

Expand Down