feat(ui): add prompt garden guide for image-to-image

This commit is contained in:
linshen committed 2026-05-10 12:58:33 +08:00
1 parent e1e9e9a8d4
commit bf675c3cdd
4 files changed
+168

No files matched your search

@@ -146,6 +146,56 @@
:disabled="isOptimizing"
/>
<div
v-if="showPromptGardenEmptyGuide"
class="prompt-garden-empty-guide"
data-testid="image-image2image-prompt-garden-guide"
>
<div class="prompt-garden-empty-guide__icon">
<NIcon>
<Plant2 />
</NIcon>
</div>
<div class="prompt-garden-empty-guide__copy">
<NText strong>
{{ t('common.promptGarden.image2ImageGuideTitle') }}
</NText>
<NText depth="3" class="prompt-garden-empty-guide__hint">
{{ t('common.promptGarden.image2ImageGuideHint') }}
</NText>
</div>
<div class="prompt-garden-empty-guide__actions">
<NButton
size="small"
secondary
:disabled="isPromptGardenGuideDisabled"
data-testid="image-image2image-prompt-garden-discover"
@click="handlePromptGardenDiscover"
>
<template #icon>
<NIcon>
<ExternalLink />
</NIcon>
</template>
{{ t('common.promptGarden.discoverShort') }}
</NButton>
<NButton
size="small"
secondary
:disabled="isPromptGardenGuideDisabled"
data-testid="image-image2image-prompt-garden-import"
@click="showPromptGardenImport = true"
>
<template #icon>
<NIcon>
<FileImport />
</NIcon>
</template>
{{ t('common.promptGarden.importShort') }}
</NButton>
</div>
</div>
<!-- 图片上传区域 - Image2Image 模式始终显示 -->
<NSpace
vertical
@@ -712,12 +762,18 @@
:renderPhase="previewRenderPhase"
/>
<PromptGardenImportDialog
v-model:show="showPromptGardenImport"
@confirm="handlePromptGardenImportConfirm"
/>
<!-- 模板管理器由 App 统一管理,这里不再渲染 -->
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, inject, ref, reactive, computed, watch, nextTick, type Ref } from 'vue'
import { useRouter, type LocationQueryRaw } from 'vue-router'
import {
NCard,
@@ -742,9 +798,11 @@ import {
NTooltip,
type UploadFileInfo,
} from "naive-ui";
import { ExternalLink, FileImport, Plant2 } from '@vicons/tabler'
import { useI18n } from "vue-i18n";
import PromptPanelUI from "../PromptPanel.vue";
import WorkspaceUtilityMenu from '../common/WorkspaceUtilityMenu.vue'
import PromptGardenImportDialog from '../common/PromptGardenImportDialog.vue'
import PromptPreviewPanel from "../PromptPreviewPanel.vue";
import SelectWithConfig from "../SelectWithConfig.vue";
import TestPanelVersionSelect from '../TestPanelVersionSelect.vue'
@@ -759,6 +817,8 @@ import { getI18nErrorMessage } from '../../utils/error'
import { withHistorySourceBindingMetadata } from '../../utils/history-source-binding'
import { resolveSourceAssetRef } from '../../utils/source-asset'
import { downloadImageSource } from '../../utils/image-download'
import { openExternalUrl } from '../../utils/open-external-url'
import type { PromptGardenImportRequest } from '../../utils/prompt-garden-import'
import { VariableAwareInput } from '../variable-extraction'
import TemporaryVariablesPanel from '../variable/TemporaryVariablesPanel.vue'
import VariableValuePreviewDialog from '../variable/VariableValuePreviewDialog.vue'
@@ -792,6 +852,7 @@ import { useWorkspaceTextModelSelection } from '../../composables/workspaces/use
import { useElementSize } from '@vueuse/core'
import { runTasksWithExecutionMode } from '../../utils/runTasksSequentially'
import {
getEnvVar,
type ContextMode,
type ImageModelConfig,
type Image2ImageRequest,
@@ -807,6 +868,7 @@ import { v4 as uuidv4 } from 'uuid'
// 国际化
const { t } = useI18n();
const router = useRouter()
interface VariantInputImageInfo {
width?: number
@@ -961,6 +1023,56 @@ const templateSelection = useWorkspaceTemplateSelection(
const selectedTemplateId = templateSelection.selectedTemplateId
const templateOptions = templateSelection.templateOptions
const showPromptGardenImport = ref(false)
const isPromptGardenEnabled = computed(() => {
const value = getEnvVar('VITE_ENABLE_PROMPT_GARDEN_IMPORT').trim().toLowerCase()
return value === '1' || value === 'true'
})
const promptGardenBaseUrl = computed(() =>
getEnvVar('VITE_PROMPT_GARDEN_BASE_URL').trim().replace(/\/$/, ''),
)
const showPromptGardenEmptyGuide = computed(() =>
isPromptGardenEnabled.value && !originalPrompt.value.trim(),
)
const isPromptGardenGuideDisabled = computed(() =>
isOptimizing.value || isIterating.value || isAnyVariantRunning.value,
)
const handlePromptGardenDiscover = () => {
void openExternalUrl(promptGardenBaseUrl.value, { logPrefix: 'PromptGarden' })
}
const handlePromptGardenImportConfirm = async (request: PromptGardenImportRequest) => {
if (!request.importCode) return false
const currentRoute = router.currentRoute.value
const query: LocationQueryRaw = {
...currentRoute.query,
importCode: request.importCode,
}
if (request.exampleId) {
query.exampleId = request.exampleId
} else {
delete query.exampleId
}
if (request.subModeKey) {
query.subModeKey = request.subModeKey
} else {
delete query.subModeKey
}
await router.push({
path: currentRoute.path,
query,
})
return true
}
const isCompareMode = computed<boolean>({
get: () => !!session.isCompareMode,
set: (value) => session.toggleCompareMode(!!value),
@@ -2509,4 +2621,54 @@ onUnmounted(() => {
min-height: 0;
overflow: auto;
}
.prompt-garden-empty-guide {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
padding: 10px 12px;
border: 1px solid color-mix(in srgb, var(--n-success-color) 10%, var(--n-border-color));
border-radius: 8px;
background: color-mix(in srgb, var(--n-success-color) 3%, var(--n-color));
}
.prompt-garden-empty-guide__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
color: color-mix(in srgb, var(--n-success-color) 76%, var(--n-text-color-3));
border-radius: 999px;
background: color-mix(in srgb, var(--n-success-color) 7%, transparent);
}
.prompt-garden-empty-guide__copy {
display: grid;
gap: 2px;
min-width: 0;
}
.prompt-garden-empty-guide__hint {
line-height: 1.45;
}
.prompt-garden-empty-guide__actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
}
@media (max-width: 720px) {
.prompt-garden-empty-guide {
grid-template-columns: auto minmax(0, 1fr);
}
.prompt-garden-empty-guide__actions {
grid-column: 1 / -1;
justify-content: flex-start;
}
}
</style>
@@ -49,6 +49,8 @@ const messages = {
"importPlaceholder": "Enter import code",
"text2ImageGuideTitle": "Discover text-to-image prompts in Prompt Garden",
"text2ImageGuideHint": "When you need a starting point, browse Garden prompts or paste an import code into this workspace.",
"image2ImageGuideTitle": "Discover image-to-image prompts in Prompt Garden",
"image2ImageGuideHint": "When you need an editing direction, browse Garden prompts or paste an import code into this workspace.",
"discoverShort": "Discover",
"importShort": "Paste code",
"importingStatus": "Importing prompt and image resources from Prompt Garden..."
@@ -47,6 +47,8 @@ const messages = {
"importPlaceholder": "输入导入码",
"text2ImageGuideTitle": "从 Prompt Garden 发现文生图提示词",
"text2ImageGuideHint": "没有灵感时,可以先去花园挑一个提示词,或粘贴导入码直接带入当前工作区。",
"image2ImageGuideTitle": "从 Prompt Garden 发现图生图提示词",
"image2ImageGuideHint": "没有改图思路时,可以先去花园挑一个图生图提示词,或粘贴导入码带入当前工作区。",
"discoverShort": "去发现",
"importShort": "粘贴导入码",
"importingStatus": "正在从 Prompt Garden 获取提示词和图片资源..."
@@ -47,6 +47,8 @@ const messages = {
"importPlaceholder": "輸入匯入碼",
"text2ImageGuideTitle": "從 Prompt Garden 發現文生圖提示詞",
"text2ImageGuideHint": "沒有靈感時,可以先去花園挑一個提示詞,或貼上匯入碼直接帶入目前工作區。",
"image2ImageGuideTitle": "從 Prompt Garden 發現圖生圖提示詞",
"image2ImageGuideHint": "沒有改圖思路時,可以先去花園挑一個圖生圖提示詞,或貼上匯入碼帶入目前工作區。",
"discoverShort": "去發現",
"importShort": "貼上匯入碼",
"importingStatus": "正在從 Prompt Garden 取得提示詞和圖片資源..."