diff --git a/.github/workflows/deploy-lowcoder-sdk-webpack-netlify.yml b/.github/workflows/deploy-lowcoder-sdk-webpack-netlify.yml new file mode 100644 index 000000000..13f64a674 --- /dev/null +++ b/.github/workflows/deploy-lowcoder-sdk-webpack-netlify.yml @@ -0,0 +1,55 @@ +# Builds client/packages/lowcoder-sdk-webpack-bundle and deploys its dist/ folder to Netlify. +# +# Deploy uses --no-build so Netlify CLI does not run the site UI "build command" (e.g. expo). +# The webpack bundle is built in the prior CI step. +# +# Repository secrets (Netlify: Site settings → General → Site details → Site ID; +# User settings → Applications → Personal access tokens): +# NETLIFY_AUTH_TOKEN — Netlify personal access token +# NETLIFY_SITE_ID — Site API ID for the Netlify site + +name: Deploy SDK Webpack Bundle to Netlify + +on: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: deploy-sdk-webpack-netlify-${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 20.x + cache: yarn + cache-dependency-path: client/yarn.lock + + - name: Install dependencies + uses: borales/actions-yarn@v4.2.0 + with: + cmd: install + dir: client + + - name: Build lowcoder-sdk-webpack-bundle + uses: borales/actions-yarn@v4.2.0 + with: + cmd: workspace lowcoder-sdk-webpack-bundle build + dir: client + + - name: Deploy dist to Netlify + working-directory: client/packages/lowcoder-sdk-webpack-bundle + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SDK_SITE_ID }} + run: npx --yes netlify-cli deploy --prod --dir=dist --no-build diff --git a/client/packages/lowcoder/src/components/ThemeSettingsSelector.tsx b/client/packages/lowcoder/src/components/ThemeSettingsSelector.tsx index f2004d43a..67cbb9c20 100644 --- a/client/packages/lowcoder/src/components/ThemeSettingsSelector.tsx +++ b/client/packages/lowcoder/src/components/ThemeSettingsSelector.tsx @@ -254,7 +254,7 @@ export default function ThemeSettingsSelector(props: ColorConfigProps) { }; const gridPaddingInputBlur = (padding: string) => { - let result = 20; + let result = 0; if (padding !== '') { result = Number(padding); } diff --git a/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx b/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx index 64122daba..b38260eed 100644 --- a/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx @@ -219,8 +219,8 @@ const childrenMap = { gridColumns: RangeControl.closed(1, 48, 24), gridRowHeight: RangeControl.closed(4, 100, 8), gridRowCount: withDefault(NumberControl, DEFAULT_ROW_COUNT), - gridPaddingX: withDefault(NumberControl, 20), - gridPaddingY: withDefault(NumberControl, 20), + gridPaddingX: withDefault(NumberControl, 0), + gridPaddingY: withDefault(NumberControl, 0), gridBg: ColorControl, gridBgImage: StringControl, gridBgImageRepeat: StringControl, @@ -342,6 +342,10 @@ function AppGeneralSettingsModal(props: ChildrenInstance) { function AppCanvasSettingsModal(props: ChildrenInstance) { const isPublicApp = useSelector(isPublicApplication); + const application = useSelector(currentApplication); + const isAggregation = !!application && isAggregationApp( + AppUILayoutType[application.applicationType] + ); const { themeList, defaultTheme, @@ -397,7 +401,7 @@ function AppCanvasSettingsModal(props: ChildrenInstance) { return ( <> - {maxWidth.propertyView({ + {!isAggregation && maxWidth.propertyView({ dropdownLabel: trans("appSetting.canvasMaxWidth"), inputLabel: trans("appSetting.userDefinedMaxWidth"), inputPlaceholder: trans("appSetting.inputUserDefinedPxValue"), @@ -462,25 +466,25 @@ function AppCanvasSettingsModal(props: ChildrenInstance) { min: 350, lastNode: {trans("appSetting.maxWidthTip")}, })} - {gridColumns.propertyView({ + {!isAggregation && gridColumns.propertyView({ label: trans("appSetting.gridColumns"), placeholder: '24', })} - {gridRowHeight.propertyView({ + {!isAggregation && gridRowHeight.propertyView({ label: trans("appSetting.gridRowHeight"), placeholder: '8', })} - {gridRowCount.propertyView({ + {!isAggregation && gridRowCount.propertyView({ label: trans("appSetting.gridRowCount"), placeholder: 'Infinity', })} {gridPaddingX.propertyView({ label: trans("appSetting.gridPaddingX"), - placeholder: '20', + placeholder: '0', })} {gridPaddingY.propertyView({ label: trans("appSetting.gridPaddingY"), - placeholder: '20', + placeholder: '0', })} {gridBg.propertyView({ label: trans("style.background"), diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx index 39de2e739..bce90b54a 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx @@ -14,7 +14,7 @@ import { ChatProvider } from "./components/context/ChatContext"; import { ChatPropertyView } from "./chatPropertyView"; import { createChatStorage } from "./utils/storageFactory"; import { QueryHandler } from "./handlers/messageHandlers"; -import { useMemo, useRef, useEffect } from "react"; +import { useMemo, useRef } from "react"; import { changeChildAction } from "lowcoder-core"; import { ChatMessage } from "./types/chatTypes"; import { trans } from "i18n"; @@ -249,16 +249,6 @@ const ChatTmpComp = new UICompBuilder( } }; - // Cleanup on unmount - useEffect(() => { - return () => { - const tableName = uniqueTableName.current; - if (tableName) { - storage.cleanup(); - } - }; - }, []); - // custom styles const styles = { style: props.style, diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainerStyles.ts b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainerStyles.ts index 1f2d4580d..a16e67954 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainerStyles.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainerStyles.ts @@ -20,6 +20,8 @@ export const StyledChatContainer = styled.div` display: flex; height: ${(props) => (props.$autoHeight ? "auto" : "100%")}; min-height: ${(props) => (props.$autoHeight ? "300px" : "unset")}; + min-width: 0; + overflow: hidden; /* Main container styles */ background: ${(props) => props.style?.background || "transparent"}; @@ -43,6 +45,8 @@ export const StyledChatContainer = styled.div` width: ${(props) => props.$sidebarWidth || "250px"}; background-color: ${(props) => props.$sidebarStyle?.sidebarBackground || "#fff"}; padding: 10px; + min-height: 0; + overflow-y: auto; } .aui-thread-list-item-title { @@ -51,9 +55,16 @@ export const StyledChatContainer = styled.div` /* Messages Window Styles */ .aui-thread-root { - flex: 1; + flex: 1 1 auto; + min-width: 0; + min-height: 0; background-color: ${(props) => props.$messagesStyle?.messagesBackground || "#f9fafb"}; - height: auto; + height: 100%; + overflow: hidden; + } + + .aui-thread-viewport { + min-height: 0; } /* User Message Styles */ diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx index f4823011e..b52359995 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx @@ -1,46 +1,49 @@ // client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx -import { useMemo, useEffect } from "react"; +import { useMemo, useContext, useRef, useEffect } from "react"; import { ChatPanelContainer } from "./ChatPanelContainer"; import { createChatStorage } from "../utils/storageFactory"; -import { N8NHandler } from "../handlers/messageHandlers"; +import { AIAssistantQueryHandler } from "../handlers/messageHandlers"; import { ChatPanelProps } from "../types/chatTypes"; -import { trans } from "i18n"; +import { EditorContext } from "@lowcoder-ee/comps/editorState"; import "@assistant-ui/styles/index.css"; import "@assistant-ui/styles/markdown.css"; // ============================================================================ -// CHAT PANEL - SIMPLIFIED BOTTOM PANEL (NO STYLING CONTROLS) +// CHAT PANEL - SIMPLIFIED BOTTOM PANEL (QUERY-BASED + AUTOMATOR) +// ---------------------------------------------------------------------------- +// We capture the EditorState in a ref so the message handler always reads +// the *latest* canvas snapshot at send-time (instead of being frozen at +// mount time, which would defeat the whole point of context awareness). // ============================================================================ -export function ChatPanel({ - tableName, - modelHost, - systemPrompt = trans("chat.defaultSystemPrompt"), - streaming = true, - onMessageUpdate -}: ChatPanelProps) { +export function ChatPanel({ + tableName, + chatQuery, + onMessageUpdate, +}: ChatPanelProps) { + const editorState = useContext(EditorContext); + const editorStateRef = useRef(editorState); + + useEffect(() => { + editorStateRef.current = editorState; + }, [editorState]); + const storage = useMemo(() => createChatStorage(tableName), [tableName] ); - const messageHandler = useMemo(() => - new N8NHandler({ - modelHost, - systemPrompt, - streaming - }), - [modelHost, systemPrompt, streaming] - ); - - // Cleanup on unmount - delete chat data from storage - useEffect(() => { - return () => { - storage.cleanup(); - }; - }, [storage]); + const messageHandler = useMemo( + () => + new AIAssistantQueryHandler({ + chatQuery, + dispatch: editorState?.rootComp?.dispatch, + getEditorState: () => editorStateRef.current, + }), + [chatQuery, editorState?.rootComp?.dispatch] + ); return ( ); -} \ No newline at end of file +} diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx index 9f0766cea..83b1028e1 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx @@ -18,7 +18,7 @@ import { RegularThreadData, ArchivedThreadData } from "./context/ChatContext"; -import { MessageHandler, ChatMessage } from "../types/chatTypes"; +import { AIAssistantMessageHandler, ChatMessage } from "../types/chatTypes"; import styled from "styled-components"; import { trans } from "i18n"; import { TooltipProvider } from "@radix-ui/react-tooltip"; @@ -26,9 +26,86 @@ import { TooltipProvider } from "@radix-ui/react-tooltip"; import "@assistant-ui/styles/index.css"; import "@assistant-ui/styles/markdown.css"; import { EditorContext } from "@lowcoder-ee/comps/editorState"; +import { ActionConfig, ActionExecuteParams } from "../../preLoadComp/types"; import { configureComponentAction } from "../../preLoadComp/actions/componentConfiguration"; -import { addComponentAction, moveComponentAction, nestComponentAction, resizeComponentAction } from "../../preLoadComp/actions/componentManagement"; -import { applyThemeAction, configureAppMetaAction, setCanvasSettingsAction } from "../../preLoadComp/actions/appConfiguration"; +import { + addComponentAction, + moveComponentAction, + nestComponentAction, + resizeComponentAction, + deleteComponentAction, + renameComponentAction, +} from "../../preLoadComp/actions/componentManagement"; +import { + applyThemeAction, + configureAppMetaAction, + setCanvasSettingsAction, + applyGlobalJSAction, + applyCSSAction, + publishAppAction, +} from "../../preLoadComp/actions/appConfiguration"; +import { applyStyleAction } from "../../preLoadComp/actions/componentStyling"; +import { addEventHandlerAction } from "../../preLoadComp/actions/componentEvents"; +import { alignComponentAction } from "../../preLoadComp/actions/componentLayout"; + +// ============================================================================ +// ACTION REGISTRY — maps LLM action names to their executor configs. +// Adding a new action is one line here + one entry in actionsCatalog.ts. +// ============================================================================ + +const ACTION_REGISTRY: Record = { + place_component: addComponentAction, + nest_component: nestComponentAction, + move_component: moveComponentAction, + resize_component: resizeComponentAction, + delete_component: deleteComponentAction, + rename_component: renameComponentAction, + set_properties: configureComponentAction, + set_style: applyStyleAction, + set_theme: applyThemeAction, + set_app_metadata: configureAppMetaAction, + set_canvas_setting: setCanvasSettingsAction, + set_global_javascript: applyGlobalJSAction, + set_global_css: applyCSSAction, + publish_app: publishAppAction, + add_event_handler: addEventHandlerAction, + align_component: alignComponentAction, +}; + +/** + * Translate an LLM action object into the ActionExecuteParams shape that + * the legacy executor functions expect. Centralises the field-mapping so + * each executor doesn't need to know about the automator format. + */ +function buildExecuteParams( + actionItem: Record, + editorState: any +): ActionExecuteParams { + const ap = actionItem.action_parameters || {}; + + let actionValue = ""; + switch (actionItem.action) { + case "rename_component": actionValue = ap.new_name || ""; break; + case "set_style": actionValue = JSON.stringify(ap); break; + case "align_component": actionValue = ap.alignment || "center"; break; + case "add_event_handler": actionValue = `${ap.event || "click"}: ${ap.action_type || "message"}`; break; + case "set_global_javascript": actionValue = ap.code || ""; break; + case "set_global_css": actionValue = ap.code || ""; break; + } + + return { + actionKey: actionItem.action, + actionValue, + actionPayload: actionItem, + selectedComponent: actionItem.component || null, + selectedEditorComponent: actionItem.component_name || null, + selectedNestComponent: null, + editorState, + selectedDynamicLayoutIndex: null, + selectedTheme: null, + selectedCustomShortcutAction: null, + }; +} // ============================================================================ // STYLED CONTAINER - SIMPLE FIXED STYLING FOR BOTTOM PANEL @@ -41,6 +118,8 @@ const StyledChatContainer = styled.div<{ display: flex; height: ${(props) => (props.autoHeight ? "auto" : "100%")}; min-height: ${(props) => (props.autoHeight ? "300px" : "unset")}; + min-width: 0; + overflow: hidden; p { margin: 0; @@ -50,12 +129,21 @@ const StyledChatContainer = styled.div<{ width: ${(props) => props.sidebarWidth || "250px"}; background-color: #fff; padding: 10px; + min-height: 0; + overflow-y: auto; } .aui-thread-root { - flex: 1; + flex: 1 1 auto; + min-width: 0; + min-height: 0; background-color: #f9fafb; - height: auto; + height: 100%; + overflow: hidden; + } + + .aui-thread-viewport { + min-height: 0; } .aui-thread-list-item { @@ -77,7 +165,7 @@ const generateId = () => Math.random().toString(36).substr(2, 9); export interface ChatPanelContainerProps { storage: any; - messageHandler: MessageHandler; + messageHandler: AIAssistantMessageHandler; placeholder?: string; onMessageUpdate?: (message: string) => void; } @@ -98,139 +186,30 @@ function ChatPanelView({ messageHandler, placeholder, onMessageUpdate }: Omit { if (!editorStateRef.current) { - console.error("No editorStateRef found"); - return; - } - - const comp = editorStateRef.current.getUIComp().children.comp; - if (!comp) { - console.error("No comp found"); + console.error("[Automator] no editorState — skipping actions"); return; } - // const layout = comp.children.layout.getView(); - // console.log("LAYOUT", layout); - + + console.log(`[Automator] executing ${actions.length} action(s)`); + let executed = 0; + for (const actionItem of actions) { - const { action, component, ...action_payload } = actionItem; - - switch (action) { - case "place_component": - await addComponentAction.execute({ - actionKey: action, - actionValue: "", - actionPayload: action_payload, - selectedComponent: component, - selectedEditorComponent: null, - selectedNestComponent: null, - editorState: editorStateRef.current, - selectedDynamicLayoutIndex: null, - selectedTheme: null, - selectedCustomShortcutAction: null - }); - break; - case "nest_component": - await nestComponentAction.execute({ - actionKey: action, - actionValue: "", - actionPayload: action_payload, - selectedComponent: component, - selectedEditorComponent: null, - selectedNestComponent: null, - editorState: editorStateRef.current, - selectedDynamicLayoutIndex: null, - selectedTheme: null, - selectedCustomShortcutAction: null - }); - break; - case "move_component": - await moveComponentAction.execute({ - actionKey: action, - actionValue: "", - actionPayload: action_payload, - selectedComponent: component, - selectedEditorComponent: null, - selectedNestComponent: null, - editorState: editorStateRef.current, - selectedDynamicLayoutIndex: null, - selectedTheme: null, - selectedCustomShortcutAction: null - }); - break; - case "resize_component": - await resizeComponentAction.execute({ - actionKey: action, - actionValue: "", - actionPayload: action_payload, - selectedComponent: component, - selectedEditorComponent: null, - selectedNestComponent: null, - editorState: editorStateRef.current, - selectedDynamicLayoutIndex: null, - selectedTheme: null, - selectedCustomShortcutAction: null - }); - break; - case "set_properties": - await configureComponentAction.execute({ - actionKey: action, - actionValue: component, - actionPayload: action_payload, - selectedEditorComponent: null, - selectedComponent: null, - selectedNestComponent: null, - editorState: editorStateRef.current, - selectedDynamicLayoutIndex: null, - selectedTheme: null, - selectedCustomShortcutAction: null - }); - break; - case "set_theme": - await applyThemeAction.execute({ - actionKey: action, - actionValue: component, - actionPayload: action_payload, - selectedEditorComponent: null, - selectedComponent: null, - selectedNestComponent: null, - editorState: editorStateRef.current, - selectedDynamicLayoutIndex: null, - selectedTheme: null, - selectedCustomShortcutAction: null - }); - break; - case "set_app_metadata": - await configureAppMetaAction.execute({ - actionKey: action, - actionValue: component, - actionPayload: action_payload, - selectedEditorComponent: null, - selectedComponent: null, - selectedNestComponent: null, - editorState: editorStateRef.current, - selectedDynamicLayoutIndex: null, - selectedTheme: null, - selectedCustomShortcutAction: null - }); - break; - case "set_canvas_setting": - await setCanvasSettingsAction.execute({ - actionKey: action, - actionValue: component, - actionPayload: action_payload, - selectedEditorComponent: null, - selectedComponent: null, - selectedNestComponent: null, - editorState: editorStateRef.current, - selectedDynamicLayoutIndex: null, - selectedTheme: null, - selectedCustomShortcutAction: null - }); - break; - default: - break; + const executor = ACTION_REGISTRY[actionItem.action]; + if (!executor) { + console.warn(`[Automator] unsupported action: ${actionItem.action}`); + continue; } - await new Promise(resolve => setTimeout(resolve, 1000)); + try { + const params = buildExecuteParams(actionItem, editorStateRef.current); + await executor.execute(params); + executed++; + } catch (err) { + console.error(`[Automator] action "${actionItem.action}" failed:`, err); + } + await new Promise((r) => setTimeout(r, 200)); } + + console.log(`[Automator] done: ${executed}/${actions.length} succeeded`); }; const convertMessage = (message: ChatMessage): ThreadMessageLike => { @@ -262,15 +241,21 @@ function ChatPanelView({ messageHandler, placeholder, onMessageUpdate }: Omit; + [key: string]: unknown; +} + +function normalizeAutomatorQueryResponse(result: any): MessageResponse { + const raw = result; + + if (!raw || typeof raw !== "object") { + throw new Error("Automator query must return an object with content and actions"); + } + + if (typeof raw.content !== "string") { + throw new Error("Automator query response must include string content"); + } + + const actions: AutomatorAction[] = []; + let invalidActionCount = 0; + + if (!Array.isArray(raw.actions)) { + throw new Error("Automator query response must include an actions array"); + } + + for (const action of raw.actions) { + if (action && typeof action === "object" && typeof action.action === "string") { + actions.push(action as AutomatorAction); + } else { + invalidActionCount++; + } + } + + return { + content: raw.content, + actions, + metadata: raw.metadata, + automator: { + isStructured: true, + explanation: raw.content, + invalidActionCount, + }, + }; +} + +function buildAutomatorQueryArgs( + message: ChatMessage, + sessionId: string | undefined, + conversationHistory: ChatMessage[], + payload: ReturnType, + messagesWithoutSystem: Array<{ role: ChatMessage["role"]; content: string }> +) { + return { + automator: { + value: { + ...payload, + message, + prompt: message.text, + sessionId, + conversationHistory, + messagesWithoutSystem, + } + }, + }; +} // ============================================================================ -// N8N HANDLER (for Bottom Panel) -// ============================================================================ - -export class N8NHandler implements MessageHandler { - constructor(private config: N8NHandlerConfig) {} - - async sendMessage(message: ChatMessage, sessionId?: string): Promise { - const { modelHost, systemPrompt, streaming } = this.config; - - if (!modelHost) { - throw new Error("Model host is required for N8N calls"); - } - - try { - const response = await fetch(modelHost, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - sessionId, - message: message.text, - systemPrompt: systemPrompt || "You are a helpful assistant.", - streaming: streaming || false - }) - }); - - if (!response.ok) { - throw new Error(`N8N call failed: ${response.status} ${response.statusText}`); - } - - const data = await response.json(); - if (data.output) { - const { explanation, actions } = JSON.parse(data.output); - return { content: explanation, actions }; - } - // Extract content from various possible response formats - const content = data.response || data.message || data.content || data.text || String(data); - - return { content }; - } catch (error) { - throw new Error(`N8N call failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - } - } -} - -// ============================================================================ -// QUERY HANDLER (for Canvas Components) +// QUERY HANDLER // ============================================================================ export class QueryHandler implements MessageHandler { constructor(private config: QueryHandlerConfig) {} - async sendMessage(message: ChatMessage, sessionId?: string): Promise { - const { chatQuery, dispatch} = this.config; - - // If no query selected or dispatch unavailable, return mock response - if (!chatQuery || !dispatch) { - await new Promise((res) => setTimeout(res, 500)); - return { content: "(mock) You typed: " + message.text }; - } + async sendMessage(message: ChatMessage): Promise { + const { chatQuery, dispatch} = this.config; + + if (!chatQuery) { + throw new Error("Select a query before sending a message"); + } + + if (!dispatch) { + throw new Error("Query dispatch is unavailable"); + } try { + console.log("Executing query:", chatQuery); const result: any = await getPromiseAfterDispatch( dispatch, routeByNameAction( chatQuery, executeQueryAction({ - // Pass the full message object so attachments are available in queries - args: { - message: { value: message }, // Full ChatMessage object with attachments - prompt: { value: message.text }, // Keep backward compatibility - }, + // Pass the full message object so attachments are available in queries + args: { + message: { value: message }, + prompt: { value: message.text }, + }, }) ) ); - + console.log("Query result:", result); return result.message } catch (e: any) { throw new Error(e?.message || "Query execution failed"); @@ -90,37 +116,102 @@ export class QueryHandler implements MessageHandler { } // ============================================================================ -// MOCK HANDLER (for testing/fallbacks) -// ============================================================================ +// AI ASSISTANT QUERY HANDLER (bottom panel) +// ---------------------------------------------------------------------------- +// This handler owns the Lowcoder side of the Automator flow: +// 1. snapshot the current editor state, +// 2. build the system prompt, tools, catalogs, and live context, +// 3. pass that payload to the selected user query, +// 4. accept the query's normalized `{ content, actions }` result. +// +// Provider-specific parsing belongs in the selected query/backend bridge. +// ============================================================================ + +export class AIAssistantQueryHandler implements AIAssistantMessageHandler { + constructor(private config: QueryHandlerConfig) {} -export class MockHandler implements MessageHandler { - constructor(private delay: number = 1000) {} + async sendMessage( + message: ChatMessage, + sessionId: string | undefined, + conversationHistory: ChatMessage[] + ): Promise { + const { chatQuery, dispatch, getEditorState } = this.config; + const history = conversationHistory; + + // Conversation history in the OpenAI {role, content} shape. + const rawHistory = history.map((msg) => ({ + role: msg.role, + content: msg.text, + })); + + if (!chatQuery) { + throw new Error("Select an Automator query before sending a message"); + } + + if (!dispatch) { + throw new Error("Automator dispatch is unavailable"); + } + + if (!getEditorState) { + throw new Error("Automator editor state is unavailable"); + } + + const editorState = getEditorState(); + const payload = buildAutomatorPayload({ + history: rawHistory, + editorState, + }); + + try { + console.log("[Automator] running query:", chatQuery, { + contextComponents: payload.context.components.length, + contextQueries: payload.context.queries.length, + messageCount: payload.messages.length, + }); - async sendMessage(message: ChatMessage): Promise { - await new Promise(resolve => setTimeout(resolve, this.delay)); - return { content: `Mock response: ${message.text}` }; + const result: any = await getPromiseAfterDispatch( + dispatch, + routeByNameAction( + chatQuery, + executeQueryAction({ + args: buildAutomatorQueryArgs( + message, + sessionId, + history, + payload, + rawHistory + ), + }) + ) + ); + + const response = normalizeAutomatorQueryResponse(result); + + console.log("[Automator] parsed", { + actions: response.actions?.length ?? 0, + invalid: response.automator?.invalidActionCount ?? 0, + }); + + return response; + } catch (e: any) { + throw new Error(e?.message || "AI assistant query execution failed"); + } } } -// ============================================================================ -// HANDLER FACTORY (creates the right handler based on type) -// ============================================================================ - -export function createMessageHandler( - type: "n8n" | "query" | "mock", - config: N8NHandlerConfig | QueryHandlerConfig -): MessageHandler { - switch (type) { - case "n8n": - return new N8NHandler(config as N8NHandlerConfig); - - case "query": - return new QueryHandler(config as QueryHandlerConfig); - - case "mock": - return new MockHandler(); - - default: - throw new Error(`Unknown message handler type: ${type}`); - } -} \ No newline at end of file +// ============================================================================ +// HANDLER FACTORY (creates the right handler based on type) +// ============================================================================ + +export function createMessageHandler( + type: "query", + config: QueryHandlerConfig +): MessageHandler { + switch (type) { + case "query": + return new QueryHandler(config); + + default: + throw new Error(`Unknown message handler type: ${type}`); + } +} diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts index d24e0ce84..bbc221e33 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts @@ -42,28 +42,41 @@ export interface ChatMessage { export interface MessageHandler { sendMessage(message: ChatMessage, sessionId?: string): Promise; // Future: sendMessageStream?(message: ChatMessage): AsyncGenerator; - } + } + + export interface AIAssistantMessageHandler { + sendMessage(message: ChatMessage, sessionId: string | undefined, conversationHistory: ChatMessage[]): Promise; + } export interface MessageResponse { content: string; metadata?: any; actions?: any[]; + /** + * When the Automator parses a structured `{explanation, actions}` reply + * we surface the parsed payload here so the UI / downstream consumers + * can show extra context (e.g. "3 actions scheduled"). + */ + automator?: { + isStructured: boolean; + explanation: string; + invalidActionCount: number; + }; } // ============================================================================ // CONFIGURATION TYPES (simplified) // ============================================================================ - export interface N8NHandlerConfig { - modelHost: string; - systemPrompt?: string; - streaming?: boolean; - } - export interface QueryHandlerConfig { chatQuery: string; dispatch: any; - } + /** + * Snapshot accessor for the live editor state. The handler calls this + * lazily on every send so it always has the *current* canvas state. + */ + getEditorState?: () => any; + } // ============================================================================ // COMPONENT PROPS (what each component actually needs) @@ -93,8 +106,6 @@ export interface ChatCoreProps { // Bottom Panel Props (simplified, no styling controls) export interface ChatPanelProps { tableName: string; - modelHost: string; - systemPrompt?: string; - streaming?: boolean; + chatQuery: string; onMessageUpdate?: (message: string) => void; } diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx index c6aae7ad2..e26bf4ab6 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx @@ -281,9 +281,6 @@ const DatePickerTmpCmp = new UICompBuilder(childrenMap, (props) => { props.onEvent ); }} - onPanelChange={() => { - handleDateChange("", props.value.onChange, noop); - }} onFocus={() => props.onEvent("focus")} onBlur={() => props.onEvent("blur")} suffixIcon={hasIcon(props.suffixIcon) && props.suffixIcon} diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/dateUIView.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/dateUIView.tsx index 5a7e188c8..9fcbc09b7 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/dateUIView.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/dateUIView.tsx @@ -55,7 +55,7 @@ const StyledAntdSelect = styled(AntdSelect)` export interface DataUIViewProps extends DateCompViewProps { value?: DatePickerProps['value']; onChange: DatePickerProps['onChange']; - onPanelChange: () => void; + onPanelChange?: () => void; onClickDateTimeZone:(value:any)=>void; tabIndex?: number; $disabledStyle?: DisabledInputStyleType; diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index 8ae653ffa..bd4016c16 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -11,11 +11,11 @@ import { AppSelectComp } from "comps/comps/layout/appSelectComp"; import { NameAndExposingInfo } from "comps/utils/exposingTypes"; import { ConstructorToComp, ConstructorToDataType } from "lowcoder-core"; import { CanvasContainer } from "comps/comps/gridLayoutComp/canvasView"; -import { CanvasContainerID } from "constants/domLocators"; -import { PreviewContainerID } from "constants/domLocators"; +import { CanvasContainerID, PreviewContainerID } from "constants/domLocators"; import { EditorContainer, EmptyContent } from "pages/common/styledComponent"; import { Layers } from "constants/Layers"; import { ExternalEditorContext } from "util/context/ExternalEditorContext"; +import { EditorContext } from "comps/editorState"; import { default as Skeleton } from "antd/es/skeleton"; import { hiddenPropertyView } from "comps/utils/propertyUtils"; import { dropdownControl } from "@lowcoder-ee/comps/controls/dropdownControl"; @@ -47,6 +47,21 @@ const TabBarItem = React.lazy(() => ); const EventOptions = [clickEvent] as const; +/** Mobile nav editor: tab bar uses position:absolute bottom; this root is the containing block */ +const MobileNavCanvasRoot = styled(CanvasContainer)` + position: relative; +`; + +/** Strip shared EditorContainer defaults (16px padding + scrollbar-gutter: stable) for mobile nav */ +const MobileNavEditorContainer = styled(EditorContainer)` + padding: 0; + padding-right: 0; + scrollbar-gutter: auto; + overflow-x: auto; + overflow-y: auto; + background: transparent; +`; + const AppViewContainer = styled.div` position: absolute; width: 100%; @@ -221,17 +236,17 @@ const TabBarWrapper = styled.div<{ $readOnly: boolean, $canvasBg: string, $tabBarHeight: string, - $maxWidth: number, $verticalAlignment: string; }>` + box-sizing: border-box; max-width: inherit; background: ${(props) => (props.$canvasBg)}; margin: 0 auto; - position: fixed; + position: ${(props) => (props.$readOnly ? "fixed" : "absolute")}; bottom: 0; left: 0; right: 0; - width: ${(props) => props.$readOnly ? "100%" : `${props.$maxWidth - 30}px`}; + width: 100%; z-index: ${Layers.tabBar}; padding-bottom: env(safe-area-inset-bottom, 0); @@ -389,7 +404,6 @@ function convertTreeData(data: any) { function TabBarView(props: TabBarProps & { tabBarHeight: string; - maxWidth: number; verticalAlignment: string; showSeparator: boolean; navIconSize: string; @@ -404,7 +418,6 @@ function TabBarView(props: TabBarProps & { $readOnly={props.readOnly} $canvasBg={canvasBg} $tabBarHeight={props.tabBarHeight} - $maxWidth={props.maxWidth} $verticalAlignment={props.verticalAlignment} > { const bgColor = (useContext(ThemeContext)?.theme || defaultTheme).canvas; const onEvent = comp.children.onEvent.getView(); + // Pull app-level Theme / Canvas Settings (managed via the left-sidebar + // "Canvas" pane and shared with normal apps + modules). Mobile nav already + // owns its own maxWidth + grid behaviour, so we only consume the + // background + padding subset here. + const editorState = useContext(EditorContext); + const appSettings = editorState?.getAppSettings(); + const canvasBg = appSettings?.gridBg; + const canvasBgImage = appSettings?.gridBgImage; + const canvasBgImageRepeat = appSettings?.gridBgImageRepeat || "no-repeat"; + const canvasBgImageSize = appSettings?.gridBgImageSize || "cover"; + const canvasBgImagePosition = appSettings?.gridBgImagePosition || "center"; + const canvasBgImageOrigin = appSettings?.gridBgImageOrigin || "padding-box"; + const canvasPaddingX = appSettings?.gridPaddingX ?? 0; + const canvasPaddingY = appSettings?.gridPaddingY ?? 0; + + const canvasBackgroundStyle: React.CSSProperties = { + background: "#FFFFFF", + }; + if (canvasBg) { + canvasBackgroundStyle.background = canvasBg; + } + if (canvasBgImage) { + canvasBackgroundStyle.backgroundImage = `url('${canvasBgImage}')`; + canvasBackgroundStyle.backgroundRepeat = canvasBgImageRepeat; + canvasBackgroundStyle.backgroundSize = canvasBgImageSize; + canvasBackgroundStyle.backgroundPosition = canvasBgImagePosition; + canvasBackgroundStyle.backgroundOrigin = canvasBgImageOrigin; + } + const canvasContentPadding = `${canvasPaddingY}px ${canvasPaddingX}px`; + const getContainer = useCallback(() => document.querySelector(`#${PreviewContainerID}`) || document.querySelector(`#${CanvasContainerID}`) || @@ -702,7 +745,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { currentTab.children.app.getView()) || ( ); } @@ -712,7 +755,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { currentTab.children.action.getView()) || ( ) }, [tabIndex, tabViews, dataOptionType]); @@ -769,7 +812,6 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { tabItemActiveStyle={navItemActiveStyle} tabBarHeight={tabBarHeight} navIconSize={navIconSize} - maxWidth={maxWidth} verticalAlignment={verticalAlignment} showSeparator={showSeparator} /> @@ -870,8 +912,12 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { if (readOnly) { return ( - - {appView} + + {appView} {menuMode === MobileMode.Hamburger ? ( <> {hamburgerButton} @@ -885,8 +931,12 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { } return ( - - {appView} + + {appView} {menuMode === MobileMode.Hamburger ? ( <> {hamburgerButton} @@ -895,7 +945,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { ) : ( tabBarView )} - + ); }); diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index 4a7e2b355..66f23635c 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -6,6 +6,7 @@ import MainContent from "components/layout/MainContent"; import { LayoutMenuItemComp, LayoutMenuItemListComp } from "comps/comps/layout/layoutMenuItemComp"; import { menuPropertyView } from "comps/comps/navComp/components/MenuItemList"; import { registerLayoutMap } from "comps/comps/uiComp"; +import { EditorContext } from "comps/editorState"; import { MultiCompBuilder, withDefault, withViewFn } from "comps/generators"; import { withDispatchHook } from "comps/generators/withDispatchHook"; import { NameAndExposingInfo } from "comps/utils/exposingTypes"; @@ -14,7 +15,7 @@ import { TopHeaderHeight } from "constants/style"; import { Section, controlItem, sectionNames } from "lowcoder-design"; import { trans } from "i18n"; import { EditorContainer, EmptyContent } from "pages/common/styledComponent"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import styled from "styled-components"; import { isUserViewMode, useAppPathParam } from "util/hooks"; import { StringControl, jsonControl } from "comps/controls/codeControl"; @@ -381,6 +382,21 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { const dataOptionType = comp.children.dataOptionType.getView(); const onEvent = comp.children.onEvent.getView(); + // Pull app-level Theme / Canvas Settings (managed via the left-sidebar + // "Canvas" pane and shared with normal apps + modules). For aggregation + // apps the grid sizing fields are intentionally hidden in the settings UI; + // we only consume the background + padding subset here. + const editorState = useContext(EditorContext); + const appSettings = editorState?.getAppSettings(); + const canvasBg = appSettings?.gridBg; + const canvasBgImage = appSettings?.gridBgImage; + const canvasBgImageRepeat = appSettings?.gridBgImageRepeat || "no-repeat"; + const canvasBgImageSize = appSettings?.gridBgImageSize || "cover"; + const canvasBgImagePosition = appSettings?.gridBgImagePosition || "center"; + const canvasBgImageOrigin = appSettings?.gridBgImageOrigin || "padding-box"; + const canvasPaddingX = appSettings?.gridPaddingX ?? 0; + const canvasPaddingY = appSettings?.gridPaddingY ?? 0; + // filter out hidden. unauthorised items filtered by server const filterItem = useCallback((item: LayoutMenuItemComp): boolean => { return !item.children.hidden.getView(); @@ -685,8 +701,25 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { /> ); + // Build canvas background style (color + optional image), driven by the + // shared app-level Canvas Settings. + const canvasBackgroundStyle: React.CSSProperties = {}; + if (canvasBg) { + canvasBackgroundStyle.background = canvasBg; + } + if (canvasBgImage) { + canvasBackgroundStyle.backgroundImage = `url('${canvasBgImage}')`; + canvasBackgroundStyle.backgroundRepeat = canvasBgImageRepeat; + canvasBackgroundStyle.backgroundSize = canvasBgImageSize; + canvasBackgroundStyle.backgroundPosition = canvasBgImagePosition; + canvasBackgroundStyle.backgroundOrigin = canvasBgImageOrigin; + } + let content = ( - + {(navPosition === 'top') && (
{ navMenu } @@ -697,7 +730,15 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { {navMenu} )} - {pageView} + + {pageView} + {(navPosition === 'bottom') && (