diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx index 100143ed33..9ff6f440b3 100644 --- a/studio/frontend/src/components/assistant-ui/thread.tsx +++ b/studio/frontend/src/components/assistant-ui/thread.tsx @@ -69,6 +69,11 @@ import { getExternalReasoningCapabilities } from "@/features/chat/provider-capab import { useRagToolDisabled } from "@/features/chat/hooks/use-rag-tool-disabled"; import { useChatRuntimeStore } from "@/features/chat/stores/chat-runtime-store"; import { useExternalProvidersStore } from "@/features/chat/stores/external-providers-store"; +import { + PLUS_MENU_ORDER, + type PlusMenuItemId, + usePlusMenuPrefsStore, +} from "@/features/chat"; import { deleteThreadMessage } from "@/features/chat/utils/delete-thread-message"; import { ThreadDocumentsBar } from "@/features/rag/components/thread-documents-bar"; import { KnowledgeBaseComposerButton } from "@/features/rag/components/knowledge-base-composer-button"; @@ -134,6 +139,7 @@ import { type KeyboardEvent, type DragEvent as ReactDragEvent, type ReactNode, + Fragment, createContext, useCallback, useContext, @@ -2093,17 +2099,179 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({ const messageCount = useAuiState(({ thread }) => thread.messages.length); const { startQueue } = useContext(PromptQueueContext); + const plusPins = usePlusMenuPrefsStore((s) => s.pins); + const [recentPrompts, setRecentPrompts] = useState([]); const refreshRecentPrompts = useCallback(async () => { try { const rows = await listPromptEntries(); - setRecentPrompts( - [...rows].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, 3), - ); + const byRecent = [...rows].sort((a, b) => b.updatedAt - a.updatedAt); + // Pinned prompts take over the submenu; fall back to the 3 most recent + // when nothing is pinned. + const pinnedIds = usePlusMenuPrefsStore.getState().pinnedPromptIds; + const pinned = byRecent.filter((p) => pinnedIds.includes(p.id)); + setRecentPrompts(pinned.length > 0 ? pinned : byRecent.slice(0, 3)); } catch { } }, []); + // Adjustable "+" menu items, keyed by id. Pinned ones render at the top + // level; the rest fall into the "More" overflow submenu. The core items + // (photos, web search, code) and "More" itself are always shown and live + // outside this map. + const plusMenuNodes: Record = { + chatWithFiles: ( + setRagEnabled(!ragEnabled)} + > + + Chat with Files + {ragEnabled && !ragDisabled ? ( + + ) : null} + + ), + mcp: ( + setMcpEnabledForChat(!mcpEnabledForChat)} + > + + MCP + {mcpEnabledForChat && !mcpDisabled ? ( + + ) : null} + + ), + savedPrompts: ( + + + + Saved prompts + + + {recentPrompts.map((p) => ( + aui.composer().setText(p.text)} + > + {p.name} + + ))} + {recentPrompts.length > 0 ? : null} + setPromptStorageOpen(true)}> + All saved prompts… + + + + ), + compareChat: ( + startCompare()}> + + Compare chat + + ), + exportChat: ( + + + + Export chat + + + { + if (!activeThreadId) return; + exportConversationRawJsonl(activeThreadId).catch(() => + toast.error("Export failed."), + ); + }} + > + Raw JSONL + + { + if (!activeThreadId) return; + exportConversationCsv(activeThreadId).catch(() => + toast.error("Export failed."), + ); + }} + > + CSV + + { + if (!activeThreadId) return; + exportConversationShareGPT(activeThreadId).catch(() => + toast.error("Export failed."), + ); + }} + > + ShareGPT JSONL + + + + ), + canvas: ( + setArtifactsEnabled(!artifactsEnabled)} + > + + Canvas + {artifactsEnabled ? ( + + ) : null} + + ), + projects: ( + + + + Projects + + + setNewProjectOpen(true)}> + + New project + + Recents + {recentProjects.length > 0 ? ( + recentProjects.map((project) => ( + openProject(project.id)} + > + + {project.name} + + )) + ) : ( + + No recent projects + + )} + + + ), + }; + const pinnedPlusItems = PLUS_MENU_ORDER.filter((id) => plusPins[id]); + const overflowPlusItems = PLUS_MENU_ORDER.filter((id) => !plusPins[id]); + return ( <> = ({ align="start" sideOffset={0} avoidCollisions={true} - className="unsloth-plus-menu w-[212px]" + className="unsloth-plus-menu w-[244px]" // Don't refocus the + on close; restored focus showed a stray ring. onCloseAutoFocus={(event) => event.preventDefault()} > @@ -2220,165 +2388,22 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({ )} - setRagEnabled(!ragEnabled)} - > - - Chat with Files - {ragEnabled && !ragDisabled ? ( - - ) : null} - - setMcpEnabledForChat(!mcpEnabledForChat)} - > - - MCP - {mcpEnabledForChat && !mcpDisabled ? ( - - ) : null} - - - - - More - - - - - - Saved prompts - - - {recentPrompts.map((p) => ( - aui.composer().setText(p.text)} - > - {p.name} - - ))} - {recentPrompts.length > 0 ? : null} - setPromptStorageOpen(true)}> - All saved prompts… - - - - startCompare()}> - - Compare chat - - - - - Export chat - - - { - if (!activeThreadId) return; - exportConversationRawJsonl(activeThreadId).catch(() => - toast.error("Export failed."), - ); - }} - > - Raw JSONL - - { - if (!activeThreadId) return; - exportConversationCsv(activeThreadId).catch(() => - toast.error("Export failed."), - ); - }} - > - CSV - - { - if (!activeThreadId) return; - exportConversationShareGPT(activeThreadId).catch(() => - toast.error("Export failed."), - ); - }} - > - ShareGPT JSONL - - - - setArtifactsEnabled(!artifactsEnabled)} - > - - Canvas - {artifactsEnabled ? ( - - ) : null} - - - - - - - - Projects - - - setNewProjectOpen(true)}> - - New project - - Recents - {recentProjects.length > 0 ? ( - recentProjects.map((project) => ( - openProject(project.id)} - > - - {project.name} - - )) - ) : ( - - No recent projects - - )} - - + {pinnedPlusItems.map((id) => ( + {plusMenuNodes[id]} + ))} + {overflowPlusItems.length > 0 ? ( + + + + More + + + {overflowPlusItems.map((id) => ( + {plusMenuNodes[id]} + ))} + + + ) : null} { side="bottom" align="start" onCloseAutoFocus={(e) => e.preventDefault()} - className="aui-action-bar-more-content z-50 min-w-32 overflow-hidden rounded-md [--radius:1.1rem] bg-popover p-1 text-popover-foreground shadow-[0_2px_8px_-2px_rgba(0,0,0,0.16)] dark:shadow-none" + className="aui-action-bar-more-content z-50 min-w-32 overflow-hidden rounded-full bg-popover p-1 text-popover-foreground shadow-[0_2px_8px_-2px_rgba(0,0,0,0.16)] dark:shadow-none" > - + Export as Markdown diff --git a/studio/frontend/src/features/chat/index.ts b/studio/frontend/src/features/chat/index.ts index dde3a75475..fcb62b94a7 100644 --- a/studio/frontend/src/features/chat/index.ts +++ b/studio/frontend/src/features/chat/index.ts @@ -18,6 +18,11 @@ export { } from "./chat-settings-sheet"; export { useChatRuntimeStore } from "./stores/chat-runtime-store"; export { useChatSearchStore } from "./stores/chat-search-store"; +export { + PLUS_MENU_ORDER, + usePlusMenuPrefsStore, + type PlusMenuItemId, +} from "./stores/plus-menu-prefs-store"; export { useChatModelRuntime } from "./hooks/use-chat-model-runtime"; export { isExternalModelId } from "./external-providers"; export { ChatSearchDialog } from "./components/chat-search-dialog"; diff --git a/studio/frontend/src/features/chat/prompt-storage/prompt-storage-dialog.tsx b/studio/frontend/src/features/chat/prompt-storage/prompt-storage-dialog.tsx index e62f96720d..2b3acbf87d 100644 --- a/studio/frontend/src/features/chat/prompt-storage/prompt-storage-dialog.tsx +++ b/studio/frontend/src/features/chat/prompt-storage/prompt-storage-dialog.tsx @@ -53,6 +53,7 @@ import { syncStoredChatMessages, } from "../utils/chat-history-storage"; import { notifyChatHistoryUpdated } from "../api/chat-api"; +import { usePlusMenuPrefsStore } from "../stores/plus-menu-prefs-store"; import type { ThreadRecord, MessageRecord } from "../types"; function newId(): string { @@ -1235,6 +1236,9 @@ function PromptCard({ const [editing, setEditing] = useState(false); const [name, setName] = useState(entry.name); const [text, setText] = useState(entry.text); + const pinnedPromptIds = usePlusMenuPrefsStore((s) => s.pinnedPromptIds); + const togglePinnedPrompt = usePlusMenuPrefsStore((s) => s.togglePinnedPrompt); + const isPinned = pinnedPromptIds.includes(entry.id); const handleSave = useCallback(async () => { const trimName = name.trim(); @@ -1281,6 +1285,9 @@ function PromptCard({ return (
+ {isPinned ? ( + + ) : null} {entry.name}
+