diff --git a/CHANGELOG.md b/CHANGELOG.md index 38e95f8cd..a2fa07ab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ versioning follows [Semantic Versioning](https://semver.org/). ## [Unreleased] +### ✨ New Features + +- **Prompt library: enqueue to queue** — Add a queue action on prompt cards and an optional "split by line" toggle so multi-line prompts can be queued as separate items. Closes #547. + --- ## [1.0.53] - 2026-05-25 diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 0b4ebd2d1..1023a78b8 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -8,6 +8,10 @@ ## [Unreleased] +### ✨ 新功能 + +- **提示词库:加入队列** — 在提示词卡片上新增加入队列操作,并提供可选的「按行拆分」开关,可将多行提示词拆分为多条队列项。Closes #547。 + --- ## [1.0.53] - 2026-05-25 diff --git a/src/components/App.tsx b/src/components/App.tsx index 813268cf8..7e95301f0 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -13,6 +13,7 @@ import { ConversationManager } from "~core/conversation-manager" import { InlineBookmarkManager } from "~core/inline-bookmark-manager" import { OutlineManager, type OutlineNode } from "~core/outline-manager" import { AI_STUDIO_SHORTCUT_SYNC_EVENT, PromptManager } from "~core/prompt-manager" +import { QUEUE_ITEMS_ADDED_EVENT } from "~core/queue-inject" import { QueueDispatcher } from "~core/queue-dispatcher" import { ensureGlobalThemeManager, type ThemeTransitionOrigin } from "~core/theme-manager" import { useEdgePeekController } from "~hooks/useEdgePeekController" @@ -829,6 +830,22 @@ export const App = () => { return () => queueDispatcher.stop() }, [queueDispatcher, settings?.features?.prompts?.promptQueue]) + useEffect(() => { + if (!queueDispatcher || !adapter) return + + const handleQueueItemsAdded = () => { + if (!queueDispatcher.isRunning()) { + queueDispatcher.start() + } + if (!adapter.isGenerating()) { + void queueDispatcher.processNextNow() + } + } + + window.addEventListener(QUEUE_ITEMS_ADDED_EVENT, handleQueueItemsAdded) + return () => window.removeEventListener(QUEUE_ITEMS_ADDED_EVENT, handleQueueItemsAdded) + }, [adapter, queueDispatcher]) + const conversationManager = useMemo(() => { return adapter ? new ConversationManager(adapter) : null }, [adapter]) diff --git a/src/components/PromptsTab.tsx b/src/components/PromptsTab.tsx index a5fdbc3d7..67777f6fd 100644 --- a/src/components/PromptsTab.tsx +++ b/src/components/PromptsTab.tsx @@ -11,6 +11,7 @@ import { EyeIcon, ImportIcon, PinIcon, + PromptQueueIcon, SettingsIcon, TimeIcon, } from "~components/icons" @@ -23,6 +24,7 @@ import { } from "~components/VariableInputDialog" import { VIRTUAL_CATEGORY } from "~constants" import type { PromptManager } from "~core/prompt-manager" +import { injectContentToQueue } from "~core/queue-inject" import { useSettingsStore } from "~stores/settings-store" import { APP_NAME } from "~utils/config" import { t } from "~utils/i18n" @@ -85,7 +87,13 @@ export const PromptsTab: React.FC = ({ const submitShortcut = useSettingsStore( (state) => state.settings.features?.prompts?.submitShortcut ?? "enter", ) - + const promptQueueEnabled = useSettingsStore( + (state) => state.settings.features?.prompts?.promptQueue ?? false, + ) + const queueInjectSplitByLine = useSettingsStore( + (state) => state.settings.features?.prompts?.queueInjectSplitByLine ?? false, + ) + const updateDeepSetting = useSettingsStore((state) => state.updateDeepSetting) const [prompts, setPrompts] = useState([]) const [categories, setCategories] = useState([]) const [selectedCategory, setSelectedCategory] = useState(VIRTUAL_CATEGORY.ALL) @@ -123,7 +131,14 @@ export const PromptsTab: React.FC = ({ prompt: Prompt | null variables: ParsedVariable[] submitAfterInsert: boolean - }>({ show: false, prompt: null, variables: [], submitAfterInsert: false }) + enqueueAfterResolve: boolean + }>({ + show: false, + prompt: null, + variables: [], + submitAfterInsert: false, + enqueueAfterResolve: false, + }) // 导入确认弹窗状态 const [importDialogState, setImportDialogState] = useState<{ @@ -184,6 +199,7 @@ export const PromptsTab: React.FC = ({ prompt: targetPrompt, variables, submitAfterInsert, + enqueueAfterResolve: false, }) return true }, @@ -366,6 +382,7 @@ export const PromptsTab: React.FC = ({ prompt, variables, submitAfterInsert, + enqueueAfterResolve: false, }) } else { // No variables; insert (and optionally submit) directly @@ -373,6 +390,54 @@ export const PromptsTab: React.FC = ({ } } + const doEnqueue = (content: string) => { + const result = injectContentToQueue(content, { splitByLine: queueInjectSplitByLine }) + if (result.ok === false) { + if (result.reason === "disabled") { + showToast( + `${t("promptEnqueueDisabled") || "Enable Prompt Queue in settings"}. ${t("queueDisableHint") || ""}`, + 3000, + ) + return + } + showToast(t("promptEnqueueEmpty") || "Nothing to queue", 2500) + return + } + showToast( + t("promptEnqueueSuccess", { count: String(result.count) }) || + `Queued ${result.count} prompt(s)`, + 2500, + ) + } + + const handleEnqueueToQueue = (prompt: Prompt, e: React.MouseEvent) => { + e.stopPropagation() + e.preventDefault() + + if (!promptQueueEnabled) { + showToast( + `${t("promptEnqueueDisabled") || "Enable Prompt Queue in settings"}. ${t("queueDisableHint") || ""}`, + 3000, + ) + return + } + + const variables = extractVariables(prompt.content) + if (variables.length > 0) { + setVariableDialogState({ + show: true, + prompt, + variables, + submitAfterInsert: false, + enqueueAfterResolve: true, + }) + return + } + + doEnqueue(prompt.content) + manager.updateLastUsed(prompt.id) + } + const doInsert = async (prompt: Prompt, content: string, submitAfterInsert = false) => { const success = await manager.insertPrompt(content) if (success) { @@ -404,11 +469,17 @@ export const PromptsTab: React.FC = ({ } const handleVariableConfirm = async (values: Record) => { - const { prompt, submitAfterInsert } = variableDialogState + const { prompt, submitAfterInsert, enqueueAfterResolve } = variableDialogState if (!prompt) return const replacedContent = replaceVariables(prompt.content, values) - setVariableDialogState({ show: false, prompt: null, variables: [], submitAfterInsert: false }) + setVariableDialogState({ + show: false, + prompt: null, + variables: [], + submitAfterInsert: false, + enqueueAfterResolve: false, + }) await new Promise((resolve) => { window.requestAnimationFrame(() => { @@ -416,6 +487,12 @@ export const PromptsTab: React.FC = ({ }) }) + if (enqueueAfterResolve) { + doEnqueue(replacedContent) + manager.updateLastUsed(prompt.id) + return + } + await doInsert(prompt, replacedContent, submitAfterInsert) } @@ -634,6 +711,7 @@ export const PromptsTab: React.FC = ({ prompt: null, variables: [], submitAfterInsert: false, + enqueueAfterResolve: false, }) }, []) @@ -1547,6 +1625,41 @@ export const PromptsTab: React.FC = ({ + {promptQueueEnabled && ( + + + + )} {/* 分类标签栏:左侧可滚动 + 右侧固定 */} @@ -1824,6 +1937,15 @@ export const PromptsTab: React.FC = ({ + {promptQueueEnabled && ( + + + + )}