From 0179724467f6dae01150901029ccd0060b33b5d7 Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Sat, 25 Jul 2026 11:55:00 +0800 Subject: [PATCH] feat(prompts): add atlascloud logo SVG and update prompt source handling --- CHANGELOG.md | 10 +- README.md | 13 ++ assets/atlascloud.svg | 1 + docs/content/docs/progress/pending-test.mdx | 4 +- .../components/canvas/canvas-side-panel.tsx | 70 +++-------- .../layout/config-prompt-sources.tsx | 2 +- .../prompts/prompt-select-dialog.tsx | 2 +- web/src/components/prompts/use-prompt-list.ts | 13 +- .../components/my-prompt-editor-dialog.tsx | 108 ---------------- .../components/prompt-detail-dialog.tsx | 9 +- web/src/pages/prompts/index.tsx | 118 ++++-------------- web/src/services/api/prompts.ts | 23 +--- web/src/stores/use-prompt-store.ts | 53 -------- 13 files changed, 80 insertions(+), 346 deletions(-) create mode 100644 assets/atlascloud.svg delete mode 100644 web/src/pages/prompts/components/my-prompt-editor-dialog.tsx delete mode 100644 web/src/stores/use-prompt-store.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f146be..f1f110c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,16 @@ ## Unreleased -+ [新增] 提示词来源新增 Banana Prompt Quicker,并支持添加自定义标准 JSON 来源。 -+ [新增] 提示词中心新增「我的提示词」,支持本地管理、从公共库保存,并可在画布提示词库中直接使用。 -+ [新增] Agent 新增统一生成任务状态查询,支持查看画布、生图工作台和视频工作台任务进度。 -+ [调整] 内置提示词来源改为读取 Image Prompts 统一 JSON 数据,不再由画布端分别解析。 +## v0.10.0 - 2026-07-25 + ++ [新增] 提示词来源新增BananaPromptQuicker,并支持添加自定义标准 JSON 来源。 ++ [新增] Agent 新增统一状态查询,支持查看画布、生图工作台和视频工作台任务进度。 ++ [调整] 内置提示词来源改为读取ImagePrompts 统一JSON 数据,不再由画布端分别解析。 + [调整] 提示词来源配置保留卡片式交互,并展示来源数量、同步状态和上次成功时间。 + [优化] 提示词来源支持独立缓存和更新,更新失败时保留上一次成功内容。 + [优化] Agent 对话区分用户与 AI 消息,并优化会话切换和回到最新消息的交互。 + [优化] 画布生成完成后保留提示词输入,并改善提示词库和资产面板的交互性能。 ++ [优化] 提示词中心标题居中、搜索输入增加防抖,并采用左侧筛选、右侧内容的双栏布局。 + [修复] 本地 Agent 完善多标签页请求隔离、结果归属、焦点回退和 Codex 会话状态同步。 + [修复] 本地 Agent 上传的图片附件可正确创建画布图片节点并连接生成流程。 + [修复] 画布提示词库支持跨来源搜索,插入节点时保留提示词标题。 diff --git a/README.md b/README.md index 96a3f93..6f890b7 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,19 @@ > > 如果你需要稳定维护自己的分支,建议自行 fork 后独立开发。二次开发与 PR 请保留原作者信息和前端页面标识。 +## 赞助商 + + + + + + +
+ Atlas Cloud + + Atlas Cloud is a full-modal AI inference platform that gives developers a single AI API to access video generation, image generation, and LLM APIs. Instead of managing multiple vendor integrations, you connect once and get unified access to 300+ curated models across all modalities. Check out Atlas Cloud's new coding plan promotion for more budget-friendly API access. +
+ ## 核心功能 - 无限画布:多画布项目、节点拖拽缩放、连线、小地图、撤销重做、导入导出。 diff --git a/assets/atlascloud.svg b/assets/atlascloud.svg new file mode 100644 index 0000000..da8d740 --- /dev/null +++ b/assets/atlascloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 4725a19..68ad22a 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,10 +5,10 @@ description: 当前版本已实现但仍需人工验证的变更项 # 待测试 +- 提示词中心布局:页面标题及提示词总数应居中;连续输入搜索文字时应在停止输入约 300ms 后再查询;桌面端分类与标签应在左侧独立滚动,右侧搜索框下直接展示提示词卡片;标签数量较多时不能继续向下挤压提示词,窄屏下应恢复上下排列且内容不溢出;不再显示「我的提示词」Tab,收藏提示词应直接加入我的资产。 - 提示词来源:6 个内置来源应从 Image Prompts 统一仓库读取,更新后数量依次为 323、494、53、76、126、129;提示词仍按 6 个来源分组并可独立启用,来源内可继续按 `tags` 筛选。添加标准 JSON URL 后应能查看内容,填写非数组 JSON 或不可访问地址时应显示失败,并继续保留该来源上一次成功缓存的内容。 - 提示词来源界面:来源应以卡片列表展示,启用开关位于左侧,数量、同步状态和上次成功时间作为次级信息显示,查看、拉取及自定义来源编辑/删除操作使用带文字按钮;底部定时拉取区域应保持独立边框布局。 -- 我的提示词:在提示词中心切换到「我的提示词」,验证新增、编辑、删除和搜索;从提示词库保存后应出现在个人列表、画布左侧提示词面板和提示词选择弹窗中,刷新页面后数据仍保留在本地,Agent 搜索提示词时也应能查到。 -- 画布提示词库:不展开任何公共来源直接搜索其中的提示词,匹配项应自动显示;点击「插入画布」后应创建正文正确且标题保持为提示词标题的文本节点。 +- 画布提示词库:不应再显示「我的提示词」分组;不展开任何公共来源直接搜索其中的提示词,匹配项应自动显示;点击「插入画布」后应创建正文正确且标题保持为提示词标题的文本节点。 - 全站 Agent:新增 `generation_get_status` 工具,画布生成节点可按 `nodeIds` 查询,生图和视频工作台提交后会返回 `taskId` 并可查询排队、运行、成功或失败状态;需验证查询只由当前活动标签页返回。 - 本地 Agent 多标签页隔离:同时打开两个不同画布并连接同一个 Agent,分别聚焦标签页后通过 MCP 读取和修改画布,操作应只落在当前聚焦页面;网页面板发起的整个 Codex turn 应固定操作发起页面,即使中途聚焦另一标签页也不能切换目标;关闭当前页面后应回退到最近聚焦且仍连接的页面,其他页面回传同一请求结果应被拒绝。 - 本地 Agent 多标签页会话同步:所有标签页共享同一个站点级 Codex 活跃线程;任一页面发送消息、新建、恢复或删除会话后,其他页面应同步活跃线程和聊天记录;Agent 输出仅显示在事件所属线程,运行中不能新建、恢复、删除或再次发送任务。 diff --git a/web/src/components/canvas/canvas-side-panel.tsx b/web/src/components/canvas/canvas-side-panel.tsx index 2e8ac93..7ce0857 100644 --- a/web/src/components/canvas/canvas-side-panel.tsx +++ b/web/src/components/canvas/canvas-side-panel.tsx @@ -9,12 +9,11 @@ import { exportCanvasNodes } from "@/lib/canvas/canvas-export"; import { getNodeDefinition } from "@/lib/canvas/node-registry"; import { cn } from "@/lib/utils"; import { PromptDetailDialog } from "@/pages/prompts/components/prompt-detail-dialog"; -import { fetchSourcePrompts, personalPromptToPrompt, type Prompt } from "@/services/api/prompts"; +import { fetchSourcePrompts, type Prompt } from "@/services/api/prompts"; import { uploadMediaFile } from "@/services/file-storage"; import { uploadImage } from "@/services/image-storage"; import { useAssetStore, type Asset, type AssetKind } from "@/stores/use-asset-store"; import { usePromptSourceStore } from "@/stores/use-prompt-source-store"; -import { usePromptStore } from "@/stores/use-prompt-store"; import { CANVAS_SIDE_PANEL_MAX_WIDTH, CANVAS_SIDE_PANEL_MIN_WIDTH, CANVAS_SIDE_PANEL_MOTION_MS, useCanvasSidePanelStore } from "@/stores/use-canvas-side-panel-store"; import { useThemeStore } from "@/stores/use-theme-store"; import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas"; @@ -444,10 +443,9 @@ function AssetCover({ asset }: { asset: Asset }) { const CanvasPromptsTab = memo(function CanvasPromptsTab({ onInsert, theme }: { onInsert: (payload: InsertAssetPayload) => void; theme: CanvasTheme }) { const { message } = App.useApp(); const sources = usePromptSourceStore((state) => state.sources); - const personalPrompts = usePromptStore((state) => state.prompts); const enabledSources = useMemo(() => sources.filter((source) => source.enabled), [sources]); const [keyword, setKeyword] = useState(""); - const [expanded, setExpanded] = useState>({ personal: true }); + const [expanded, setExpanded] = useState>({}); const [detail, setDetail] = useState(null); const copyPrompt = async (prompt: string) => { @@ -466,32 +464,19 @@ const CanvasPromptsTab = memo(function CanvasPromptsTab({ onInsert, theme }: { o
- setExpanded((prev) => ({ ...prev, personal: !prev.personal }))} - onInsert={onInsert} - onView={setDetail} - /> - {enabledSources.length ? ( - <> - {enabledSources.map((source) => ( - setExpanded((prev) => ({ ...prev, [source.id]: !prev[source.id] }))} - onInsert={onInsert} - onView={setDetail} - /> - ))} - - ) : personalPrompts.length === 0 ? : null} + {enabledSources.length ? enabledSources.map((source) => ( + setExpanded((prev) => ({ ...prev, [source.id]: !prev[source.id] }))} + onInsert={onInsert} + onView={setDetail} + /> + )) : }
setDetail(null)} onCopy={(prompt) => void copyPrompt(prompt)} /> @@ -499,31 +484,6 @@ const CanvasPromptsTab = memo(function CanvasPromptsTab({ onInsert, theme }: { o ); }); -function PersonalPromptGroup({ items, keyword, open, theme, onToggle, onInsert, onView }: { items: Prompt[]; keyword: string; open: boolean; theme: CanvasTheme; onToggle: () => void; onInsert: (payload: InsertAssetPayload) => void; onView: (prompt: Prompt) => void }) { - const showResults = open || !!keyword.trim(); - const filtered = useMemo(() => { - const query = keyword.trim().toLowerCase(); - return query ? items.filter((item) => [item.title, item.prompt, item.description, ...item.tags].join(" ").toLowerCase().includes(query)) : items; - }, [items, keyword]); - - return ( -
- - {showResults ? ( -
- {filtered.map((item) => onInsert({ kind: "text", content: item.prompt, title: item.title })} onView={() => onView(item)} />)} - {!filtered.length ?
{keyword.trim() ? "无匹配提示词" : "还没有保存提示词"}
: null} -
- ) : null} -
- ); -} - function PromptSourceGroup({ sourceId, sourceName, diff --git a/web/src/components/layout/config-prompt-sources.tsx b/web/src/components/layout/config-prompt-sources.tsx index a7c1769..6dcabca 100644 --- a/web/src/components/layout/config-prompt-sources.tsx +++ b/web/src/components/layout/config-prompt-sources.tsx @@ -45,7 +45,7 @@ export function ConfigPromptSources() { const handleDelete = (source: PromptSource) => { modal.confirm({ title: `删除「${source.name}」?`, - content: "来源配置会被移除,已经保存到我的提示词的内容不受影响。", + content: "来源配置会被移除,已经加入我的资产的内容不受影响。", okText: "删除", okButtonProps: { danger: true }, cancelText: "取消", diff --git a/web/src/components/prompts/prompt-select-dialog.tsx b/web/src/components/prompts/prompt-select-dialog.tsx index 4edbf30..6f7b4fc 100644 --- a/web/src/components/prompts/prompt-select-dialog.tsx +++ b/web/src/components/prompts/prompt-select-dialog.tsx @@ -12,7 +12,7 @@ export function PromptSelectDialog({ open, onOpenChange, onSelect }: { open: boo const [keyword, setKeyword] = useState(""); const [selectedTags, setSelectedTags] = useState([]); const [selectedCategory, setSelectedCategory] = useState(ALL_PROMPTS_OPTION); - const { query, items, tags: promptTags, categories: promptCategories } = usePromptList({ keyword, tags: selectedTags, category: selectedCategory, enabled: open, includePersonal: true }); + const { query, items, tags: promptTags, categories: promptCategories } = usePromptList({ keyword, tags: selectedTags, category: selectedCategory, enabled: open }); const toggleTag = (tag: string) => { if (tag === ALL_PROMPTS_OPTION) return setSelectedTags([]); setSelectedTags((items) => (items.includes(tag) ? items.filter((item) => item !== tag) : [...items, tag])); diff --git a/web/src/components/prompts/use-prompt-list.ts b/web/src/components/prompts/use-prompt-list.ts index 58270f1..24af0ce 100644 --- a/web/src/components/prompts/use-prompt-list.ts +++ b/web/src/components/prompts/use-prompt-list.ts @@ -1,14 +1,19 @@ -import { useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useInfiniteQuery } from "@tanstack/react-query"; import { ALL_PROMPTS_OPTION, fetchPrompts } from "@/services/api/prompts"; export const PROMPT_PAGE_SIZE = 20; -export function usePromptList({ keyword, tags, category, enabled = true, includePersonal = false }: { keyword: string; tags: string[]; category: string; enabled?: boolean; includePersonal?: boolean }) { +export function usePromptList({ keyword, tags, category, enabled = true }: { keyword: string; tags: string[]; category: string; enabled?: boolean }) { + const [debouncedKeyword, setDebouncedKeyword] = useState(keyword); + useEffect(() => { + const timer = setTimeout(() => setDebouncedKeyword(keyword), 300); + return () => clearTimeout(timer); + }, [keyword]); const query = useInfiniteQuery({ - queryKey: ["prompts", keyword, tags, category, includePersonal], - queryFn: ({ pageParam }) => fetchPrompts({ keyword, tag: tags, category, page: pageParam, pageSize: PROMPT_PAGE_SIZE, includePersonal }), + queryKey: ["prompts", debouncedKeyword, tags, category], + queryFn: ({ pageParam }) => fetchPrompts({ keyword: debouncedKeyword, tag: tags, category, page: pageParam, pageSize: PROMPT_PAGE_SIZE }), initialPageParam: 1, getNextPageParam: (lastPage, pages) => (pages.reduce((total, page) => total + page.items.length, 0) < lastPage.total ? pages.length + 1 : undefined), enabled, diff --git a/web/src/pages/prompts/components/my-prompt-editor-dialog.tsx b/web/src/pages/prompts/components/my-prompt-editor-dialog.tsx deleted file mode 100644 index 7055234..0000000 --- a/web/src/pages/prompts/components/my-prompt-editor-dialog.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { App, Button, Input, Modal, Space } from "antd"; -import { useEffect, useState } from "react"; - -import type { PersonalPrompt, PersonalPromptInput } from "@/stores/use-prompt-store"; - -const EMPTY_PROMPT: PersonalPromptInput = { - title: "", - prompt: "", - description: "", - coverUrl: "", - referenceImageUrls: [], - tags: [], -}; - -export function MyPromptEditorDialog({ open, prompt, onSave, onClose }: { open: boolean; prompt: PersonalPrompt | null; onSave: (value: PersonalPromptInput) => void; onClose: () => void }) { - const { message } = App.useApp(); - const [draft, setDraft] = useState(EMPTY_PROMPT); - const [tags, setTags] = useState(""); - const [referenceImages, setReferenceImages] = useState(""); - - useEffect(() => { - if (!open) return; - setDraft(prompt ? toInput(prompt) : EMPTY_PROMPT); - setTags(prompt?.tags.join(", ") || ""); - setReferenceImages(prompt?.referenceImageUrls.join("\n") || ""); - }, [open, prompt]); - - const patch = (value: Partial) => setDraft((current) => ({ ...current, ...value })); - const save = () => { - if (!draft.title.trim()) return message.warning("请输入标题"); - if (!draft.prompt.trim()) return message.warning("请输入提示词"); - onSave({ - ...draft, - title: draft.title.trim(), - prompt: draft.prompt.trim(), - description: draft.description.trim(), - coverUrl: draft.coverUrl.trim(), - tags: splitValues(tags, /[,,\n]/), - referenceImageUrls: splitValues(referenceImages, /\n/), - }); - onClose(); - }; - - return ( - - - - - } - > -
- - - -
- - -
- -
-
- ); -} - -function toInput(prompt: PersonalPrompt): PersonalPromptInput { - return { - title: prompt.title, - prompt: prompt.prompt, - description: prompt.description, - coverUrl: prompt.coverUrl, - referenceImageUrls: prompt.referenceImageUrls, - tags: prompt.tags, - imageMode: prompt.imageMode, - imageModel: prompt.imageModel, - imageSize: prompt.imageSize, - imageCount: prompt.imageCount, - }; -} - -function splitValues(value: string, separator: RegExp) { - return Array.from(new Set(value.split(separator).map((item) => item.trim()).filter(Boolean))); -} diff --git a/web/src/pages/prompts/components/prompt-detail-dialog.tsx b/web/src/pages/prompts/components/prompt-detail-dialog.tsx index 1199d31..01d6e4b 100644 --- a/web/src/pages/prompts/components/prompt-detail-dialog.tsx +++ b/web/src/pages/prompts/components/prompt-detail-dialog.tsx @@ -1,9 +1,9 @@ -import { BookmarkPlus, Copy, FileText, FolderPlus } from "lucide-react"; +import { Copy, FileText, FolderPlus } from "lucide-react"; import { Button, Modal, Space, Tag } from "antd"; import { formatPromptDate, type Prompt } from "@/services/api/prompts"; -export function PromptDetailDialog({ prompt, onClose, onCopy, onSaveAsset, onSavePrompt }: { prompt: Prompt | null; onClose: () => void; onCopy: (prompt: string) => void; onSaveAsset?: (prompt: Prompt) => void; onSavePrompt?: (prompt: Prompt) => void }) { +export function PromptDetailDialog({ prompt, onClose, onCopy, onSaveAsset }: { prompt: Prompt | null; onClose: () => void; onCopy: (prompt: string) => void; onSaveAsset?: (prompt: Prompt) => void }) { return ( <> @@ -35,11 +35,6 @@ export function PromptDetailDialog({ prompt, onClose, onCopy, onSaveAsset, onSav 加入我的资产 ) : null} - {onSavePrompt ? ( - - ) : null} diff --git a/web/src/pages/prompts/index.tsx b/web/src/pages/prompts/index.tsx index 9efaf08..5e6911d 100644 --- a/web/src/pages/prompts/index.tsx +++ b/web/src/pages/prompts/index.tsx @@ -1,38 +1,24 @@ -import { BookmarkPlus, FolderPlus, Pencil, Plus, Search, Trash2 } from "lucide-react"; -import { type ReactNode, type UIEvent, useEffect, useMemo, useState } from "react"; -import { App, Button, Empty, Input, Popconfirm, Space, Spin, Tabs, Tag, Tooltip } from "antd"; +import { FolderPlus, Search } from "lucide-react"; +import { type ReactNode, type UIEvent, useEffect, useState } from "react"; +import { App, Button, Empty, Input, Spin, Tag } from "antd"; import { PromptCard } from "@/components/prompts/prompt-card"; import { usePromptList } from "@/components/prompts/use-prompt-list"; -import { MyPromptEditorDialog } from "./components/my-prompt-editor-dialog"; import { PromptDetailDialog } from "./components/prompt-detail-dialog"; import { useCopyText } from "@/hooks/use-copy-text"; import { cn } from "@/lib/utils"; import { useAssetStore } from "@/stores/use-asset-store"; -import { usePromptStore, type PersonalPrompt, type PersonalPromptInput } from "@/stores/use-prompt-store"; -import { ALL_PROMPTS_OPTION, personalPromptToPrompt, type Prompt } from "@/services/api/prompts"; +import { ALL_PROMPTS_OPTION, type Prompt } from "@/services/api/prompts"; export default function PromptsPage() { const { message } = App.useApp(); - const [activeTab, setActiveTab] = useState("library"); const [titleKeyword, setTitleKeyword] = useState(""); const [selectedTags, setSelectedTags] = useState([]); const [selectedCategory, setSelectedCategory] = useState(ALL_PROMPTS_OPTION); const [selectedPrompt, setSelectedPrompt] = useState(null); - const [editorOpen, setEditorOpen] = useState(false); - const [editingPrompt, setEditingPrompt] = useState(null); const addAsset = useAssetStore((state) => state.addAsset); - const personalPrompts = usePromptStore((state) => state.prompts); - const addPrompt = usePromptStore((state) => state.addPrompt); - const updatePrompt = usePromptStore((state) => state.updatePrompt); - const removePrompt = usePromptStore((state) => state.removePrompt); const copyText = useCopyText(); - const { query, items: promptItems, tags: promptTags, categories: promptCategoryOptions, total: totalPrompts } = usePromptList({ keyword: titleKeyword, tags: selectedTags, category: selectedCategory, enabled: activeTab === "library" }); - const filteredPersonalPrompts = useMemo(() => { - const keyword = titleKeyword.trim().toLowerCase(); - if (!keyword) return personalPrompts; - return personalPrompts.filter((item) => [item.title, item.prompt, item.description, ...item.tags].join(" ").toLowerCase().includes(keyword)); - }, [personalPrompts, titleKeyword]); + const { query, items: promptItems, tags: promptTags, categories: promptCategoryOptions, total: totalPrompts } = usePromptList({ keyword: titleKeyword, tags: selectedTags, category: selectedCategory }); useEffect(() => { if (query.isError) message.error(query.error instanceof Error ? query.error.message : "获取提示词失败"); @@ -48,104 +34,52 @@ export default function PromptsPage() { message.success("已加入我的资产"); }; - const saveToMyPrompts = (item: Prompt) => { - addPrompt(toPersonalInput(item)); - message.success("已保存到我的提示词"); - }; - - const openNewPrompt = () => { - setEditingPrompt(null); - setEditorOpen(true); - }; - - const openEditPrompt = (item: PersonalPrompt) => { - setEditingPrompt(item); - setEditorOpen(true); - }; - - const savePersonalPrompt = (input: PersonalPromptInput) => { - if (editingPrompt) updatePrompt(editingPrompt.id, input); - else addPrompt(input); - message.success(editingPrompt ? "提示词已更新" : "提示词已添加"); - }; - const handleListScroll = (event: UIEvent) => { - if (activeTab !== "library") return; const target = event.currentTarget; if (query.hasNextPage && !query.isFetchingNextPage && target.scrollTop + target.clientHeight >= target.scrollHeight - 160) void query.fetchNextPage(); }; - const personalItems = filteredPersonalPrompts.map(personalPromptToPrompt); - const visibleCount = activeTab === "library" ? totalPrompts : filteredPersonalPrompts.length; - return (
-
-
-
-
-

提示词中心

-

当前共 {visibleCount} 条提示词

-
- {activeTab === "personal" ? ( - - ) : null} +
+
+
+

提示词中心

+

当前共 {totalPrompts} 条提示词

- -
- } value={titleKeyword} placeholder="搜索标题、内容或标签" onChange={(event) => setTitleKeyword(event.target.value)} /> -
- {activeTab === "library" ? ( -
+
+ +
+ } value={titleKeyword} placeholder="搜索标题、内容或标签" onChange={(event) => setTitleKeyword(event.target.value)} /> + {query.isLoading ?
: null} + {!query.isLoading ?
} onCopy={(item) => copyText(item.prompt, "提示词已复制")} emptyText="没有找到匹配的提示词" />
: null} +
{query.isFetchingNextPage ? "加载中..." : query.hasNextPage ? "继续向下滚动加载更多" : promptItems.length > 0 ? "已经到底了" : null}
+
+
- - {activeTab === "library" && query.isLoading ?
: null} - {activeTab === "library" && !query.isLoading ? ( - <>
- setSelectedPrompt(null)} onCopy={(prompt) => copyText(prompt, "提示词已复制")} onSaveAsset={selectedPrompt?.sourceId === "personal" ? undefined : savePromptAsset} onSavePrompt={selectedPrompt?.sourceId === "personal" ? undefined : saveToMyPrompts} /> - setEditorOpen(false)} /> + setSelectedPrompt(null)} onCopy={(prompt) => copyText(prompt, "提示词已复制")} onSaveAsset={savePromptAsset} />
); } function PromptFilter({ label, options, selected, onChange }: { label: string; options: string[]; selected: string; onChange: (value: string) => void }) { - return
{label}
{options.map((option) => onChange(option)}>{option})}
; + return
{label}
{options.map((option) => onChange(option)}>{option})}
; } function PromptGrid({ items, onOpen, onCopy, renderActions, emptyText }: { items: Prompt[]; onOpen: (item: Prompt) => void; onCopy: (item: Prompt) => void; renderActions: (item: Prompt) => ReactNode; emptyText: string }) { - return
{items.map((item) => onOpen(item)} onCopy={() => onCopy(item)} extraAction={renderActions(item)} />)}
{items.length === 0 ? : null}
; + return
{items.map((item) => onOpen(item)} onCopy={() => onCopy(item)} extraAction={renderActions(item)} />)}
{items.length === 0 ? : null}
; } -function toPersonalInput(item: Prompt): PersonalPromptInput { - return { title: item.title, prompt: item.prompt, description: item.description, coverUrl: item.coverUrl, referenceImageUrls: item.referenceImageUrls, tags: item.tags, imageMode: item.imageMode, imageModel: item.imageModel, imageSize: item.imageSize, imageCount: item.imageCount }; -} diff --git a/web/src/services/api/prompts.ts b/web/src/services/api/prompts.ts index eedf6c6..9b96d05 100644 --- a/web/src/services/api/prompts.ts +++ b/web/src/services/api/prompts.ts @@ -2,7 +2,6 @@ import localforage from "localforage"; import { runPromptSource, type RawPrompt } from "./prompt-source-runtime"; import { usePromptSourceStore } from "@/stores/use-prompt-source-store"; -import { usePromptStore, type PersonalPrompt } from "@/stores/use-prompt-store"; import type { PromptSource } from "./prompt-source-presets"; export type Prompt = RawPrompt & { @@ -12,7 +11,6 @@ export type Prompt = RawPrompt & { }; export const ALL_PROMPTS_OPTION = "全部"; -export const PERSONAL_PROMPTS_CATEGORY = "我的提示词"; export type PromptListResponse = { items: Prompt[]; @@ -76,17 +74,6 @@ function withSourceMeta(source: PromptSource, items: RawPrompt[]): Prompt[] { })); } -export function personalPromptToPrompt(item: PersonalPrompt): Prompt { - return { - ...item, - coverUrl: item.coverUrl || item.referenceImageUrls[0] || "", - sourceId: "personal", - category: PERSONAL_PROMPTS_CATEGORY, - githubUrl: "", - preview: "", - }; -} - async function readSourceCache(sourceId: string) { return promptCacheStore.getItem(cacheKey(sourceId)); } @@ -135,7 +122,7 @@ async function getSourcePrompts(source: PromptSource): Promise { return (await readSourceCache(source.id))?.items || []; } -async function getAllPrompts(includePersonal: boolean): Promise { +async function getAllPrompts(): Promise { const settled = await Promise.all( enabledSources().map(async (source) => { try { @@ -145,19 +132,17 @@ async function getAllPrompts(includePersonal: boolean): Promise { } }), ); - const personal = includePersonal ? usePromptStore.getState().prompts.map(personalPromptToPrompt) : []; - return [...personal, ...settled.flat()]; + return settled.flat(); } -export async function fetchPrompts({ keyword = "", tag = [], category = ALL_PROMPTS_OPTION, page = 1, pageSize = 20, includePersonal = true }: { keyword?: string; tag?: string[]; category?: string; page?: number; pageSize?: number; includePersonal?: boolean } = {}) { - const items = await getAllPrompts(includePersonal); +export async function fetchPrompts({ keyword = "", tag = [], category = ALL_PROMPTS_OPTION, page = 1, pageSize = 20 }: { keyword?: string; tag?: string[]; category?: string; page?: number; pageSize?: number } = {}) { + const items = await getAllPrompts(); const normalizedKeyword = keyword.trim().toLowerCase(); const normalizedPage = Math.max(1, page); const normalizedPageSize = Math.max(1, Math.min(100, pageSize)); const withoutTagFilter = filterPrompts(items, { keyword: normalizedKeyword, category, tags: [] }); const filtered = filterPrompts(items, { keyword: normalizedKeyword, category, tags: tag }); const categories = enabledSources().map((source) => source.name); - if (includePersonal && usePromptStore.getState().prompts.length) categories.unshift(PERSONAL_PROMPTS_CATEGORY); return { items: filtered.slice((normalizedPage - 1) * normalizedPageSize, normalizedPage * normalizedPageSize), diff --git a/web/src/stores/use-prompt-store.ts b/web/src/stores/use-prompt-store.ts deleted file mode 100644 index 079be89..0000000 --- a/web/src/stores/use-prompt-store.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { nanoid } from "nanoid"; -import { create } from "zustand"; -import { createJSONStorage, persist } from "zustand/middleware"; - -import { localForageStorage } from "@/lib/localforage-storage"; - -export type PersonalPrompt = { - id: string; - title: string; - prompt: string; - description: string; - coverUrl: string; - referenceImageUrls: string[]; - tags: string[]; - createdAt: string; - updatedAt: string; - imageMode?: string; - imageModel?: string; - imageSize?: string; - imageCount?: number; -}; - -export type PersonalPromptInput = Omit; - -type PromptStore = { - hydrated: boolean; - prompts: PersonalPrompt[]; - addPrompt: (prompt: PersonalPromptInput) => string; - updatePrompt: (id: string, prompt: PersonalPromptInput) => void; - removePrompt: (id: string) => void; -}; - -export const usePromptStore = create()( - persist( - (set) => ({ - hydrated: false, - prompts: [], - addPrompt: (prompt) => { - const id = nanoid(); - const now = new Date().toISOString(); - set((state) => ({ prompts: [{ ...prompt, id, createdAt: now, updatedAt: now }, ...state.prompts] })); - return id; - }, - updatePrompt: (id, prompt) => set((state) => ({ prompts: state.prompts.map((item) => (item.id === id ? { ...item, ...prompt, updatedAt: new Date().toISOString() } : item)) })), - removePrompt: (id) => set((state) => ({ prompts: state.prompts.filter((item) => item.id !== id) })), - }), - { - name: "infinite-canvas:prompt_store", - storage: createJSONStorage(() => localForageStorage), - onRehydrateStorage: () => () => usePromptStore.setState({ hydrated: true }), - }, - ), -);