diff --git a/.changeset/pre.json b/.changeset/pre.json index 76e294f..f0f36d0 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -44,9 +44,11 @@ "shiny-doodles-jump", "slick-roses-fix", "social-donkeys-cross", + "soft-cases-share", "stale-groups-poke", "tender-jeans-occur", "true-suits-fly", + "vast-places-rhyme", "wicked-spoons-fry" ] } diff --git a/.changeset/soft-cases-share.md b/.changeset/soft-cases-share.md new file mode 100644 index 0000000..7727d11 --- /dev/null +++ b/.changeset/soft-cases-share.md @@ -0,0 +1,6 @@ +--- +"@pulse-editor/shared-utils": patch +"@pulse-editor/react-api": patch +--- + +Add queue to command react hook diff --git a/.changeset/vast-places-rhyme.md b/.changeset/vast-places-rhyme.md new file mode 100644 index 0000000..d4ccac7 --- /dev/null +++ b/.changeset/vast-places-rhyme.md @@ -0,0 +1,6 @@ +--- +"@pulse-editor/shared-utils": patch +"@pulse-editor/react-api": patch +--- + +Add env hook diff --git a/npm-packages/react-api/CHANGELOG.md b/npm-packages/react-api/CHANGELOG.md index df9c39a..6578d29 100644 --- a/npm-packages/react-api/CHANGELOG.md +++ b/npm-packages/react-api/CHANGELOG.md @@ -1,5 +1,21 @@ # @pulse-editor/react-api +## 0.1.1-alpha.36 + +### Patch Changes + +- Add queue to command react hook +- Updated dependencies + - @pulse-editor/shared-utils@0.1.1-alpha.36 + +## 0.1.1-alpha.35 + +### Patch Changes + +- Add env hook +- Updated dependencies + - @pulse-editor/shared-utils@0.1.1-alpha.35 + ## 0.1.1-alpha.34 ### Patch Changes diff --git a/npm-packages/react-api/package.json b/npm-packages/react-api/package.json index 599c33c..ca259d4 100644 --- a/npm-packages/react-api/package.json +++ b/npm-packages/react-api/package.json @@ -1,6 +1,6 @@ { "name": "@pulse-editor/react-api", - "version": "0.1.1-alpha.34", + "version": "0.1.1-alpha.36", "main": "dist/main.js", "files": [ "dist" @@ -38,7 +38,7 @@ "typescript-eslint": "^8.30.1" }, "peerDependencies": { - "@pulse-editor/shared-utils": "0.1.1-alpha.34", + "@pulse-editor/shared-utils": "0.1.1-alpha.36", "react": "^19.0.0", "react-dom": "^19.0.0" } diff --git a/npm-packages/react-api/src/hooks/editor/use-command.ts b/npm-packages/react-api/src/hooks/editor/use-command.ts index 103d27d..8c3afa5 100644 --- a/npm-packages/react-api/src/hooks/editor/use-command.ts +++ b/npm-packages/react-api/src/hooks/editor/use-command.ts @@ -4,8 +4,8 @@ import { IMCMessageTypeEnum, ReceiverHandler, } from "@pulse-editor/shared-utils"; +import { useEffect, useRef, useState } from "react"; import useIMC from "../../lib/use-imc"; -import { useEffect, useState } from "react"; /** * Register an extension command to listen to IMC messages from the core, @@ -17,7 +17,8 @@ import { useEffect, useState } from "react"; */ export default function useCommand( commandInfo: CommandInfo, - callbackHandler?: (args: any) => Promise + callbackHandler?: (args: any) => Promise, + isExtReady: boolean = true ) { const { isReady, imc } = useIMC(getReceiverHandlerMap()); @@ -25,25 +26,29 @@ export default function useCommand( ((args: any) => Promise) | undefined >(undefined); + // Queue to hold commands until extension is ready + const commandQueue = useRef<{ args: any; resolve: (v: any) => void }[]>([]); + + async function executeCommand(args: any) { + if (!handler) return; + + const res = await handler(args); + return res; + } + function getReceiverHandlerMap() { const receiverHandlerMap = new Map([ [ IMCMessageTypeEnum.EditorRunExtCommand, - async (senderWindow: Window, message: IMCMessage) => { + async (_senderWindow: Window, message: IMCMessage) => { if (!commandInfo) { throw new Error("Extension command is not available"); } - const { - name, - args, - }: { - name: string; - args: any; - } = message.payload; + const { name, args }: { name: string; args: any } = message.payload; if (name === commandInfo.name) { - // Check if the parameters match the command's parameters + // Validate parameters const commandParameters = commandInfo.parameters; if ( Object.keys(args).length !== Object.keys(commandParameters).length @@ -54,6 +59,7 @@ export default function useCommand( }, got ${Object.keys(args).length}` ); } + for (const [key, value] of Object.entries(args)) { if (commandInfo.parameters[key] === undefined) { throw new Error(`Invalid parameter: ${key}`); @@ -67,13 +73,15 @@ export default function useCommand( } } - // Execute the command handler with the parameters - if (handler) { - const res = await handler(args); - if (res) { - return res; - } + // If extension is ready, execute immediately + if (isExtReady) { + return await executeCommand(args); } + + // Otherwise, queue the command and return when executed + return new Promise((resolve) => { + commandQueue.current.push({ args, resolve }); + }); } }, ], @@ -81,9 +89,21 @@ export default function useCommand( return receiverHandlerMap; } + // Flush queued commands when isExtReady becomes true + useEffect(() => { + if (isExtReady && commandQueue.current.length > 0) { + const pending = [...commandQueue.current]; + commandQueue.current = []; + pending.forEach(async ({ args, resolve }) => { + const res = await executeCommand(args); + resolve(res); + }); + } + }, [isExtReady]); + useEffect(() => { imc?.updateReceiverHandlerMap(getReceiverHandlerMap()); - }, [handler, imc]); + }, [handler, imc, isExtReady]); useEffect(() => { setHandler(() => callbackHandler); diff --git a/npm-packages/react-api/src/hooks/editor/use-env.ts b/npm-packages/react-api/src/hooks/editor/use-env.ts new file mode 100644 index 0000000..6b9a95f --- /dev/null +++ b/npm-packages/react-api/src/hooks/editor/use-env.ts @@ -0,0 +1,26 @@ +import { IMCMessage, IMCMessageTypeEnum } from "@pulse-editor/shared-utils"; +import { useEffect, useState } from "react"; +import useIMC from "../../lib/use-imc"; + +export default function usePulseEnv() { + const receiverHandlerMap = new Map< + IMCMessageTypeEnum, + (senderWindow: Window, message: IMCMessage) => Promise + >(); + + const { imc, isReady } = useIMC(receiverHandlerMap); + const [envs, setEnvs] = useState>({}); + + useEffect(() => { + if (isReady) { + imc?.sendMessage(IMCMessageTypeEnum.EditorGetEnv).then((env) => { + setEnvs(env); + }); + } + }, [isReady]); + + return { + isReady, + envs, + }; +} diff --git a/npm-packages/react-api/src/main.ts b/npm-packages/react-api/src/main.ts index f8e753b..fae3589 100644 --- a/npm-packages/react-api/src/main.ts +++ b/npm-packages/react-api/src/main.ts @@ -1,11 +1,11 @@ import useAgentTools from "./hooks/agent/use-agent-tools"; import useAgents from "./hooks/agent/use-agents"; +import useCommand from "./hooks/editor/use-command"; import useFileView from "./hooks/editor/use-file-view"; import useLoading from "./hooks/editor/use-loading"; import useNotification from "./hooks/editor/use-notification"; import useTheme from "./hooks/editor/use-theme"; import useToolbar from "./hooks/editor/use-toolbar"; -import useCommand from "./hooks/editor/use-command"; import useImageGen from "./hooks/ai-modality/use-image-gen"; import useLLM from "./hooks/ai-modality/use-llm"; @@ -13,22 +13,24 @@ import useOCR from "./hooks/ai-modality/use-ocr"; import useSTT from "./hooks/ai-modality/use-stt"; import useTTS from "./hooks/ai-modality/use-tts"; import useVideoGen from "./hooks/ai-modality/use-video-gen"; +import usePulseEnv from "./hooks/editor/use-env"; import useTerminal from "./hooks/terminal/use-terminal"; export { useAgentTools, useAgents, + useCommand, useFileView, - useNotification, - useTheme, - useToolbar, useImageGen, - useVideoGen, useLLM, + useLoading, + useNotification, useOCR, + usePulseEnv, useSTT, useTTS, - useCommand, useTerminal, - useLoading, + useTheme, + useToolbar, + useVideoGen, }; diff --git a/npm-packages/shared-utils/CHANGELOG.md b/npm-packages/shared-utils/CHANGELOG.md index 0b0a716..5645e88 100644 --- a/npm-packages/shared-utils/CHANGELOG.md +++ b/npm-packages/shared-utils/CHANGELOG.md @@ -1,5 +1,17 @@ # @pulse-editor/shared-utils +## 0.1.1-alpha.36 + +### Patch Changes + +- Add queue to command react hook + +## 0.1.1-alpha.35 + +### Patch Changes + +- Add env hook + ## 0.1.1-alpha.34 ### Patch Changes diff --git a/npm-packages/shared-utils/package.json b/npm-packages/shared-utils/package.json index 28a57ad..4045d31 100644 --- a/npm-packages/shared-utils/package.json +++ b/npm-packages/shared-utils/package.json @@ -1,6 +1,6 @@ { "name": "@pulse-editor/shared-utils", - "version": "0.1.1-alpha.34", + "version": "0.1.1-alpha.36", "main": "dist/main.js", "files": [ "dist" diff --git a/npm-packages/shared-utils/src/types/types.ts b/npm-packages/shared-utils/src/types/types.ts index f2d704e..eef7256 100644 --- a/npm-packages/shared-utils/src/types/types.ts +++ b/npm-packages/shared-utils/src/types/types.ts @@ -33,6 +33,8 @@ export enum IMCMessageTypeEnum { EditorThemeUpdate = "editor-theme-update", // Send notification EditorShowNotification = "editor-show-notification", + // Get environment variables + EditorGetEnv = "editor-get-env", // #endregion // #region Platform API interaction messages (require OS-like environment) diff --git a/web/components/explorer/app/app-explorer.tsx b/web/components/explorer/app/app-explorer.tsx index 67f12f9..2adac79 100644 --- a/web/components/explorer/app/app-explorer.tsx +++ b/web/components/explorer/app/app-explorer.tsx @@ -1,7 +1,9 @@ import ExtensionPreview from "@/components/extension/extension-preview"; import { EditorContext } from "@/components/providers/editor-context-provider"; +import { useScreenSize } from "@/lib/hooks/use-screen-size"; import { useTabViewManager } from "@/lib/hooks/use-tab-view-manager"; import { AppViewConfig } from "@/lib/types"; +import { Button } from "@heroui/react"; import { useContext } from "react"; import { v4 } from "uuid"; @@ -9,11 +11,19 @@ export default function AppExplorer() { const editorContext = useContext(EditorContext); const { createAppViewInCanvasView } = useTabViewManager(); + const { isLandscape } = useScreenSize(); const extensions = editorContext?.persistSettings?.extensions ?? []; const previews = extensions.map((ext, index) => ( -
+
{ + e.dataTransfer.setData("text/plain", JSON.stringify(ext)); + }} + > ({ + ...prev, + isSideMenuOpen: false, + })); + } }} />
)); return ( -
+

Tap or drag an extension to open it.

-
+
{previews}
+
); } diff --git a/web/components/explorer/file-system/fs-explorer.tsx b/web/components/explorer/file-system/fs-explorer.tsx index 4a75cdf..dbd5ce1 100644 --- a/web/components/explorer/file-system/fs-explorer.tsx +++ b/web/components/explorer/file-system/fs-explorer.tsx @@ -102,23 +102,7 @@ export default function FileSystemExplorer({ // Browse inside a project return ( -
- {/*
-
- { - const index = tabItems.findIndex( - (tab) => tab.name === item?.name, - ); - setSelectedTabIndex(index !== -1 ? index : 0); - }} - isClosable={false} - /> -
-
*/} - +
diff --git a/web/components/interface/command-viewer.tsx b/web/components/interface/command-viewer.tsx index d123dee..0801eee 100644 --- a/web/components/interface/command-viewer.tsx +++ b/web/components/interface/command-viewer.tsx @@ -1,4 +1,5 @@ import useCommands from "@/lib/hooks/use-commands"; +import usePlatformAIAssistant from "@/lib/hooks/use-platform-ai-assistant"; import { Command } from "@/lib/types"; import { addToast, @@ -10,11 +11,10 @@ import { Spinner, } from "@heroui/react"; import { useCallback, useContext, useEffect, useRef, useState } from "react"; -import { EditorContext } from "../providers/editor-context-provider"; -import Icon from "../misc/icon"; -import usePlatformAIAssistant from "@/lib/hooks/use-platform-ai-assistant"; import Markdown from "react-markdown"; import remarkGfm from "remark-gfm"; +import Icon from "../misc/icon"; +import { EditorContext } from "../providers/editor-context-provider"; const inputPlaceholders = [ "Type anything...", @@ -236,6 +236,14 @@ export default function CommandViewer() {
)} + {inputTextValue && ( + +
+ + Enter +
+
+ )}
+ ) + } + > + {command.commandInfo.name} + + ))} + +
+ )}
); diff --git a/web/components/interface/editor-toolbar.tsx b/web/components/interface/editor-toolbar.tsx index 5402ed2..05f9863 100644 --- a/web/components/interface/editor-toolbar.tsx +++ b/web/components/interface/editor-toolbar.tsx @@ -1,15 +1,15 @@ "use client"; -import { Button, Divider, Tooltip } from "@heroui/react"; -import { useContext, useState } from "react"; import Icon from "@/components/misc/icon"; import AppSettingsModal from "@/components/modals/app-settings-modal"; +import usePlatformAIAssistant from "@/lib/hooks/use-platform-ai-assistant"; +import useRecorder from "@/lib/hooks/use-recorder"; +import { Button, Divider, Tooltip } from "@heroui/react"; import { AnimatePresence, motion } from "framer-motion"; -import { EditorContext } from "../providers/editor-context-provider"; +import { useContext, useState } from "react"; import AgentConfigModal from "../modals/agent-config-modal"; import ExtensionMarketplaceModal from "../modals/extension-marketplace-modal"; -import usePlatformAIAssistant from "@/lib/hooks/use-platform-ai-assistant"; -import useRecorder from "@/lib/hooks/use-recorder"; +import { EditorContext } from "../providers/editor-context-provider"; export default function EditorToolbar() { const editorContext = useContext(EditorContext); @@ -18,7 +18,6 @@ export default function EditorToolbar() { const { isRecording, record } = useRecorder(); const [isAgentListModalOpen, setIsAgentListModalOpen] = useState(false); - const [isExtensionModalOpen, setIsExtensionModalOpen] = useState(false); const [isAppSettingsModalOpen, setAppIsSettingsModalOpen] = useState(false); function setIsOpen(val: boolean) { @@ -146,15 +145,23 @@ export default function EditorToolbar() { isIconOnly className="text-default-foreground h-8 w-8 min-w-8 px-1 py-1" onPress={() => { - setIsExtensionModalOpen(true); + editorContext?.setEditorStates((prev) => ({ + ...prev, + isMarketplaceOpen: true, + })); }} > + editorContext?.setEditorStates((prev) => ({ + ...prev, + isMarketplaceOpen: isOpen, + })) + } /> {/* */} diff --git a/web/components/interface/navigation/nav-side-menu.tsx b/web/components/interface/navigation/nav-side-menu.tsx index f9f356b..8872183 100644 --- a/web/components/interface/navigation/nav-side-menu.tsx +++ b/web/components/interface/navigation/nav-side-menu.tsx @@ -4,12 +4,12 @@ import Tabs from "@/components/misc/tabs"; import ProjectSettingsModal from "@/components/modals/project-settings-modal"; import { EditorContext } from "@/components/providers/editor-context-provider"; import useExplorer from "@/lib/hooks/use-explorer"; +import { useScreenSize } from "@/lib/hooks/use-screen-size"; import { isWeb } from "@/lib/platform-api/platform-checker"; import { TabItem } from "@/lib/types"; import { Button } from "@heroui/react"; import { AnimatePresence, motion } from "framer-motion"; import { useContext, useState } from "react"; -import { useMediaQuery } from "react-responsive"; import FileSystemExplorer from "../../explorer/file-system/fs-explorer"; import Icon from "../../misc/icon"; @@ -24,7 +24,7 @@ export default function NavSideMenu({ {isMenuOpen && ( -
+
)} + + {/* Environment Variables */} +

+ Environment Variables: +

+ {Object.entries(editorContext?.persistSettings?.envs ?? {}).length > + 0 && ( +
+ {Object.entries(editorContext?.persistSettings?.envs ?? {}).map( + ([key, value]) => ( +
+
{key}
+
+ {value} +
+ +
+ ), + )} +
+ )} + +
+ + + +
+
+ +
); @@ -812,8 +897,7 @@ function DevExtensionSettings({ const [devExtensionId, setDevExtensionId] = useState(""); const [devExtensionVersion, setDevExtensionVersion] = useState(""); - const { installExtension } = - useExtensionManager(); + const { installExtension } = useExtensionManager(); // Load installed extensions useEffect(() => { diff --git a/web/components/providers/imc-provider.tsx b/web/components/providers/imc-provider.tsx index a2f3b95..2698a5e 100644 --- a/web/components/providers/imc-provider.tsx +++ b/web/components/providers/imc-provider.tsx @@ -1,5 +1,20 @@ "use client"; +import { runAgentMethodLocal } from "@/lib/agent/agent-runner"; +import { getImageGenModel } from "@/lib/modalities/image-gen/image-gen"; +import { getLLMModel } from "@/lib/modalities/llm/llm"; +import { recognizeText } from "@/lib/modalities/ocr/ocr"; +import { getSTTModel } from "@/lib/modalities/stt/stt"; +import { getTTSModel } from "@/lib/modalities/tts/tts"; +import { + getDefaultImageModelConfig, + getDefaultLLMConfig, + getDefaultSTTConfig, + getDefaultTTSConfig, + getDefaultVideoModelConfig, +} from "@/lib/modalities/utils"; +import { getVideoGenModel } from "@/lib/modalities/video-gen/video-gen"; +import { getAPIKey } from "@/lib/settings/api-manager-utils"; import { IMCContextType } from "@/lib/types"; import { ImageModelConfig, @@ -13,21 +28,6 @@ import { } from "@pulse-editor/shared-utils"; import { createContext, useContext, useEffect, useState } from "react"; import { EditorContext } from "./editor-context-provider"; -import { getAPIKey } from "@/lib/settings/api-manager-utils"; -import { runAgentMethodLocal } from "@/lib/agent/agent-runner"; -import { getLLMModel } from "@/lib/modalities/llm/llm"; -import { getTTSModel } from "@/lib/modalities/tts/tts"; -import { getSTTModel } from "@/lib/modalities/stt/stt"; -import { recognizeText } from "@/lib/modalities/ocr/ocr"; -import { - getDefaultImageModelConfig, - getDefaultLLMConfig, - getDefaultSTTConfig, - getDefaultTTSConfig, - getDefaultVideoModelConfig, -} from "@/lib/modalities/utils"; -import { getImageGenModel } from "@/lib/modalities/image-gen/image-gen"; -import { getVideoGenModel } from "@/lib/modalities/video-gen/video-gen"; export const IMCContext = createContext(undefined); @@ -418,6 +418,16 @@ export default function InterModuleCommunicationProvider({ // const model = getMusicGenModel() }, ], + [ + IMCMessageTypeEnum.EditorGetEnv, + async ( + senderWindow: Window, + message: IMCMessage, + abortSignal?: AbortSignal, + ) => { + return editorContext?.persistSettings?.envs ?? {}; + }, + ], ]); return newMap; diff --git a/web/components/views/base/base-app-view.tsx b/web/components/views/base/base-app-view.tsx index 6b2aaa9..07b8c88 100644 --- a/web/components/views/base/base-app-view.tsx +++ b/web/components/views/base/base-app-view.tsx @@ -1,5 +1,6 @@ import Loading from "@/components/interface/loading"; import NotAuthorized from "@/components/interface/not-authorized"; +import { EditorContext } from "@/components/providers/editor-context-provider"; import { IMCContext } from "@/components/providers/imc-provider"; import useExtensionManager from "@/lib/hooks/use-extension-manager"; import { @@ -21,6 +22,7 @@ export default function BaseAppView({ config: AppViewConfig; viewId: string; }) { + const editorContext = useContext(EditorContext); const imcContext = useContext(IMCContext); const [noAccessToApp, setNoAccessToApp] = useState(false); @@ -78,9 +80,9 @@ export default function BaseAppView({ } // Download and load the extension app if specified - async function loadAppFromRegistry(appName: string, inviteCode?: string) { + async function loadAppFromRegistry(appId: string, inviteCode?: string) { const url = getAPIUrl(`/api/extension/get`); - url.searchParams.set("name", appName); + url.searchParams.set("name", appId); url.searchParams.set("latest", "true"); if (inviteCode) url.searchParams.set("inviteCode", inviteCode); @@ -93,8 +95,6 @@ export default function BaseAppView({ const fetchedExts: ExtensionMeta[] = await res.json(); - console.log("Fetched extensions:", fetchedExts); - const extensions: Extension[] = await Promise.all( fetchedExts.map(async (extMeta) => { // If backend does not provide mfVersion, try to load it from the manifest @@ -140,6 +140,14 @@ export default function BaseAppView({ return ext; } + async function loadAppFromCache(appId: string) { + const ext = editorContext?.persistSettings?.extensions?.find( + (ext) => ext.config.id === appId, + ); + + return ext; + } + async function installAndOpenApp(ext: Extension) { await installExtension( ext.remoteOrigin, @@ -153,11 +161,15 @@ export default function BaseAppView({ setPulseAppViewModel(viewModel); } - async function loadApp() { + async function openApp() { console.log("App query parameter:", config.app); if (!config.app) return; - else if ( + + const cachedExt = await loadAppFromCache(config.app); + if (cachedExt) { + await installAndOpenApp(cachedExt); + } else if ( config.app?.startsWith("http://") || config.app?.startsWith("https://") ) { @@ -174,7 +186,7 @@ export default function BaseAppView({ } } - loadApp(); + openApp(); }, [config]); return noAccessToApp ? ( diff --git a/web/components/views/layout/controls/canvas-node-control.tsx b/web/components/views/canvas/canvas-node-control.tsx similarity index 81% rename from web/components/views/layout/controls/canvas-node-control.tsx rename to web/components/views/canvas/canvas-node-control.tsx index ca23aad..4c72217 100644 --- a/web/components/views/layout/controls/canvas-node-control.tsx +++ b/web/components/views/canvas/canvas-node-control.tsx @@ -6,9 +6,13 @@ import { NodeResizeControl } from "@xyflow/react"; export default function CanvasNodeControl({ controlActions, setIsResizing, + isShowingWorkflowConnector, + setIsShowingWorkflowConnector, }: { controlActions: Record void) | undefined>; setIsResizing: (resizing: boolean) => void; + isShowingWorkflowConnector: boolean; + setIsShowingWorkflowConnector: (showing: boolean) => void; }) { return ( <> @@ -24,6 +28,19 @@ export default function CanvasNodeControl({ + +
{/* Popover is interfering with the drag area... */} diff --git a/web/components/views/layout/canvas-node-view-layout.tsx b/web/components/views/canvas/canvas-node-view-layout.tsx similarity index 80% rename from web/components/views/layout/canvas-node-view-layout.tsx rename to web/components/views/canvas/canvas-node-view-layout.tsx index 53e4094..8f08c1d 100644 --- a/web/components/views/layout/canvas-node-view-layout.tsx +++ b/web/components/views/canvas/canvas-node-view-layout.tsx @@ -2,7 +2,7 @@ import { isMobile } from "@/lib/platform-api/platform-checker"; import { Popover, PopoverContent, PopoverTrigger } from "@heroui/react"; import { NodeResizer } from "@xyflow/react"; import { useState } from "react"; -import CanvasNodeControl from "./controls/canvas-node-control"; +import CanvasNodeControl from "./canvas-node-control"; export default function CanvasNodeViewLayout({ height = "100%", @@ -17,6 +17,8 @@ export default function CanvasNodeViewLayout({ }) { const [isShowingMenu, setIsShowingMenu] = useState(false); const [isResizing, setIsResizing] = useState(false); + const [isShowingWorkflowConnector, setIsShowingWorkflowConnector] = + useState(false); return (
@@ -52,6 +56,16 @@ export default function CanvasNodeViewLayout({
+ {isShowingWorkflowConnector && ( + <> + {/* Input Area */} +
+ + {/* Output Area */} +
+ + )} + { const nodeProps = props as Node<{ config: AppViewConfig }>; diff --git a/web/components/views/canvas/nodes/backend-node.tsx b/web/components/views/canvas/nodes/backend-node.tsx index 4160941..7f8a120 100644 --- a/web/components/views/canvas/nodes/backend-node.tsx +++ b/web/components/views/canvas/nodes/backend-node.tsx @@ -2,7 +2,7 @@ import { AppViewConfig } from "@/lib/types"; import { Node } from "@xyflow/react"; import BaseAppView from "../../base/base-app-view"; import { memo } from "react"; -import CanvasNodeViewLayout from "../../layout/canvas-node-view-layout"; +import CanvasNodeViewLayout from "../canvas-node-view-layout"; /* Runs backend part of pulse app. */ const BackendNode = memo((props: any) => { diff --git a/web/components/views/console-panel/console-panel-view.tsx b/web/components/views/console-panel/console-panel-view.tsx index 9fb87f8..64d95e1 100644 --- a/web/components/views/console-panel/console-panel-view.tsx +++ b/web/components/views/console-panel/console-panel-view.tsx @@ -17,7 +17,7 @@ import { ExtensionTypeEnum, ViewModel } from "@pulse-editor/shared-utils"; import { AnimatePresence, motion } from "framer-motion"; import { v4 } from "uuid"; import SandboxAppLoader from "../../app-loaders/sandbox-app-loader"; -import AppViewLayout from "../layout/app-view-layout"; +import AppViewLayout from "../standalone-app/app-view-layout"; function ConsoleNavBar({ consoles, diff --git a/web/components/views/layout/controls/app-control.tsx b/web/components/views/standalone-app/app-control.tsx similarity index 100% rename from web/components/views/layout/controls/app-control.tsx rename to web/components/views/standalone-app/app-control.tsx diff --git a/web/components/views/layout/app-view-layout.tsx b/web/components/views/standalone-app/app-view-layout.tsx similarity index 96% rename from web/components/views/layout/app-view-layout.tsx rename to web/components/views/standalone-app/app-view-layout.tsx index 3119758..87c1f01 100644 --- a/web/components/views/layout/app-view-layout.tsx +++ b/web/components/views/standalone-app/app-view-layout.tsx @@ -1,6 +1,6 @@ import { Popover, PopoverContent, PopoverTrigger } from "@heroui/react"; import { useState } from "react"; -import AppControl from "./controls/app-control"; +import AppControl from "./app-control"; export default function AppViewLayout({ height = "100%", diff --git a/web/components/views/standalone-app/standalone-app-view.tsx b/web/components/views/standalone-app/standalone-app-view.tsx index 1944868..0cd8c8d 100644 --- a/web/components/views/standalone-app/standalone-app-view.tsx +++ b/web/components/views/standalone-app/standalone-app-view.tsx @@ -1,6 +1,6 @@ import { AppViewConfig } from "@/lib/types"; import BaseAppView from "../base/base-app-view"; -import AppViewLayout from "../layout/app-view-layout"; +import AppViewLayout from "./app-view-layout"; export default function StandaloneAppView({ config, diff --git a/web/components/views/view-area.tsx b/web/components/views/view-area.tsx index a3e0d3a..39d9013 100644 --- a/web/components/views/view-area.tsx +++ b/web/components/views/view-area.tsx @@ -1,5 +1,5 @@ import { useTabViewManager } from "@/lib/hooks/use-tab-view-manager"; -import { AppViewConfig, CanvasViewConfig } from "@/lib/types"; +import { AppViewConfig, CanvasViewConfig, Extension } from "@/lib/types"; import { ViewModeEnum } from "@pulse-editor/shared-utils"; import { ReactFlowProvider } from "@xyflow/react"; import { useSearchParams } from "next/navigation"; @@ -27,6 +27,7 @@ export default function ViewArea() { selectTab, closeTabView, createTabView, + createAppViewInCanvasView, deleteAppViewInCanvasView, activeTabView, } = useTabViewManager(); @@ -130,61 +131,83 @@ export default function ViewArea() { console.log("Tab views changed:", tabViews); }, [tabViews]); - if (tabViews.length === 0) { - return ; - } - - if (tabIndex < 0 || tabIndex >= tabViews.length) { - return
No view selected
; - } - return (
{ + e.preventDefault(); + }} + onDrop={(e) => { + e.preventDefault(); + const data = e.dataTransfer.getData("text/plain"); + console.log("Dropped item:", data); + const ext: Extension = JSON.parse(data); + const config: AppViewConfig = { + app: ext.config.id, + viewId: v4(), + recommendedHeight: ext.config.recommendedHeight, + recommendedWidth: ext.config.recommendedWidth, + }; + createAppViewInCanvasView(config); + }} > - {isShowTabs && ( -
- { - const index = tabItems.findIndex( - (tab) => tab.name === item?.name, - ); - selectTab(index !== -1 ? index : 0); - }} - isShowPagination={true} - onTabClose={(item) => { - const index = tabItems.findIndex( - (tab) => tab.name === item?.name, - ); - if (index !== -1) { - closeTabView(tabViews[index]); - } - }} - /> -
- )} -
- {tabViews.map((tabView, idx) => ( -
- {tabView.type === ViewModeEnum.App ? ( - + ) : tabIndex < 0 || tabIndex >= tabViews.length ? ( +
No view selected
+ ) : ( +
+ {isShowTabs && ( +
+ { + const index = tabItems.findIndex( + (tab) => tab.name === item?.name, + ); + selectTab(index !== -1 ? index : 0); + }} + isShowPagination={true} + onTabClose={(item) => { + const index = tabItems.findIndex( + (tab) => tab.name === item?.name, + ); + if (index !== -1) { + closeTabView(tabViews[index]); + } + }} /> - ) : tabView.type === ViewModeEnum.Canvas ? ( - - ) : ( -
Unknown view type
- )} +
+ )} +
+ {tabViews.map((tabView, idx) => ( +
+ {tabView.type === ViewModeEnum.App ? ( + + ) : tabView.type === ViewModeEnum.Canvas ? ( + + ) : ( +
Unknown view type
+ )} +
+ ))}
- ))} -
+
+ )}
); } diff --git a/web/lib/agent/agent-runner.ts b/web/lib/agent/agent-runner.ts index b342832..fea977c 100644 --- a/web/lib/agent/agent-runner.ts +++ b/web/lib/agent/agent-runner.ts @@ -1,3 +1,5 @@ +import { JsonOutputParser } from "@langchain/core/output_parsers"; +import { ChatPromptTemplate } from "@langchain/core/prompts"; import { Agent, AgentMethod, @@ -8,10 +10,8 @@ import { isArrayType, isObjectType, } from "@pulse-editor/shared-utils"; -import { getLLMModel } from "../modalities/llm/llm"; import toast from "react-hot-toast"; -import { ChatPromptTemplate } from "@langchain/core/prompts"; -import { JsonOutputParser } from "@langchain/core/output_parsers"; +import { getLLMModel } from "../modalities/llm/llm"; import { fetchAPI } from "../pulse-editor-website/backend"; import { parseJsonChunk } from "./stream-chunk-parser"; @@ -145,8 +145,8 @@ export async function getAgentPrompt( const userPromptTemplate = `\ ${method.prompt} -Finally, you must return a valid JSON object string. The requirements for the JSON object are as follows, -you must make sure the JSON string is parsable and valid: +Finally, you must return a valid JSON object string in the required format. The requirements for the JSON object are as follows, +you must make sure the JSON string is parse-able and valid: \`\`\` {{ ${Array.from(Object.entries(method.returns)).map( @@ -178,7 +178,7 @@ async function extractReturns(result: string): Promise> { function getReturnVariablePrompt(variable: TypedVariable) { const typePrompt = getReturnVariableTypePrompt(variable.type); - return `(Return type: ${typePrompt}. Description: ${variable.description}.)`; + return `(Return type: ${typePrompt} Description: ${variable.description}.)`; } function getReturnVariableTypePrompt(type: TypedVariableType): string { @@ -196,11 +196,11 @@ function getReturnVariableTypePrompt(type: TypedVariableType): string { ); const typePrompt = `An object with the following properties: -\`\`\` +""" {{ ${properties.join(", ")} }} -\`\`\` +""" `; return typePrompt; diff --git a/web/lib/agent/built-in-agents/editor-assistant.ts b/web/lib/agent/built-in-agents/editor-assistant.ts index 7c36725..ecf69f8 100644 --- a/web/lib/agent/built-in-agents/editor-assistant.ts +++ b/web/lib/agent/built-in-agents/editor-assistant.ts @@ -16,15 +16,11 @@ editor extension to edit the video, and finally using a voice generator extensio 4. Assist users in troubleshooting common issues for the editor itself. If a user asks anything about an extension, you should suggest the user to visit the extension's marketplace page and/or contact the extension developer. +5. You must return your answer in the required format. You will receive a message from user, which may contain a question or a request for assistance. Your task is to provide a helpful response based on the user's input. -Remember, you will not directly assist the user in creating or editing content, as you are not -an application-level assistant agent. Instead, you will provide information and guidance to help users -to navigate the platform and its extensions. - - Knowledge about Pulse Editor: \`\`\` Pulse Editor is a modular, extensible, cross-platform, AI-powered creativity platform @@ -258,15 +254,15 @@ Project directory tree: description: `The arguments that you suggested for user to run the command from the extension. \ This must match the command's parameters provided earlier.`, }, - suggestedViewId: { - type: "string", - description: - "The ID of the view (usually a uuid) that you suggest the user to run the command on. \ -Note, this is not the same as the module/extension ID (usually a named ID). In order to suggest a view ID, \ -you must match the commands' module ID (named ID) with the opened views' extensionConfig.id (named ID). \ -If a command and an opened view has the same extension/module ID (named ID), you can use that \ -opened view's ID (uuid) as the suggested view ID.", - }, + // suggestedViewId: { + // type: "string", + // description: + // "The ID of the view (usually a uuid) that you suggest the user to run the command on. \ + // Note, this is not the same as the module/extension ID (usually a named ID). In order to suggest a view ID, \ + // you must match the commands' module ID (named ID) with the opened views' extensionConfig.id (named ID). \ + // If a command and an opened view has the same extension/module ID (named ID), you can use that \ + // opened view's ID (uuid) as the suggested view ID.", + // }, response: { type: "string", description: `The platform-level assistant agent's response to the user's input or question. \ diff --git a/web/lib/agent/stream-chunk-parser.ts b/web/lib/agent/stream-chunk-parser.ts index a364254..358846a 100644 --- a/web/lib/agent/stream-chunk-parser.ts +++ b/web/lib/agent/stream-chunk-parser.ts @@ -1,21 +1,21 @@ export function parseJsonChunk(chunk: string) { const jsonObjects = []; - // Replace multiple spaces or other delimiters with a single space and trim - const cleanedChunk = chunk.trim(); + let braceCount = 0; + let current = ""; - // Use a regular expression to match JSON objects - // This assumes objects are separated by spaces and are valid JSON - const jsonRegex = /({[^{}]*})/g; - const matches = cleanedChunk.match(jsonRegex); + for (const char of chunk.trim()) { + if (char === "{") braceCount++; + if (char === "}") braceCount--; - if (matches) { - for (const match of matches) { + current += char; + + if (braceCount === 0 && current.trim()) { try { - const parsed = JSON.parse(match); - jsonObjects.push(parsed); - } catch (error) { - console.error("Error parsing JSON object:", match, error); + jsonObjects.push(JSON.parse(current)); + } catch (e) { + console.error("Error parsing JSON object:", current, e); } + current = ""; } } diff --git a/web/lib/hooks/use-commands.ts b/web/lib/hooks/use-commands.ts index 5de9584..5d04b6d 100644 --- a/web/lib/hooks/use-commands.ts +++ b/web/lib/hooks/use-commands.ts @@ -1,22 +1,22 @@ +import { EditorContext } from "@/components/providers/editor-context-provider"; import { IMCContext } from "@/components/providers/imc-provider"; import { CommandDefinition, IMCMessageTypeEnum, ViewModeEnum, } from "@pulse-editor/shared-utils"; -import { use, useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import { v4 } from "uuid"; import { AppViewConfig, - Command, CanvasViewConfig, + Command, Extension, TabView, } from "../types"; -import { EditorContext } from "@/components/providers/editor-context-provider"; -import { useTabViewManager } from "./use-tab-view-manager"; -import toast from "react-hot-toast"; -import { v4 } from "uuid"; import { useMenuActions } from "./use-menu-actions"; +import { useTabViewManager } from "./use-tab-view-manager"; /** * Use commands in active tab. @@ -182,7 +182,7 @@ export default function useCommands() { }); } - imcContext?.polyIMC?.sendMessage( + return await imcContext?.polyIMC?.sendMessage( ext.config.id + "-" + viewId, IMCMessageTypeEnum.EditorRunExtCommand, { name: command.commandInfo.name, args }, @@ -193,7 +193,7 @@ export default function useCommands() { throw new Error("View ID is required for view commands"); } - imcContext?.polyIMC?.sendMessage( + return await imcContext?.polyIMC?.sendMessage( command.viewId, IMCMessageTypeEnum.EditorRunExtCommand, { name: command.commandInfo.name, args }, diff --git a/web/lib/hooks/use-platform-ai-assistant.ts b/web/lib/hooks/use-platform-ai-assistant.ts index a822bde..ba270ce 100644 --- a/web/lib/hooks/use-platform-ai-assistant.ts +++ b/web/lib/hooks/use-platform-ai-assistant.ts @@ -1,16 +1,13 @@ -import { useContext, useEffect, useState } from "react"; -import useSpeech2Speech from "./use-speech2speech"; -import useTTS from "./use-tts"; import { EditorContext } from "@/components/providers/editor-context-provider"; -import { getAPIKey } from "@/lib/settings/api-manager-utils"; -import { editorAssistantAgent } from "@/lib/agent/built-in-agents/editor-assistant"; import { getAgentLLMConfig, - getAgentPrompt, runAgentMethodCloud, runAgentMethodLocal, } from "@/lib/agent/agent-runner"; +import { editorAssistantAgent } from "@/lib/agent/built-in-agents/editor-assistant"; +import { getDefaultLLMConfig } from "@/lib/modalities/utils"; import { getPlatform } from "@/lib/platform-api/platform-checker"; +import { getAPIKey } from "@/lib/settings/api-manager-utils"; import { AppViewConfig, CanvasViewConfig, @@ -18,14 +15,14 @@ import { PlatformEnum, TabView, } from "@/lib/types"; +import { ViewModeEnum } from "@pulse-editor/shared-utils"; +import { useContext, useEffect, useState } from "react"; import toast from "react-hot-toast"; -import { getDefaultLLMConfig } from "@/lib/modalities/utils"; -import { usePlatformApi } from "./use-platform-api"; import useCommands from "./use-commands"; +import { usePlatformApi } from "./use-platform-api"; +import useSpeech2Speech from "./use-speech2speech"; import { useTabViewManager } from "./use-tab-view-manager"; -import { ViewModeEnum } from "@pulse-editor/shared-utils"; -import { fetchAPI } from "../pulse-editor-website/backend"; -import { parseJsonChunk } from "../agent/stream-chunk-parser"; +import useTTS from "./use-tts"; export default function usePlatformAIAssistant() { const editorContext = useContext(EditorContext); @@ -103,6 +100,8 @@ export default function usePlatformAIAssistant() { response: string; } = assistantResult; + console.log("Assistant result:", assistantResult); + // TODO: The agent needs to confirm the command with the user // TODO: before executing it. const args = suggestedArgs.reduce( @@ -120,8 +119,6 @@ export default function usePlatformAIAssistant() { thinkingText: "Executing command...", })); - console.log("Assistant result:", assistantResult); - const command = commands.find( (cmd) => cmd.commandInfo.name === suggestedCmd, ); diff --git a/web/lib/hooks/use-screen-size.ts b/web/lib/hooks/use-screen-size.ts new file mode 100644 index 0000000..aa603fc --- /dev/null +++ b/web/lib/hooks/use-screen-size.ts @@ -0,0 +1,8 @@ +import { useMediaQuery } from "react-responsive"; + +export function useScreenSize() { + const isLandscape = useMediaQuery({ + query: "(min-width: 768px)", + }); + return { isLandscape }; +} diff --git a/web/lib/hooks/use-tab-view-manager.ts b/web/lib/hooks/use-tab-view-manager.ts index 26538ec..5a3281a 100644 --- a/web/lib/hooks/use-tab-view-manager.ts +++ b/web/lib/hooks/use-tab-view-manager.ts @@ -277,11 +277,12 @@ export function useTabViewManager() { currentTab = await createTabView(ViewModeEnum.Canvas, { viewId: `canvas-${v4()}`, } as CanvasViewConfig); + } else if (currentTab?.type !== ViewModeEnum.Canvas) { + currentTab = await createTabView(ViewModeEnum.Canvas, { + viewId: `canvas-${v4()}`, + } as CanvasViewConfig); } - if (currentTab?.type !== ViewModeEnum.Canvas) { - throw new Error("Current tab is not a canvas"); - } const newCanvasConfig: CanvasViewConfig = { ...currentTab.config, nodes: [ diff --git a/web/lib/platform-api/platform-checker.ts b/web/lib/platform-api/platform-checker.ts index a78c4f9..5ed24da 100644 --- a/web/lib/platform-api/platform-checker.ts +++ b/web/lib/platform-api/platform-checker.ts @@ -1,6 +1,6 @@ +import { PlatformEnum } from "@/lib/types"; import { Capacitor } from "@capacitor/core"; import isElectron from "is-electron"; -import { PlatformEnum } from "@/lib/types"; export function getPlatform() { // Capacitor diff --git a/web/lib/types.ts b/web/lib/types.ts index 3cb1946..04de537 100644 --- a/web/lib/types.ts +++ b/web/lib/types.ts @@ -83,6 +83,7 @@ export type EditorStates = { // Side menu panel isSideMenuOpen?: boolean; + isMarketplaceOpen?: boolean; }; /** @@ -129,6 +130,9 @@ export type PersistentSettings = { mobileHost?: string; isUseManagedCloud?: boolean; + + // Environment variables + envs?: Record; }; // #endregion diff --git a/web/package.json b/web/package.json index 22beaea..d8ec892 100644 --- a/web/package.json +++ b/web/package.json @@ -8,7 +8,7 @@ "build": "next build", "lint": "next lint", "start": "serve ../build/next", - "dev-https": "next dev --experimental-https" + "dev-https": "next dev --experimental-https" }, "dependencies": { "@capacitor-community/safe-area": "^7.0.0-alpha.1",