From 9abacbd75cf31f1e4426ccd00f569be20b1df578 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 27 Apr 2026 19:50:05 +0500 Subject: [PATCH 01/33] test bottom chat panel --- .../packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx b/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx index a7693b7bc8..36da7b865c 100644 --- a/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx +++ b/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx @@ -120,7 +120,7 @@ function Bottom(props: any) { From 58d2a7e1b5137ef7599d8a5dcc8c14988455c35a Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 30 Apr 2026 02:05:05 +0500 Subject: [PATCH 02/33] add new query handler for ai assistant --- .../src/comps/comps/chatComp/chatComp.tsx | 12 +- .../comps/chatComp/components/ChatPanel.tsx | 32 ++--- .../components/ChatPanelContainer.tsx | 18 ++- .../chatComp/handlers/messageHandlers.ts | 110 +++++++++--------- .../comps/comps/chatComp/types/chatTypes.ts | 14 +-- .../src/pages/editor/bottom/BottomPanel.tsx | 46 +++++++- 6 files changed, 127 insertions(+), 105 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx index 39de2e7393..bce90b54a4 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/ChatPanel.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx index f4823011e6..0de049b0ff 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx @@ -1,47 +1,39 @@ // client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx -import { useMemo, useEffect } from "react"; +import { useMemo, useContext } 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) // ============================================================================ export function ChatPanel({ tableName, - modelHost, - systemPrompt = trans("chat.defaultSystemPrompt"), - streaming = true, + chatQuery, onMessageUpdate }: ChatPanelProps) { + const editorState = useContext(EditorContext); + const storage = useMemo(() => createChatStorage(tableName), [tableName] ); const messageHandler = useMemo(() => - new N8NHandler({ - modelHost, - systemPrompt, - streaming + new AIAssistantQueryHandler({ + chatQuery, + dispatch: editorState?.rootComp?.dispatch, }), - [modelHost, systemPrompt, streaming] + [chatQuery, editorState?.rootComp?.dispatch] ); - // Cleanup on unmount - delete chat data from storage - useEffect(() => { - return () => { - storage.cleanup(); - }; - }, [storage]); - 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 9f0766cea4..690607a81f 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"; @@ -77,7 +77,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; } @@ -262,11 +262,17 @@ function ChatPanelView({ messageHandler, placeholder, onMessageUpdate }: Omit { - const { modelHost, systemPrompt, streaming } = this.config; + async sendMessage(message: ChatMessage): Promise { + const { chatQuery, dispatch} = this.config; - if (!modelHost) { - throw new Error("Model host is required for N8N calls"); + // If no query selected or dispatch unavailable, return mock response + if (!chatQuery || !dispatch) { + console.log("No query selected or dispatch unavailable, returning mock response"); + await new Promise((res) => setTimeout(res, 500)); + return { content: "(mock) You typed: " + message.text }; } 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'}`); + 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 + }, + }) + ) + ); + console.log("Query result:", result); + return result.message + } catch (e: any) { + throw new Error(e?.message || "Query execution failed"); } } } // ============================================================================ -// QUERY HANDLER (for Canvas Components) +// AI ASSISTANT QUERY HANDLER (bottom panel) // ============================================================================ -export class QueryHandler implements MessageHandler { +export class AIAssistantQueryHandler implements AIAssistantMessageHandler { 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 + async sendMessage(message: ChatMessage, sessionId?: string, conversationHistory?: ChatMessage[]): Promise { + const { chatQuery, dispatch } = this.config; + const history = conversationHistory ?? [message]; + const llmMessages = history.map((msg) => ({ + role: msg.role, + content: msg.text, + })); + if (!chatQuery || !dispatch) { + console.log("No AI assistant query selected or dispatch unavailable, returning mock response"); await new Promise((res) => setTimeout(res, 500)); return { content: "(mock) You typed: " + message.text }; } try { + console.log("Executing AI assistant 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 + args: { + message: { value: message }, + prompt: { value: message.text }, + sessionId: { value: sessionId }, + conversationHistory: { value: history }, + messages: { value: llmMessages }, }, }) ) ); - - return result.message + console.log("AI assistant query result:", result); + return result.message; } catch (e: any) { - throw new Error(e?.message || "Query execution failed"); + throw new Error(e?.message || "AI assistant query execution failed"); } } } @@ -107,15 +108,12 @@ export class MockHandler implements MessageHandler { // ============================================================================ export function createMessageHandler( - type: "n8n" | "query" | "mock", - config: N8NHandlerConfig | QueryHandlerConfig + type: "query" | "mock", + config: QueryHandlerConfig ): MessageHandler { switch (type) { - case "n8n": - return new N8NHandler(config as N8NHandlerConfig); - case "query": - return new QueryHandler(config as QueryHandlerConfig); + return new QueryHandler(config); case "mock": return new MockHandler(); 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 d24e0ce84f..17ad236fa5 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts @@ -43,6 +43,10 @@ export interface ChatMessage { sendMessage(message: ChatMessage, sessionId?: string): Promise; // Future: sendMessageStream?(message: ChatMessage): AsyncGenerator; } + + export interface AIAssistantMessageHandler { + sendMessage(message: ChatMessage, sessionId?: string, conversationHistory?: ChatMessage[]): Promise; + } export interface MessageResponse { content: string; @@ -54,12 +58,6 @@ export interface ChatMessage { // CONFIGURATION TYPES (simplified) // ============================================================================ - export interface N8NHandlerConfig { - modelHost: string; - systemPrompt?: string; - streaming?: boolean; - } - export interface QueryHandlerConfig { chatQuery: string; dispatch: any; @@ -93,8 +91,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/pages/editor/bottom/BottomPanel.tsx b/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx index 36da7b865c..86da991bad 100644 --- a/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx +++ b/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx @@ -2,7 +2,7 @@ import { BottomContent } from "pages/editor/bottom/BottomContent"; import { ResizableBox, ResizeCallbackData } from "react-resizable"; import styled from "styled-components"; import * as React from "react"; -import { useMemo, useState } from "react"; +import { useContext, useMemo, useState } from "react"; import { getPanelStyle, savePanelStyle } from "util/localStorageUtil"; import { BottomResultPanel } from "../../../components/resultPanel/BottomResultPanel"; import { AppState } from "../../../redux/reducers"; @@ -13,8 +13,10 @@ import Flex from "antd/es/flex"; import type { MenuProps } from 'antd/es/menu'; import { BuildOutlined, DatabaseOutlined } from "@ant-design/icons"; import Menu from "antd/es/menu/menu"; +import Select from "antd/es/select"; import { AIGenerate } from "lowcoder-design"; import { ChatPanel } from "@lowcoder-ee/comps/comps/chatComp/components/ChatPanel"; +import { EditorContext } from "comps/editorState"; type MenuItem = Required['items'][number]; @@ -60,6 +62,18 @@ const ChatTitle = styled.h3` color: #222222; `; +const QuerySelectorWrapper = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +const QueryLabel = styled.span` + font-size: 12px; + color: #8b8fa3; + white-space: nowrap; +`; + const preventDefault = (e: any) => { e.preventDefault(); }; @@ -84,6 +98,17 @@ function Bottom(props: any) { const [bottomHeight, setBottomHeight] = useState(panelStyle.bottom.h); const [currentOption, setCurrentOption] = useState("data"); + const [selectedQuery, setSelectedQuery] = useState(""); + + const editorState = useContext(EditorContext); + + const queryOptions = useMemo(() => { + if (!editorState) return []; + return editorState.queryCompInfoList().map((info) => ({ + label: info.name, + value: info.name, + })); + }, [editorState]); const items: MenuItem[] = [ { key: 'data', icon: , label: 'Data Queries' }, @@ -98,7 +123,7 @@ function Bottom(props: any) { height={panelStyle.bottom.h} resizeHandles={["n"]} minConstraints={[680, 285]} - maxConstraints={[Infinity, clientHeight - 48 - 40]} // - app_header - right_header + maxConstraints={[Infinity, clientHeight - 48 - 40]} onResizeStart={addListener} onResizeStop={resizeStop} > @@ -117,12 +142,23 @@ function Bottom(props: any) { Lowcoder AI Assistant + + Query: + )} From 9a5d6314dd8de71779e67341187a684c515d0302 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 6 May 2026 23:34:34 +0500 Subject: [PATCH 04/33] add tools callings --- .../chatComp/handlers/messageHandlers.ts | 23 +- .../preLoadComp/actions/automator/README.md | 214 ++++++++---------- .../preLoadComp/actions/automator/index.ts | 28 ++- .../actions/automator/orchestrator.ts | 6 + .../actions/automator/responseParser.ts | 124 ++++++++-- .../actions/automator/systemPrompt.ts | 37 ++- .../actions/automator/toolDefinitions.ts | 122 ++++++++++ 7 files changed, 385 insertions(+), 169 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/handlers/messageHandlers.ts b/client/packages/lowcoder/src/comps/comps/chatComp/handlers/messageHandlers.ts index a72c320ce5..0c18ae7689 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/handlers/messageHandlers.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/handlers/messageHandlers.ts @@ -5,7 +5,7 @@ import { routeByNameAction, executeQueryAction } from "lowcoder-core"; import { getPromiseAfterDispatch } from "util/promiseUtils"; import { buildAutomatorPayload, - parseAutomatorResponse, + parseResponse, } from "../../preLoadComp/actions/automator"; // ============================================================================ @@ -119,11 +119,13 @@ export class AIAssistantQueryHandler implements AIAssistantMessageHandler { prompt: { value: message.text }, sessionId: { value: sessionId }, conversationHistory: { value: history }, - // `messages` is what the test JS query consumes — now - // automatically prefixed with the Automator system prompt. messages: { value: payload.messages }, - // ---- New explicit fields for power users + // ---- Tool calling: the JS query should forward this to the + // HTTP body so the LLM can call `execute_automator_actions` + tools: { value: payload.tools }, + + // ---- Extra fields for power users system: { value: payload.system }, context: { value: payload.context }, actionsCatalog: { value: payload.actionsCatalog }, @@ -134,18 +136,21 @@ export class AIAssistantQueryHandler implements AIAssistantMessageHandler { ) ); - // The query is expected to return `{ message: { role, content } }`. - // We then parse the content — even if the model returned plain prose - // we still surface it as a normal assistant reply. + // The query may return tool_calls (new path) or plain content (legacy). + // `parseResponse` tries tool_calls first, then falls back to text JSON + // extraction, so old queries that haven't been updated keep working. const raw = result?.message ?? result ?? {}; const content: string = typeof raw === "string" ? raw : typeof raw.content === "string" ? raw.content - : JSON.stringify(raw); + : typeof raw === "object" && !raw.tool_calls + ? JSON.stringify(raw) + : ""; + const toolCalls: unknown[] | undefined = raw?.tool_calls; - const parsed = parseAutomatorResponse(content); + const parsed = parseResponse({ content, tool_calls: toolCalls }); const displayText = parsed.isStructured && parsed.explanation diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/README.md b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/README.md index 7f2417dce9..cabf8ea081 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/README.md +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/README.md @@ -10,7 +10,7 @@ nesting modals, and so on. --- -## How the flow works +## How the flow works (tool-calling) ``` ┌────────────────────────────┐ @@ -24,21 +24,20 @@ nesting modals, and so on. │ - lean system prompt + actions catalog │ │ - curated component cheatsheet │ │ - conversation history │ + │ - OpenAI tool definitions (execute_automator_…) │ └────────────┬───────────────────────────────────────┘ - │ messages = [system, ...history] - │ context, system, actionsCatalog, ... + │ messages, tools, context, … ▼ ┌────────────────────────────────────────────────────┐ │ YOUR Lowcoder JS query (the "model bridge") │ - │ e.g. forwards `messages` to an OpenAI HTTP query │ + │ Forwards messages + tools to an HTTP query │ └────────────┬───────────────────────────────────────┘ - │ { message: { role, content } } + │ { message: { role, content, tool_calls } } ▼ ┌────────────────────────────────────────────────────┐ - │ parseAutomatorResponse() │ - │ - extracts JSON `{ explanation, actions }` │ - │ - tolerates ```json fences and prose noise │ - │ - validates actions against the supported set │ + │ parseResponse() │ + │ 1. tool_calls present? → extract actions (clean) │ + │ 2. fallback → legacy JSON text extraction │ └────────────┬───────────────────────────────────────┘ │ ▼ @@ -49,6 +48,12 @@ nesting modals, and so on. └────────────────────────────────────────────────────┘ ``` +The model uses **tool calling** (function calling) instead of embedding +JSON in its text. When the model wants to act on the canvas, it calls the +`execute_automator_actions` tool with `{ explanation, actions }` — the API +guarantees valid JSON. When it needs clarification, it responds with plain +text (no tool call). No custom parsing needed. + Everything is **client-side**. The only thing you wire on the backend is the LLM HTTP call — through a regular Lowcoder data query. @@ -61,9 +66,6 @@ the LLM, and one JS query that the Automator panel calls. ### 1. The HTTP query — `llmHttp` -This one talks to the LLM provider. Examples below use OpenAI; swap the URL -and headers for Ollama / Anthropic / Together / Groq. - | Field | Value | | --- | --- | | Method | `POST` | @@ -77,14 +79,13 @@ Body (raw JSON, with Lowcoder bindings): { "model": "gpt-4o-mini", "temperature": 0.2, - "response_format": { "type": "json_object" }, - "messages": {{ messages.value }} + "messages": {{ messages.value }}, + "tools": {{ tools.value }} } ``` -> Tip: `response_format: json_object` is the OpenAI-only switch that forces -> a single JSON object reply. With Anthropic or Ollama you can drop it; the -> Automator's parser tolerates fenced ```json blocks too. +> The `tools` parameter tells the model about `execute_automator_actions`. +> The model decides when to call it vs. when to respond with plain text. #### Ollama variant @@ -92,20 +93,28 @@ Body (raw JSON, with Lowcoder bindings): { "model": "llama3.1", "stream": false, - "format": "json", - "messages": {{ messages.value }} + "messages": {{ messages.value }}, + "tools": {{ tools.value }} } ``` URL: `http://localhost:11434/api/chat` #### Anthropic variant +Anthropic uses a slightly different tool format. Map the OpenAI tool +definition to Anthropic's `tools` shape in your JS query: + ```json { "model": "claude-3-5-sonnet-latest", "max_tokens": 4096, "system": "{{ system.value }}", - "messages": {{ messagesWithoutSystem.value }} + "messages": {{ messagesWithoutSystem.value }}, + "tools": [{ + "name": "execute_automator_actions", + "description": "Execute Lowcoder Automator actions on the canvas.", + "input_schema": {{ JSON.stringify(tools.value[0].function.parameters) }} + }] } ``` URL: `https://api.anthropic.com/v1/messages`, @@ -114,29 +123,35 @@ headers: `x-api-key: YOUR_KEY`, `anthropic-version: 2023-06-01`. ### 2. The JS query — `assistantBridge` This is the query you select in the Automator panel's "Query:" dropdown. -All it does is forward the prompt to your HTTP query and unwrap the reply. +It forwards messages + tools to your HTTP query and returns the response. ```js return llmHttp.run({ - messages: messages.value, // already includes the system prompt -}).then((data) => ({ - message: { - role: "assistant", - // OpenAI: data.choices[0].message.content - // Ollama: data.message.content - // Anthropic: data.content[0].text - content: data?.choices?.[0]?.message?.content - || data?.message?.content - || data?.content?.[0]?.text - || "No response from model.", - }, -})); + messages: messages.value, + tools: tools.value, +}).then((data) => { + const msg = data?.choices?.[0]?.message; + return { + message: { + role: "assistant", + content: msg?.content || "", + tool_calls: msg?.tool_calls || [], + }, + }; +}); ``` Now in the bottom panel, switch to **Lowcoder AI**, pick `assistantBridge` in the Query dropdown, and start chatting. The **Automator** toggle next to -the dropdown controls whether the system prompt + live context is injected -(default: ON). +the dropdown controls whether the system prompt + live context + tools are +injected (default: ON). + +### Legacy setup (still works) + +If you have existing queries that don't pass `tools` and rely on the model +embedding JSON in its text content, they still work. The parser falls back +to the old text-extraction logic automatically. But the tool-calling path +is recommended — it's more reliable and simpler to set up. --- @@ -146,11 +161,12 @@ Inside the JS query you can use any of these args: | Arg | What it is | | --- | --- | -| `messages` | Final OpenAI-style message array, already prefixed with the Automator system prompt and live editor context. **The default and recommended choice.** | +| `messages` | Final OpenAI-style message array, prefixed with the Automator system prompt and live editor context. **The default and recommended choice.** | +| `tools` | OpenAI-compatible tool definitions array. Pass this to the HTTP body alongside `messages`. | | `messagesWithoutSystem` | Same array minus the leading `system` message. Use with Anthropic. | | `system` | The composed system prompt string by itself. | | `context` | The live editor snapshot (components, queries, canvas grid, selected). | -| `actionsCatalog` | The catalog of allowed actions (so you can show it in tooltips, etc). | +| `actionsCatalog` | The catalog of allowed actions. | | `componentCatalog` | The curated cheatsheet of component shapes. | | `prompt` | The latest user message text only. | | `conversationHistory` | The full ChatMessage history including IDs/timestamps. | @@ -159,62 +175,54 @@ Inside the JS query you can use any of these args: --- -## What the model is expected to return +## What the model returns -A single JSON object — no prose, no fences: +### With tool calling (recommended) + +When the model wants to act, it returns a `tool_calls` array: ```json { - "explanation": "Created a basic Todo app with title, input, button and table.", - "actions": [ - { - "action": "place_component", - "component": "text", - "component_name": "todoTitle", - "layout": { "x": 0, "y": 0, "w": 24, "h": 4 }, - "action_parameters": { "text": "## My Todos", "type": "markdown" } - }, - { - "action": "place_component", - "component": "input", - "component_name": "newTodoInput", - "layout": { "x": 0, "y": 4, "w": 18, "h": 6 }, - "action_parameters": { - "label": { "text": "New task", "position": "row" }, - "placeholder": "What needs doing?" - } - }, - { - "action": "place_component", - "component": "button", - "component_name": "addTodoBtn", - "layout": { "x": 18, "y": 4, "w": 6, "h": 6 }, - "action_parameters": { "text": "Add", "type": "primary" } - }, - { - "action": "place_component", - "component": "table", - "component_name": "todoTable", - "layout": { "x": 0, "y": 10, "w": 24, "h": 30 }, - "action_parameters": { - "columns": [ - { "title": "Task", "dataIndex": "task", "render": { "compType": "text", "comp": { "text": "{{currentCell}}" } } }, - { "title": "Status", "dataIndex": "status", "render": { "compType": "text", "comp": { "text": "{{currentCell}}" } } } - ], - "data": "[{\"task\":\"Buy groceries\",\"status\":\"Pending\"}]" - } + "choices": [{ + "message": { + "role": "assistant", + "content": "I'll create a basic Todo app with a title, input, button, and table.", + "tool_calls": [{ + "id": "call_abc123", + "type": "function", + "function": { + "name": "execute_automator_actions", + "arguments": "{\"explanation\":\"Creating a Todo app...\",\"actions\":[{\"action\":\"place_component\",\"component\":\"text\",\"component_name\":\"todoTitle\",\"layout\":{\"x\":0,\"y\":0,\"w\":24,\"h\":4},\"action_parameters\":{\"text\":\"## My Todos\",\"type\":\"markdown\"}}]}" + } + }] } - ] + }] } ``` -The Automator parses this, executes every action, and shows the -`explanation` in the chat with a small footer like -`— Automator: 4 actions executed`. +When the model needs clarification, it responds with just text (no tool calls): + +```json +{ + "choices": [{ + "message": { + "role": "assistant", + "content": "I can build that for you. Would you like:\n- A simple table view?\n- A kanban board layout?\n\nPlease confirm and I'll proceed." + } + }] +} +``` -If the model says "actions: []" with a bullet-point plan, that's the -clarification flow — you reply "go ahead" (or with corrections) and it -returns a real action list on the next turn. +### Legacy (text JSON) + +A single JSON object in the text content: + +```json +{ + "explanation": "Created a basic Todo app.", + "actions": [...] +} +``` --- @@ -256,32 +264,12 @@ Adding a new action is a two-step change: 5. Type: **`add a delete button column to the table`** → it should reuse the existing `todoTable` name (this is the "context awareness" win). 6. Toggle **Automator** off → send a message → the JS query receives only - the raw conversation history (useful for plain ChatGPT-style flows). + the raw conversation history, no tools, no system prompt (useful for + plain ChatGPT-style flows). --- -## Supported component types - -The component catalog (`componentCatalog.ts`) includes 30 types: - -`text`, `button`, `input`, `numberInput`, `textArea`, `password`, `select`, -`checkbox`, `radio`, `switch`, `slider`, `rating`, `date`, `form`, -`container`, `modal`, `drawer`, `table`, `listView`, `card`, -`tabbedContainer`, `image`, `video`, `avatar`, `chart`, `progress`, -`navigation`, `timeline`, `step`, `divider` - -The model can use any component type registered in Lowcoder, even if it's -not in the catalog — the catalog just provides property hints and defaults. - ---- - -## Architecture (how it replaces `Latest_prompt.md`) - -The old `Latest_prompt.md` (4.7K lines) was pasted into n8n manually. It -couldn't see the canvas, shipped a massive component catalog every turn, -and duplicated rules dozens of times. - -The Automator splits that into small, focused modules: +## Architecture | File | Purpose | | --- | --- | @@ -289,11 +277,9 @@ The Automator splits that into small, focused modules: | `actionsCatalog.ts` | Machine-readable list of all supported actions | | `componentCatalog.ts` | Curated cheatsheet (only relevant types sent per turn) | | `editorSnapshot.ts` | Live context from `EditorState` (components, queries, canvas) | -| `responseParser.ts` | Robust JSON extraction from model output | -| `orchestrator.ts` | Assembles system + context + history into the message array | +| `toolDefinitions.ts` | OpenAI-compatible tool definitions for function calling | +| `responseParser.ts` | Dual-path parser: tool_calls (clean) → text fallback (legacy) | +| `orchestrator.ts` | Assembles system + context + history + tools into the payload | `ChatPanelContainer.tsx` holds the `ACTION_REGISTRY` — a simple map from -action names to executor functions. Adding a new action is one line there -plus one catalog entry. - -The legacy `Latest_prompt.md` is kept as reference only — nothing imports it. +action names to executor functions. diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts index d81af885e1..64708ed1e9 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts @@ -21,6 +21,8 @@ export { } from "./editorSnapshot"; export { parseAutomatorResponse, + parseToolCallResponse, + parseResponse, type ParsedAutomatorResponse, type AutomatorAction, } from "./responseParser"; @@ -30,19 +32,33 @@ export { type OrchestratorInput, type OrchestratorOutput, } from "./orchestrator"; +export { + buildToolDefinitions, + TOOL_NAME, + type OpenAIToolDefinition, +} from "./toolDefinitions"; /** * Quick-start guide — see automator/README.md for full details. * * 1. Create an HTTP query (e.g. "llmQuery") pointing at your model endpoint. + * Include `"tools": {{ tools.value }}` in the request body. + * * 2. Create a JS query (e.g. "aiQuery") that calls the HTTP query: * - * return llmQuery.run({ messages: messages.value }).then((data) => ({ - * message: { - * role: "assistant", - * content: data?.choices?.[0]?.message?.content || "No response." - * } - * })); + * return llmQuery.run({ + * messages: messages.value, + * tools: tools.value, + * }).then((data) => { + * const msg = data?.choices?.[0]?.message; + * return { + * message: { + * role: "assistant", + * content: msg?.content || "", + * tool_calls: msg?.tool_calls || [], + * }, + * }; + * }); * * 3. In the bottom panel, click the AI tab, select "aiQuery", and chat. */ diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts index ade593a3b8..c0417bc97d 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts @@ -9,6 +9,7 @@ import { } from "./editorSnapshot"; import { getComponentCatalog, ComponentCatalogEntry } from "./componentCatalog"; import { composeSystemMessage } from "./systemPrompt"; +import { buildToolDefinitions, OpenAIToolDefinition } from "./toolDefinitions"; /** * A "chat message" in the OpenAI-compatible shape (role + content). Almost @@ -35,6 +36,8 @@ export interface OrchestratorInput { export interface OrchestratorOutput { /** Full message array including the synthesised system message. */ messages: LLMMessage[]; + /** OpenAI-compatible tool definitions for function calling. */ + tools: OpenAIToolDefinition[]; /** The composed system message string (also exposed for power users). */ system: string; /** The editor context snapshot (also exposed separately). */ @@ -67,6 +70,8 @@ export function buildAutomatorPayload(input: OrchestratorInput): OrchestratorOut editorContext: context, }); + const tools = withSystemPrompt ? buildToolDefinitions() : []; + const messages: LLMMessage[] = []; if (withSystemPrompt) { messages.push({ role: "system", content: system }); @@ -77,6 +82,7 @@ export function buildAutomatorPayload(input: OrchestratorInput): OrchestratorOut return { messages, + tools, system, context, actionsCatalog: ACTIONS_CATALOG, diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/responseParser.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/responseParser.ts index 41dd7262da..e319832954 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/responseParser.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/responseParser.ts @@ -1,16 +1,24 @@ // client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/responseParser.ts /** - * Extracts the structured `{ explanation, actions }` object from an LLM - * response. Models don't always return clean JSON — they may wrap it in - * markdown fences, prepend prose, or just return plain text. This parser - * handles all of those gracefully. + * Parses LLM responses into the `{ explanation, actions }` shape that the + * Automator executor expects. * - * Action-name validation is intentionally omitted here — the executor - * registry in ChatPanelContainer already skips unknown actions with a - * warning, so double-checking here would be redundant. + * Two parsing paths, tried in order: + * + * 1. **Tool-calls path** (preferred) — the model called + * `execute_automator_actions` via OpenAI function-calling. The + * arguments are guaranteed-valid JSON, so parsing is trivial. + * + * 2. **Legacy text path** (fallback) — the model returned a raw JSON + * object in its text content (possibly wrapped in markdown fences or + * surrounded by prose). This path uses the same balanced-brace + * extraction that shipped before the tool-calling refactor, so + * existing queries that haven't been updated keep working. */ +import { TOOL_NAME } from "./toolDefinitions"; + export interface ParsedAutomatorResponse { explanation: string; actions: AutomatorAction[]; @@ -28,15 +36,71 @@ export interface AutomatorAction { [key: string]: unknown; } +// ──────────────────────────────────────────────────────────────────────── +// 1. TOOL-CALLS PATH (new, clean) +// ──────────────────────────────────────────────────────────────────────── + /** - * Extract a JSON object from free-form model text. - * - * Strategy: - * 1. Whole string is JSON → parse it. - * 2. Contains a ```json fence → parse the fence content. - * 3. Contains a balanced `{ … }` → parse that substring. - * 4. Give up → return null. + * Parse the `tool_calls` array from an OpenAI-compatible chat completion + * response. Looks for our `execute_automator_actions` call and extracts + * its `{ explanation, actions }` arguments. */ +export function parseToolCallResponse( + toolCalls: unknown[], + textContent?: string +): ParsedAutomatorResponse { + if (!Array.isArray(toolCalls) || toolCalls.length === 0) { + return { explanation: "", actions: [], invalidActionCount: 0, isStructured: false }; + } + + const call = toolCalls.find( + (tc: any) => tc?.function?.name === TOOL_NAME + ) as any; + + if (!call?.function?.arguments) { + return { explanation: "", actions: [], invalidActionCount: 0, isStructured: false }; + } + + try { + const args = + typeof call.function.arguments === "string" + ? JSON.parse(call.function.arguments) + : call.function.arguments; + + let explanation = typeof args.explanation === "string" ? args.explanation : ""; + if (textContent && explanation) { + explanation = textContent + "\n\n" + explanation; + } else if (textContent && !explanation) { + explanation = textContent; + } + + const rawActions = Array.isArray(args.actions) ? args.actions : []; + const actions: AutomatorAction[] = []; + let invalidCount = 0; + + for (const a of rawActions) { + if (a && typeof a === "object" && typeof a.action === "string") { + actions.push(a as AutomatorAction); + } else { + invalidCount++; + } + } + + return { + explanation: explanation || (actions.length > 0 ? "" : ""), + actions, + invalidActionCount: invalidCount, + isStructured: true, + }; + } catch { + return { explanation: "", actions: [], invalidActionCount: 0, isStructured: false }; + } +} + +// ──────────────────────────────────────────────────────────────────────── +// 2. LEGACY TEXT PATH (backward compatibility) +// ──────────────────────────────────────────────────────────────────────── + function extractJson(raw: string): Record | null { if (!raw) return null; const trimmed = raw.trim(); @@ -69,6 +133,11 @@ function extractJson(raw: string): Record | null { return null; } +/** + * Legacy parser: extract `{ explanation, actions }` from free-form model + * text. Kept for backward compatibility with queries that don't pass + * `tools` yet. + */ export function parseAutomatorResponse(raw: string): ParsedAutomatorResponse { const fallback: ParsedAutomatorResponse = { explanation: raw ?? "", @@ -81,14 +150,12 @@ export function parseAutomatorResponse(raw: string): ParsedAutomatorResponse { const obj = extractJson(raw); if (!obj) return fallback; - // Normalise explanation (string, array of strings, or other) let explanation = ""; const e = obj.explanation; if (typeof e === "string") explanation = e; else if (Array.isArray(e)) explanation = e.filter((x) => typeof x === "string").map((x) => `- ${x}`).join("\n"); else if (e != null) explanation = JSON.stringify(e); - // Accept any actions that have an `action` string field const rawActions = Array.isArray(obj.actions) ? obj.actions : []; const actions: AutomatorAction[] = []; let invalidCount = 0; @@ -107,3 +174,28 @@ export function parseAutomatorResponse(raw: string): ParsedAutomatorResponse { isStructured: true, }; } + +// ──────────────────────────────────────────────────────────────────────── +// 3. UNIFIED ENTRY POINT +// ──────────────────────────────────────────────────────────────────────── + +/** + * Parse a model response, trying the tool-calls path first and falling + * back to legacy text extraction. + * + * @param response - The raw message object returned by the user's JS query. + * Expected shape: `{ content?: string; tool_calls?: any[] }` + */ +export function parseResponse(response: { + content?: string; + tool_calls?: unknown[]; +}): ParsedAutomatorResponse { + const { content, tool_calls } = response; + + if (Array.isArray(tool_calls) && tool_calls.length > 0) { + const result = parseToolCallResponse(tool_calls, content || undefined); + if (result.isStructured) return result; + } + + return parseAutomatorResponse(content || ""); +} diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts index ffac6565d4..abfd015ca5 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts @@ -20,31 +20,22 @@ export const AUTOMATOR_SYSTEM_PROMPT = ` You are the Lowcoder Automator — an embedded assistant inside the Lowcoder visual app builder. Your job is to translate natural-language requests from a -human builder into a sequence of structured UI actions that the runtime will -execute on the canvas. +human builder into structured UI actions that the runtime will execute on the +canvas. -# Output contract (NON-NEGOTIABLE) +# How to respond -Reply with ONE single raw JSON object. No prose outside JSON, no markdown -fences, no commentary. The object MUST have exactly two top-level keys: - -{ - "explanation": "", - "actions": [ /* zero or more actions, see the action catalog */ ] -} - -If the user request is ambiguous, vague, or you don't have enough info: - - return "actions": [] - - in "explanation" describe a bullet-point plan and ask for confirmation. - - DO NOT invent components or guess. +You have a tool called \`execute_automator_actions\`. Use it when you are +ready to modify the canvas. When the request is ambiguous or you need +clarification, respond with plain text instead — do NOT call the tool with +an empty actions array. If the user explicitly says "go ahead", "do it", "build it", "implement", -or similar approval, then emit the actions array. +or similar approval after a clarification round, call the tool. # How to use the live context -The user message is preceded by a JSON block titled "EDITOR_CONTEXT". It +The system message includes a JSON block titled "EDITOR_CONTEXT". It contains: - canvas: grid columns, row height, max width - selected: currently selected component name (may be null) @@ -61,11 +52,10 @@ Use this context to: # How to use the action catalog -After "EDITOR_CONTEXT" you will see a JSON block titled "ACTIONS_CATALOG" -listing the EXACT set of actions you may emit, with their required and -optional fields. You MUST NOT use any action or component type that is not -listed there. If something is not possible with the catalog, say so in -"explanation" and emit an empty "actions" array. +You will also see a JSON block titled "ACTIONS_CATALOG" listing the EXACT +set of actions you may emit, with their required and optional fields. You +MUST NOT use any action or component type that is not listed there. If +something is not possible with the catalog, explain why in plain text. # Layout rules (short) @@ -90,7 +80,6 @@ listed there. If something is not possible with the catalog, say so in # Reminders -- Output JSON ONLY. No \`\`\`json fences. No leading/trailing text. - All field names match the catalog exactly (snake_case where shown). - Every action MUST include \`action\` and (when relevant) \`component\` and \`component_name\`. diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts new file mode 100644 index 0000000000..0e875ca6ac --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts @@ -0,0 +1,122 @@ +// client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts + +/** + * Generates OpenAI-compatible tool (function-calling) definitions from + * the ACTIONS_CATALOG. + * + * Instead of asking the model to emit raw JSON inside its text content + * (fragile, needs custom parsing), we register a single tool — + * `execute_automator_actions` — that the model **calls** when it wants + * to mutate the canvas. The API guarantees `tool_calls[].function.arguments` + * is valid JSON, so no balanced-brace extraction or fence-stripping needed. + * + * When the model needs clarification it simply responds with text (no tool + * call), which naturally replaces the old `"actions": []` convention. + * + * The tool definition is provider-agnostic: OpenAI, Groq, Together, and + * Ollama all accept the same `tools` shape. Anthropic needs a small + * remapping (documented in README.md). + */ + +import { ACTIONS_CATALOG } from "./actionsCatalog"; + +export interface OpenAIToolDefinition { + type: "function"; + function: { + name: string; + description: string; + parameters: Record; + }; +} + +function buildActionItemSchema(): Record { + return { + type: "object", + properties: { + action: { + type: "string", + enum: ACTIONS_CATALOG.map((a) => a.action), + description: "The action to perform on the canvas.", + }, + component: { + type: "string", + description: + "Component type (e.g. 'button', 'input', 'table'). Required for place_component and nest_component.", + }, + component_name: { + type: "string", + description: "Unique name for the component on the canvas.", + }, + parent_component_name: { + type: "string", + description: + "Parent container path for nest_component (e.g. 'form1.container.body.0.view').", + }, + layout: { + type: "object", + properties: { + x: { type: "number", description: "Grid column position (0-based)." }, + y: { type: "number", description: "Grid row position." }, + w: { type: "number", description: "Width in grid columns." }, + h: { type: "number", description: "Height in grid rows." }, + }, + description: "Grid layout position and size.", + }, + action_parameters: { + type: "object", + description: + "Action-specific parameters (properties, styles, event config, etc.). Shape depends on the action and component type — see the component catalog in the system prompt.", + }, + }, + required: ["action"], + }; +} + +/** + * Build the OpenAI `tools` array to pass alongside `messages` in the + * chat-completions request. Currently returns a single tool; the array + * wrapper keeps the door open for future per-action tools if we want + * tighter per-action schemas. + */ +export function buildToolDefinitions(): OpenAIToolDefinition[] { + const actionSummary = ACTIONS_CATALOG.map( + (a) => ` - ${a.action}: ${a.purpose}` + ).join("\n"); + + return [ + { + type: "function", + function: { + name: "execute_automator_actions", + description: [ + "Execute one or more Lowcoder Automator actions on the canvas.", + "Call this tool when you want to place, configure, style, move,", + "resize, delete, or otherwise modify components in the app.", + "Do NOT call this tool when you need clarification — just respond", + "with text instead.", + "", + "Available actions:", + actionSummary, + ].join("\n"), + parameters: { + type: "object", + properties: { + explanation: { + type: "string", + description: + "Brief markdown summary of what you are doing and why.", + }, + actions: { + type: "array", + description: "Ordered list of actions to execute on the canvas.", + items: buildActionItemSchema(), + }, + }, + required: ["explanation", "actions"], + }, + }, + }, + ]; +} + +export const TOOL_NAME = "execute_automator_actions"; From 18944621b854ace6f643ad12d2d3431d599e32ab Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Sat, 9 May 2026 02:11:04 +0500 Subject: [PATCH 05/33] add more components support + remove unecessary code --- .../comps/chatComp/components/ChatPanel.tsx | 33 +- .../components/ChatPanelContainer.tsx | 32 +- .../chatComp/handlers/messageHandlers.ts | 331 +++++++++--------- .../comps/comps/chatComp/types/chatTypes.ts | 30 +- .../preLoadComp/actions/automator/README.md | 285 --------------- .../actions/automator/componentCatalog.ts | 294 ++++++++++++++-- .../actions/automator/editorSnapshot.ts | 44 ++- .../preLoadComp/actions/automator/index.ts | 32 -- .../actions/automator/orchestrator.ts | 14 +- .../actions/automator/responseParser.ts | 201 ----------- .../actions/automator/systemPrompt.ts | 11 +- .../actions/automator/toolDefinitions.ts | 13 +- .../src/pages/editor/bottom/BottomPanel.tsx | 98 ++---- 13 files changed, 549 insertions(+), 869 deletions(-) delete mode 100644 client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/README.md delete mode 100644 client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/responseParser.ts 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 6c861e2156..b52359995d 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx @@ -11,24 +11,18 @@ import "@assistant-ui/styles/index.css"; import "@assistant-ui/styles/markdown.css"; // ============================================================================ -// CHAT PANEL - SIMPLIFIED BOTTOM PANEL (QUERY-BASED + AUTOMATOR) +// 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). // ============================================================================ -interface ExtendedChatPanelProps extends ChatPanelProps { - /** When false, send conversation history without the Automator system prompt. */ - enableAutomator?: boolean; -} - -export function ChatPanel({ - tableName, - chatQuery, - onMessageUpdate, - enableAutomator = true, -}: ExtendedChatPanelProps) { +export function ChatPanel({ + tableName, + chatQuery, + onMessageUpdate, +}: ChatPanelProps) { const editorState = useContext(EditorContext); const editorStateRef = useRef(editorState); @@ -43,14 +37,13 @@ export function ChatPanel({ const messageHandler = useMemo( () => - new AIAssistantQueryHandler({ - chatQuery, - dispatch: editorState?.rootComp?.dispatch, - getEditorState: () => editorStateRef.current, - enableAutomator, - }), - [chatQuery, editorState?.rootComp?.dispatch, enableAutomator] - ); + new AIAssistantQueryHandler({ + chatQuery, + dispatch: editorState?.rootComp?.dispatch, + getEditorState: () => editorStateRef.current, + }), + [chatQuery, editorState?.rootComp?.dispatch] + ); return ( Math.random().toString(36).substr(2, 9); -/** - * Append a small footer to the assistant message summarising what the - * Automator just did, so the human can audit at a glance. - */ -function formatAutomatorFooter(actionsCount: number, invalidCount: number): string { - if (actionsCount === 0 && invalidCount === 0) return ""; - const parts: string[] = []; - if (actionsCount > 0) { - parts.push(`${actionsCount} action${actionsCount === 1 ? "" : "s"} executed`); - } - if (invalidCount > 0) { - parts.push(`${invalidCount} skipped (unsupported)`); - } - return `\n\n_— Automator: ${parts.join(", ")}_`; -} - export interface ChatPanelContainerProps { storage: any; messageHandler: AIAssistantMessageHandler; @@ -260,17 +244,13 @@ 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, + } + }, + }; +} // ============================================================================ // QUERY HANDLER @@ -15,15 +81,16 @@ import { export class QueryHandler implements MessageHandler { constructor(private config: QueryHandlerConfig) {} - async sendMessage(message: ChatMessage): Promise { - const { chatQuery, dispatch} = this.config; - - // If no query selected or dispatch unavailable, return mock response - if (!chatQuery || !dispatch) { - console.log("No query selected or dispatch unavailable, returning mock response"); - 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); @@ -32,11 +99,11 @@ export class QueryHandler implements MessageHandler { 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 }, + }, }) ) ); @@ -51,162 +118,100 @@ export class QueryHandler implements MessageHandler { // ============================================================================ // AI ASSISTANT QUERY HANDLER (bottom panel) // ---------------------------------------------------------------------------- -// This is the heart of the Lowcoder Automator. On every send it: -// 1. snapshots the current editor state (components, queries, canvas), -// 2. composes a lean system prompt + actions catalog + live context, -// 3. forwards the enriched `messages` array (and a few extras) to the -// user-defined Lowcoder query (typically a JS query that calls an LLM -// via an HTTP query), -// 4. parses the model's text reply back into `{ explanation, actions }`, -// 5. returns both — the chat panel renders `explanation` and dispatches -// `actions` against the editor. -// ============================================================================ +// 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) {} - async sendMessage( - message: ChatMessage, - sessionId?: string, - conversationHistory?: ChatMessage[] - ): Promise { - const { chatQuery, dispatch, getEditorState, enableAutomator = true } = this.config; - const history = conversationHistory ?? [message]; - - // Conversation history in the OpenAI {role, content} shape. - const rawHistory = history.map((msg) => ({ + 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, })); - // Build the Automator payload. When the editor state is unavailable - // (eg. mock setup) we still get a valid (empty) snapshot so the prompt - // is consistent. - const editorState = getEditorState ? getEditorState() : null; - const payload = buildAutomatorPayload({ - history: rawHistory, - editorState, - withSystemPrompt: enableAutomator, - }); - - if (!chatQuery || !dispatch) { - console.log( - "[Automator] No query selected or dispatch unavailable, returning mock" - ); - await new Promise((res) => setTimeout(res, 300)); - return { - content: - "(mock) Connect a query in the AI Assistant header to enable the Automator.\n\nYou typed: " + - message.text, - }; - } - - try { - console.log("[Automator] running query:", chatQuery, { - contextComponents: payload.context.components.length, + 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, }); const result: any = await getPromiseAfterDispatch( dispatch, - routeByNameAction( - chatQuery, - executeQueryAction({ - args: { - // ---- Backward-compatible fields (don't break old test queries) - message: { value: message }, - prompt: { value: message.text }, - sessionId: { value: sessionId }, - conversationHistory: { value: history }, - messages: { value: payload.messages }, - - // ---- Tool calling: the JS query should forward this to the - // HTTP body so the LLM can call `execute_automator_actions` - tools: { value: payload.tools }, - - // ---- Extra fields for power users - system: { value: payload.system }, - context: { value: payload.context }, - actionsCatalog: { value: payload.actionsCatalog }, - componentCatalog: { value: payload.componentCatalog }, - messagesWithoutSystem: { value: rawHistory }, - }, - }) - ) - ); - - // The query may return tool_calls (new path) or plain content (legacy). - // `parseResponse` tries tool_calls first, then falls back to text JSON - // extraction, so old queries that haven't been updated keep working. - const raw = result?.message ?? result ?? {}; - const content: string = - typeof raw === "string" - ? raw - : typeof raw.content === "string" - ? raw.content - : typeof raw === "object" && !raw.tool_calls - ? JSON.stringify(raw) - : ""; - const toolCalls: unknown[] | undefined = raw?.tool_calls; - - const parsed = parseResponse({ content, tool_calls: toolCalls }); - - const displayText = - parsed.isStructured && parsed.explanation - ? parsed.explanation - : content; - - console.log("[Automator] parsed", { - isStructured: parsed.isStructured, - actions: parsed.actions.length, - invalid: parsed.invalidActionCount, - }); - - return { - content: displayText, - actions: parsed.actions, - automator: { - isStructured: parsed.isStructured, - explanation: parsed.explanation, - invalidActionCount: parsed.invalidActionCount, - }, - }; - } catch (e: any) { - throw new Error(e?.message || "AI assistant query execution failed"); - } + 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"); + } } } -// ============================================================================ -// MOCK HANDLER (for testing/fallbacks) -// ============================================================================ - -export class MockHandler implements MessageHandler { - constructor(private delay: number = 1000) {} - - async sendMessage(message: ChatMessage): Promise { - await new Promise(resolve => setTimeout(resolve, this.delay)); - return { content: `Mock response: ${message.text}` }; - } -} - -// ============================================================================ -// HANDLER FACTORY (creates the right handler based on type) -// ============================================================================ - -export function createMessageHandler( - type: "query" | "mock", - config: QueryHandlerConfig -): MessageHandler { - switch (type) { - case "query": - return new QueryHandler(config); - - 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 4fcb734ec0..bbc221e337 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts @@ -42,11 +42,11 @@ 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, conversationHistory?: ChatMessage[]): Promise; - } + } + + export interface AIAssistantMessageHandler { + sendMessage(message: ChatMessage, sessionId: string | undefined, conversationHistory: ChatMessage[]): Promise; + } export interface MessageResponse { content: string; @@ -71,20 +71,12 @@ export interface ChatMessage { 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. - * Optional — when missing the Automator falls back to a context-less - * passthrough (legacy behaviour). - */ - getEditorState?: () => any; - /** - * When false, the handler skips injecting the Automator system prompt - * and just forwards `messages` (the conversation history) as-is. Useful - * for plain ChatGPT-style queries that don't drive the canvas. - */ - enableAutomator?: boolean; - } + /** + * 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) diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/README.md b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/README.md deleted file mode 100644 index cabf8ea081..0000000000 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/README.md +++ /dev/null @@ -1,285 +0,0 @@ -# Lowcoder Automator - -A query-driven, context-aware AI assistant that builds Lowcoder apps for you. - -The Automator lives in the bottom panel under **Lowcoder AI**. You pick a -Lowcoder query that talks to your favourite LLM (OpenAI, Anthropic, Ollama, -…), type a request in plain English, and it returns a structured set of -actions that mutate the canvas — placing components, configuring forms, -nesting modals, and so on. - ---- - -## How the flow works (tool-calling) - -``` - ┌────────────────────────────┐ - │ User types in chat panel │ - └────────────┬───────────────┘ - │ - ▼ - ┌────────────────────────────────────────────────────┐ - │ buildAutomatorPayload() │ - │ - snapshot of current editor (components/queries) │ - │ - lean system prompt + actions catalog │ - │ - curated component cheatsheet │ - │ - conversation history │ - │ - OpenAI tool definitions (execute_automator_…) │ - └────────────┬───────────────────────────────────────┘ - │ messages, tools, context, … - ▼ - ┌────────────────────────────────────────────────────┐ - │ YOUR Lowcoder JS query (the "model bridge") │ - │ Forwards messages + tools to an HTTP query │ - └────────────┬───────────────────────────────────────┘ - │ { message: { role, content, tool_calls } } - ▼ - ┌────────────────────────────────────────────────────┐ - │ parseResponse() │ - │ 1. tool_calls present? → extract actions (clean) │ - │ 2. fallback → legacy JSON text extraction │ - └────────────┬───────────────────────────────────────┘ - │ - ▼ - ┌────────────────────────────────────────────────────┐ - │ ChatPanelContainer.performAction() │ - │ - dispatches each action through the existing │ - │ add/nest/move/resize/configure executors │ - └────────────────────────────────────────────────────┘ -``` - -The model uses **tool calling** (function calling) instead of embedding -JSON in its text. When the model wants to act on the canvas, it calls the -`execute_automator_actions` tool with `{ explanation, actions }` — the API -guarantees valid JSON. When it needs clarification, it responds with plain -text (no tool call). No custom parsing needed. - -Everything is **client-side**. The only thing you wire on the backend is -the LLM HTTP call — through a regular Lowcoder data query. - ---- - -## Setting up the model bridge (queries) - -You need exactly **two queries** in your app: one HTTP query that talks to -the LLM, and one JS query that the Automator panel calls. - -### 1. The HTTP query — `llmHttp` - -| Field | Value | -| --- | --- | -| Method | `POST` | -| URL | `https://api.openai.com/v1/chat/completions` | -| Headers | `Authorization: Bearer YOUR_KEY`, `Content-Type: application/json` | -| Body | see below | - -Body (raw JSON, with Lowcoder bindings): - -```json -{ - "model": "gpt-4o-mini", - "temperature": 0.2, - "messages": {{ messages.value }}, - "tools": {{ tools.value }} -} -``` - -> The `tools` parameter tells the model about `execute_automator_actions`. -> The model decides when to call it vs. when to respond with plain text. - -#### Ollama variant - -```json -{ - "model": "llama3.1", - "stream": false, - "messages": {{ messages.value }}, - "tools": {{ tools.value }} -} -``` -URL: `http://localhost:11434/api/chat` - -#### Anthropic variant - -Anthropic uses a slightly different tool format. Map the OpenAI tool -definition to Anthropic's `tools` shape in your JS query: - -```json -{ - "model": "claude-3-5-sonnet-latest", - "max_tokens": 4096, - "system": "{{ system.value }}", - "messages": {{ messagesWithoutSystem.value }}, - "tools": [{ - "name": "execute_automator_actions", - "description": "Execute Lowcoder Automator actions on the canvas.", - "input_schema": {{ JSON.stringify(tools.value[0].function.parameters) }} - }] -} -``` -URL: `https://api.anthropic.com/v1/messages`, -headers: `x-api-key: YOUR_KEY`, `anthropic-version: 2023-06-01`. - -### 2. The JS query — `assistantBridge` - -This is the query you select in the Automator panel's "Query:" dropdown. -It forwards messages + tools to your HTTP query and returns the response. - -```js -return llmHttp.run({ - messages: messages.value, - tools: tools.value, -}).then((data) => { - const msg = data?.choices?.[0]?.message; - return { - message: { - role: "assistant", - content: msg?.content || "", - tool_calls: msg?.tool_calls || [], - }, - }; -}); -``` - -Now in the bottom panel, switch to **Lowcoder AI**, pick `assistantBridge` -in the Query dropdown, and start chatting. The **Automator** toggle next to -the dropdown controls whether the system prompt + live context + tools are -injected (default: ON). - -### Legacy setup (still works) - -If you have existing queries that don't pass `tools` and rely on the model -embedding JSON in its text content, they still work. The parser falls back -to the old text-extraction logic automatically. But the tool-calling path -is recommended — it's more reliable and simpler to set up. - ---- - -## What gets sent to your JS query - -Inside the JS query you can use any of these args: - -| Arg | What it is | -| --- | --- | -| `messages` | Final OpenAI-style message array, prefixed with the Automator system prompt and live editor context. **The default and recommended choice.** | -| `tools` | OpenAI-compatible tool definitions array. Pass this to the HTTP body alongside `messages`. | -| `messagesWithoutSystem` | Same array minus the leading `system` message. Use with Anthropic. | -| `system` | The composed system prompt string by itself. | -| `context` | The live editor snapshot (components, queries, canvas grid, selected). | -| `actionsCatalog` | The catalog of allowed actions. | -| `componentCatalog` | The curated cheatsheet of component shapes. | -| `prompt` | The latest user message text only. | -| `conversationHistory` | The full ChatMessage history including IDs/timestamps. | -| `sessionId` | Current chat thread id (useful for server-side memory). | -| `message` | The full latest user `ChatMessage` object. | - ---- - -## What the model returns - -### With tool calling (recommended) - -When the model wants to act, it returns a `tool_calls` array: - -```json -{ - "choices": [{ - "message": { - "role": "assistant", - "content": "I'll create a basic Todo app with a title, input, button, and table.", - "tool_calls": [{ - "id": "call_abc123", - "type": "function", - "function": { - "name": "execute_automator_actions", - "arguments": "{\"explanation\":\"Creating a Todo app...\",\"actions\":[{\"action\":\"place_component\",\"component\":\"text\",\"component_name\":\"todoTitle\",\"layout\":{\"x\":0,\"y\":0,\"w\":24,\"h\":4},\"action_parameters\":{\"text\":\"## My Todos\",\"type\":\"markdown\"}}]}" - } - }] - } - }] -} -``` - -When the model needs clarification, it responds with just text (no tool calls): - -```json -{ - "choices": [{ - "message": { - "role": "assistant", - "content": "I can build that for you. Would you like:\n- A simple table view?\n- A kanban board layout?\n\nPlease confirm and I'll proceed." - } - }] -} -``` - -### Legacy (text JSON) - -A single JSON object in the text content: - -```json -{ - "explanation": "Created a basic Todo app.", - "actions": [...] -} -``` - ---- - -## Supported actions - -See `actionsCatalog.ts` for the full list with examples. Summary: - -| Action | Purpose | -| --- | --- | -| `place_component` | New component on the root canvas | -| `nest_component` | New component inside an existing container | -| `move_component` | Reposition an existing component | -| `resize_component` | Resize an existing component | -| `delete_component` | Remove an existing component | -| `rename_component` | Rename an existing component | -| `align_component` | Align a component (left / center / right) | -| `set_properties` | Update properties on an existing component | -| `set_style` | Apply visual styles to a component | -| `add_event_handler` | Add an event handler (click, change, etc.) | -| `set_theme` | Apply a theme | -| `set_app_metadata` | Update app title / description | -| `set_canvas_setting` | Update grid columns / row height / max width | -| `set_global_css` | Set global CSS for the app | -| `set_global_javascript` | Set global JS that runs on load | -| `publish_app` | Publish the app for end-users | - -Adding a new action is a two-step change: -1. Add an entry to the `ACTION_REGISTRY` map in `ChatPanelContainer.tsx`. -2. Add an entry to `ACTIONS_CATALOG` in `actionsCatalog.ts`. - ---- - -## Test plan / quick smoke - -1. In a fresh app, create the two queries above (`llmHttp`, `assistantBridge`). -2. Open the bottom panel → **Lowcoder AI** → pick `assistantBridge`. -3. Type: **`build a basic todo app`**. -4. Expect: a brief explanation + 3-5 components placed on the canvas. -5. Type: **`add a delete button column to the table`** → it should reuse the - existing `todoTable` name (this is the "context awareness" win). -6. Toggle **Automator** off → send a message → the JS query receives only - the raw conversation history, no tools, no system prompt (useful for - plain ChatGPT-style flows). - ---- - -## Architecture - -| File | Purpose | -| --- | --- | -| `systemPrompt.ts` | Short, stable rules for the model | -| `actionsCatalog.ts` | Machine-readable list of all supported actions | -| `componentCatalog.ts` | Curated cheatsheet (only relevant types sent per turn) | -| `editorSnapshot.ts` | Live context from `EditorState` (components, queries, canvas) | -| `toolDefinitions.ts` | OpenAI-compatible tool definitions for function calling | -| `responseParser.ts` | Dual-path parser: tool_calls (clean) → text fallback (legacy) | -| `orchestrator.ts` | Assembles system + context + history + tools into the payload | - -`ChatPanelContainer.tsx` holds the `ACTION_REGISTRY` — a simple map from -action names to executor functions. diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts index b57cced1ef..1eb6e23f1b 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts @@ -1,18 +1,14 @@ // client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts +import { uiCompRegistry, type UICompManifest } from "comps/uiCompRegistry"; + /** - * Curated, *minimal* reference of the component types the model is most - * likely to need when assembling small apps (todo, CRUD, login, dashboard). - * - * The legacy `Latest_prompt.md` shipped a 4.7K-line catalog covering every - * component. That blew up token budgets and led the model to invent fields. - * Here we keep just the essentials, with curated default property shapes - * derived from the legacy doc and the live `defaultDataFn` runtime values. + * Component reference for the Automator. * - * The catalog is deliberately *additive*: callers can call - * `getComponentCatalog()` to get the default subset, or - * `getComponentCatalog([...extra])` to inject additional types they know - * the user wants (e.g. via UI hints). + * Curated entries provide known-good property examples for common components. + * Every registered Lowcoder component is then added from `uiCompRegistry` so + * the Automator can discover the full insertion panel, including newer + * components like Chat, Chat Box, and Chat Controller. */ export interface ComponentCatalogEntry { @@ -30,8 +26,132 @@ export interface ComponentCatalogEntry { example: Record; /** Notes the model should heed. */ notes?: string; + /** Display name shown in the Lowcoder component panel. */ + name?: string; + /** English display name from the component manifest. */ + enName?: string; + /** Component panel categories. Empty means hidden from normal insertion UI. */ + categories?: readonly string[]; + /** Short manifest description when it is serialisable. */ + description?: string; } +export const LOWCODER_COMPONENT_TYPES: string[] = [ + "chart", + "basicChart", + "barChart", + "lineChart", + "pieChart", + "scatterChart", + "candleStickChart", + "funnelChart", + "gaugeChart", + "graphChart", + "heatmapChart", + "radarChart", + "sankeyChart", + "sunburstChart", + "themeriverChart", + "treeChart", + "treemapChart", + "openLayersGeoMap", + "chartsGeoMap", + "table", + "tableLite", + "pivotTable", + "mermaid", + "timeline", + "responsiveLayout", + "pageLayout", + "columnLayout", + "splitLayout", + "floatTextContainer", + "card", + "tabbedContainer", + "collapsibleContainer", + "container", + "listView", + "grid", + "multiTags", + "modal", + "drawer", + "toast", + "divider", + "navigation", + "step", + "cascader", + "link", + "floatingButton", + "calendar", + "timer", + "sharingcomponent", + "videocomponent", + "meeting", + "avatar", + "avatarGroup", + "comment", + "mention", + "chatController", + "chatBox", + "form", + "jsonSchemaForm", + "jsonEditor", + "jsonExplorer", + "richTextEditor", + "input", + "password", + "numberInput", + "textArea", + "autocomplete", + "switch", + "checkbox", + "radio", + "date", + "dateRange", + "time", + "timeRange", + "slider", + "rangeSlider", + "button", + "controlButton", + "dropdown", + "toggleButton", + "segmentedControl", + "rating", + "ganttChart", + "kanban", + "hillchart", + "bpmnEditor", + "progress", + "progressCircle", + "file", + "fileViewer", + "image", + "carousel", + "audio", + "video", + "shape", + "jsonLottie", + "icon", + "imageEditor", + "colorPicker", + "qrCode", + "scanner", + "signature", + "select", + "tour", + "multiSelect", + "tree", + "treeSelect", + "transfer", + "turnstileCaptcha", + "chat", + "iframe", + "custom", + "module", + "text", +]; + const TEXT: ComponentCatalogEntry = { type: "text", defaultLayout: { w: 12, h: 4 }, @@ -435,7 +555,37 @@ const RADIO: ComponentCatalogEntry = { }, }; -const DEFAULT_CATALOG: ComponentCatalogEntry[] = [ +const CHAT: ComponentCatalogEntry = { + type: "chat", + defaultLayout: { w: 12, h: 20 }, + required: [], + optional: ["chatQuery", "tableName", "placeholder"], + example: { + tableName: "LC_AI", + placeholder: "Ask anything...", + }, + notes: "AI chat component for embedding a conversational assistant in the app.", +}; + +const CHAT_BOX: ComponentCatalogEntry = { + type: "chatBox", + defaultLayout: { w: 12, h: 24 }, + required: [], + optional: ["messages", "controller", "placeholder"], + example: {}, + notes: "Chat UI for displaying messages and sending user input. Pair with chatController for realtime typing/presence.", +}; + +const CHAT_CONTROLLER: ComponentCatalogEntry = { + type: "chatController", + defaultLayout: { w: 12, h: 5 }, + required: [], + optional: ["roomId"], + example: {}, + notes: "Realtime chat controller hook. Use with chatBox for presence and typing indicators.", +}; + +const CURATED_CATALOG: ComponentCatalogEntry[] = [ TEXT, BUTTON, INPUT, @@ -466,20 +616,120 @@ const DEFAULT_CATALOG: ComponentCatalogEntry[] = [ TIMELINE, STEP, DIVIDER, + CHAT, + CHAT_BOX, + CHAT_CONTROLLER, ]; +const CURATED_BY_TYPE = new Map(CURATED_CATALOG.map((entry) => [entry.type, entry])); + +function serialiseDescription(description: UICompManifest["description"]): string | undefined { + if (typeof description === "string") return description; + if (typeof description === "number") return String(description); + return undefined; +} + +function fallbackEntry(type: string, manifest: UICompManifest): ComponentCatalogEntry { + const layout = manifest.layoutInfo ?? { w: 6, h: 5 }; + return { + type, + name: manifest.name, + enName: manifest.enName, + categories: manifest.categories, + description: serialiseDescription(manifest.description), + isContainer: manifest.isContainer, + defaultLayout: { + w: layout.w, + h: layout.h, + }, + required: [], + optional: [], + example: {}, + notes: + "Registered Lowcoder component. Use an empty action_parameters object when no property shape is listed, or set properties afterward with set_properties.", + }; +} + +function typeOnlyFallbackEntry(type: string): ComponentCatalogEntry { + const curated = CURATED_BY_TYPE.get(type); + if (curated) return curated; + + return { + type, + defaultLayout: { w: 6, h: 5 }, + required: [], + optional: [], + example: {}, + notes: + "Lowcoder component listed in comps/index.tsx. Use an empty action_parameters object when no property shape is listed, or set properties afterward with set_properties.", + }; +} + +function mergeManifestMetadata( + entry: ComponentCatalogEntry, + manifest: UICompManifest +): ComponentCatalogEntry { + return { + ...entry, + name: manifest.name, + enName: manifest.enName, + categories: manifest.categories, + description: serialiseDescription(manifest.description), + isContainer: entry.isContainer ?? manifest.isContainer, + defaultLayout: entry.defaultLayout ?? manifest.layoutInfo ?? { w: 6, h: 5 }, + }; +} + +function buildFullCatalog(): ComponentCatalogEntry[] { + const registryEntries = Object.entries(uiCompRegistry); + const registryTypes = new Set(registryEntries.map(([type]) => type)); + const knownTypes = new Set(LOWCODER_COMPONENT_TYPES); + + const listedEntries = LOWCODER_COMPONENT_TYPES.map((type) => { + const manifest = uiCompRegistry[type]; + if (!manifest) return typeOnlyFallbackEntry(type); + + const curated = CURATED_BY_TYPE.get(type); + return curated + ? mergeManifestMetadata(curated, manifest) + : fallbackEntry(type, manifest); + }); + + const extraRegistryEntries = registryEntries + .filter(([type]) => !knownTypes.has(type)) + .map(([type, manifest]) => { + const curated = CURATED_BY_TYPE.get(type); + return curated + ? mergeManifestMetadata(curated, manifest) + : fallbackEntry(type, manifest); + }); + + const curatedOnlyEntries = CURATED_CATALOG.filter( + (entry) => !registryTypes.has(entry.type) && !knownTypes.has(entry.type) + ); + + return [...listedEntries, ...extraRegistryEntries, ...curatedOnlyEntries].sort((a, b) => { + const categoryA = a.categories?.[0] ?? ""; + const categoryB = b.categories?.[0] ?? ""; + if (categoryA !== categoryB) return categoryA.localeCompare(categoryB); + return (a.enName || a.name || a.type).localeCompare(b.enName || b.name || b.type); + }); +} + /** - * Returns the curated component catalog. If `onlyTypes` is provided we only - * include those entries (useful when the user already mentioned specific - * components and we want to keep token usage low). + * Returns all registered Lowcoder components. If `onlyTypes` is provided we + * return those entries first and keep the remaining catalog afterward, so the + * model sees the user's requested component names without losing access to the + * rest of Lowcoder's palette. */ export function getComponentCatalog(onlyTypes?: string[]): ComponentCatalogEntry[] { - if (!onlyTypes || onlyTypes.length === 0) return DEFAULT_CATALOG; - const set = new Set(onlyTypes); - const filtered = DEFAULT_CATALOG.filter((c) => set.has(c.type)); - return filtered.length > 0 ? filtered : DEFAULT_CATALOG; + const fullCatalog = buildFullCatalog(); + if (!onlyTypes || onlyTypes.length === 0) return fullCatalog; + + const requested = new Set(onlyTypes); + const mentioned = fullCatalog.filter((c) => requested.has(c.type)); + const remaining = fullCatalog.filter((c) => !requested.has(c.type)); + return mentioned.length > 0 ? [...mentioned, ...remaining] : fullCatalog; } -export const COMPONENT_TYPES_DEFAULT: string[] = DEFAULT_CATALOG.map( - (c) => c.type -); +export const COMPONENT_TYPES_DEFAULT: string[] = buildFullCatalog().map((c) => c.type); diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/editorSnapshot.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/editorSnapshot.ts index a49136160a..de7575f077 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/editorSnapshot.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/editorSnapshot.ts @@ -1,6 +1,8 @@ // client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/editorSnapshot.ts import type { EditorState } from "@lowcoder-ee/comps/editorState"; +import { uiCompRegistry } from "comps/uiCompRegistry"; +import { LOWCODER_COMPONENT_TYPES } from "./componentCatalog"; /** * A compact, JSON-serialisable view of the live editor state. @@ -111,9 +113,9 @@ export function buildEditorSnapshot(editorState: EditorState | null | undefined) const settings = safe(() => editorState.getAppSettings(), {} as any); - const components: ComponentSnapshot[] = safe( + const components = safe( () => - editorState.uiCompInfoList().map((info: any) => { + editorState.uiCompInfoList().map((info: any): ComponentSnapshot => { // The layout x/y/w/h lives on the rootComp's layout map, keyed by // the same key as `getAllUICompMap`. We don't have a direct lookup // here without scanning, so we leave layout undefined for now and @@ -124,15 +126,15 @@ export function buildEditorSnapshot(editorState: EditorState | null | undefined) hints: pickHints(info.data), }; }), - [] as ComponentSnapshot[] + [] ); // Try to enrich with layout positions from the root grid. safe(() => { - const ui = editorState.getUIComp(); - const comp = ui?.children?.comp; - const layoutObj = comp?.children?.layout?.getView?.() ?? {}; - const items = comp?.children?.items?.children ?? {}; + const uiComp: any = editorState.getUIComp(); + const compChildren = uiComp?.children?.comp?.children; + const layoutObj = compChildren?.layout?.getView?.() ?? {}; + const items = compChildren?.items?.children ?? {}; const byName: Record = {}; for (const [key, layout] of Object.entries(layoutObj)) { const item: any = items[key]; @@ -207,21 +209,21 @@ export function buildEditorSnapshot(editorState: EditorState | null | undefined) * free-form prompt. Used to slim down the component catalog we send to the * model so it stays under token budgets. * - * Returns an empty list if no obvious match — caller should fall back to - * the default curated catalog. + * Returns an empty list if no obvious match — caller should keep the full + * component catalog in its default registry order. */ export function inferMentionedComponentTypes(prompt: string): string[] { if (!prompt) return []; const lower = prompt.toLowerCase(); - const candidates = [ - "text", "button", "input", "numberInput", "textArea", "password", - "select", "checkbox", "radio", "switch", "slider", "rating", "date", - "form", "container", "modal", "drawer", - "table", "listView", "card", "tabbedContainer", - "image", "video", "avatar", "chart", - "progress", "navigation", "timeline", "step", "divider", - ]; + const registryTypes = Object.keys(uiCompRegistry); + const candidates = Array.from(new Set([...LOWCODER_COMPONENT_TYPES, ...registryTypes])); const aliases: Record = { + chatbox: "chatBox", + "chat box": "chatBox", + "chat-box": "chatBox", + "chat controller": "chatController", + "chat-controller": "chatController", + "ai chat": "chat", dropdown: "select", "list view": "listView", "list-view": "listView", @@ -243,7 +245,13 @@ export function inferMentionedComponentTypes(prompt: string): string[] { }; const found = new Set(); for (const c of candidates) { - if (lower.includes(c.toLowerCase())) found.add(c); + const manifest = uiCompRegistry[c]; + const names = [ + c, + manifest?.name, + manifest?.enName, + ].filter(Boolean) as string[]; + if (names.some((name) => lower.includes(name.toLowerCase()))) found.add(c); } for (const [alias, real] of Object.entries(aliases)) { if (lower.includes(alias)) found.add(real); diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts index 64708ed1e9..76ebcb3900 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts @@ -19,13 +19,6 @@ export { type ComponentSnapshot, type QuerySnapshot, } from "./editorSnapshot"; -export { - parseAutomatorResponse, - parseToolCallResponse, - parseResponse, - type ParsedAutomatorResponse, - type AutomatorAction, -} from "./responseParser"; export { buildAutomatorPayload, type LLMMessage, @@ -37,28 +30,3 @@ export { TOOL_NAME, type OpenAIToolDefinition, } from "./toolDefinitions"; - -/** - * Quick-start guide — see automator/README.md for full details. - * - * 1. Create an HTTP query (e.g. "llmQuery") pointing at your model endpoint. - * Include `"tools": {{ tools.value }}` in the request body. - * - * 2. Create a JS query (e.g. "aiQuery") that calls the HTTP query: - * - * return llmQuery.run({ - * messages: messages.value, - * tools: tools.value, - * }).then((data) => { - * const msg = data?.choices?.[0]?.message; - * return { - * message: { - * role: "assistant", - * content: msg?.content || "", - * tool_calls: msg?.tool_calls || [], - * }, - * }; - * }); - * - * 3. In the bottom panel, click the AI tab, select "aiQuery", and chat. - */ diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts index c0417bc97d..3f4f855119 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts @@ -29,8 +29,6 @@ export interface OrchestratorInput { history: { role: "user" | "assistant"; content: string }[]; /** Live editor state — used to build the EDITOR_CONTEXT block. */ editorState: EditorState | null | undefined; - /** When false, skip injecting the system prompt entirely (raw passthrough). */ - withSystemPrompt?: boolean; } export interface OrchestratorOutput { @@ -54,12 +52,13 @@ export interface OrchestratorOutput { * not call the network. */ export function buildAutomatorPayload(input: OrchestratorInput): OrchestratorOutput { - const { history, editorState, withSystemPrompt = true } = input; + const { history, editorState } = input; const context = buildEditorSnapshot(editorState); // Slim down the component catalog based on the *latest* user message so - // we keep the prompt under control. + // the requested components appear first. We still include the full + // Lowcoder registry so the Automator can add any component from the panel. const lastUser = [...history].reverse().find((m) => m.role === "user"); const mentioned = inferMentionedComponentTypes(lastUser?.content ?? ""); const componentCatalog = getComponentCatalog(mentioned); @@ -70,12 +69,9 @@ export function buildAutomatorPayload(input: OrchestratorInput): OrchestratorOut editorContext: context, }); - const tools = withSystemPrompt ? buildToolDefinitions() : []; + const tools = buildToolDefinitions(componentCatalog.map((component) => component.type)); - const messages: LLMMessage[] = []; - if (withSystemPrompt) { - messages.push({ role: "system", content: system }); - } + const messages: LLMMessage[] = [{ role: "system", content: system }]; for (const m of history) { messages.push({ role: m.role, content: m.content }); } diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/responseParser.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/responseParser.ts deleted file mode 100644 index e319832954..0000000000 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/responseParser.ts +++ /dev/null @@ -1,201 +0,0 @@ -// client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/responseParser.ts - -/** - * Parses LLM responses into the `{ explanation, actions }` shape that the - * Automator executor expects. - * - * Two parsing paths, tried in order: - * - * 1. **Tool-calls path** (preferred) — the model called - * `execute_automator_actions` via OpenAI function-calling. The - * arguments are guaranteed-valid JSON, so parsing is trivial. - * - * 2. **Legacy text path** (fallback) — the model returned a raw JSON - * object in its text content (possibly wrapped in markdown fences or - * surrounded by prose). This path uses the same balanced-brace - * extraction that shipped before the tool-calling refactor, so - * existing queries that haven't been updated keep working. - */ - -import { TOOL_NAME } from "./toolDefinitions"; - -export interface ParsedAutomatorResponse { - explanation: string; - actions: AutomatorAction[]; - invalidActionCount: number; - isStructured: boolean; -} - -export interface AutomatorAction { - action: string; - component?: string; - component_name?: string; - parent_component_name?: string; - layout?: { x?: number; y?: number; w?: number; h?: number }; - action_parameters?: Record; - [key: string]: unknown; -} - -// ──────────────────────────────────────────────────────────────────────── -// 1. TOOL-CALLS PATH (new, clean) -// ──────────────────────────────────────────────────────────────────────── - -/** - * Parse the `tool_calls` array from an OpenAI-compatible chat completion - * response. Looks for our `execute_automator_actions` call and extracts - * its `{ explanation, actions }` arguments. - */ -export function parseToolCallResponse( - toolCalls: unknown[], - textContent?: string -): ParsedAutomatorResponse { - if (!Array.isArray(toolCalls) || toolCalls.length === 0) { - return { explanation: "", actions: [], invalidActionCount: 0, isStructured: false }; - } - - const call = toolCalls.find( - (tc: any) => tc?.function?.name === TOOL_NAME - ) as any; - - if (!call?.function?.arguments) { - return { explanation: "", actions: [], invalidActionCount: 0, isStructured: false }; - } - - try { - const args = - typeof call.function.arguments === "string" - ? JSON.parse(call.function.arguments) - : call.function.arguments; - - let explanation = typeof args.explanation === "string" ? args.explanation : ""; - if (textContent && explanation) { - explanation = textContent + "\n\n" + explanation; - } else if (textContent && !explanation) { - explanation = textContent; - } - - const rawActions = Array.isArray(args.actions) ? args.actions : []; - const actions: AutomatorAction[] = []; - let invalidCount = 0; - - for (const a of rawActions) { - if (a && typeof a === "object" && typeof a.action === "string") { - actions.push(a as AutomatorAction); - } else { - invalidCount++; - } - } - - return { - explanation: explanation || (actions.length > 0 ? "" : ""), - actions, - invalidActionCount: invalidCount, - isStructured: true, - }; - } catch { - return { explanation: "", actions: [], invalidActionCount: 0, isStructured: false }; - } -} - -// ──────────────────────────────────────────────────────────────────────── -// 2. LEGACY TEXT PATH (backward compatibility) -// ──────────────────────────────────────────────────────────────────────── - -function extractJson(raw: string): Record | null { - if (!raw) return null; - const trimmed = raw.trim(); - - if (trimmed.startsWith("{")) { - try { return JSON.parse(trimmed); } catch { /* continue */ } - } - - const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i); - if (fence?.[1]) { - try { return JSON.parse(fence[1].trim()); } catch { /* continue */ } - } - - const start = trimmed.indexOf("{"); - if (start >= 0) { - let depth = 0, inStr = false, esc = false; - for (let i = start; i < trimmed.length; i++) { - const ch = trimmed[i]; - if (esc) { esc = false; continue; } - if (ch === "\\") { esc = true; continue; } - if (ch === '"') { inStr = !inStr; continue; } - if (inStr) continue; - if (ch === "{") depth++; - else if (ch === "}" && --depth === 0) { - try { return JSON.parse(trimmed.slice(start, i + 1)); } catch { return null; } - } - } - } - - return null; -} - -/** - * Legacy parser: extract `{ explanation, actions }` from free-form model - * text. Kept for backward compatibility with queries that don't pass - * `tools` yet. - */ -export function parseAutomatorResponse(raw: string): ParsedAutomatorResponse { - const fallback: ParsedAutomatorResponse = { - explanation: raw ?? "", - actions: [], - invalidActionCount: 0, - isStructured: false, - }; - if (!raw || typeof raw !== "string") return fallback; - - const obj = extractJson(raw); - if (!obj) return fallback; - - let explanation = ""; - const e = obj.explanation; - if (typeof e === "string") explanation = e; - else if (Array.isArray(e)) explanation = e.filter((x) => typeof x === "string").map((x) => `- ${x}`).join("\n"); - else if (e != null) explanation = JSON.stringify(e); - - const rawActions = Array.isArray(obj.actions) ? obj.actions : []; - const actions: AutomatorAction[] = []; - let invalidCount = 0; - for (const a of rawActions) { - if (a && typeof a === "object" && typeof a.action === "string") { - actions.push(a as AutomatorAction); - } else { - invalidCount++; - } - } - - return { - explanation: explanation || (actions.length > 0 ? "" : raw), - actions, - invalidActionCount: invalidCount, - isStructured: true, - }; -} - -// ──────────────────────────────────────────────────────────────────────── -// 3. UNIFIED ENTRY POINT -// ──────────────────────────────────────────────────────────────────────── - -/** - * Parse a model response, trying the tool-calls path first and falling - * back to legacy text extraction. - * - * @param response - The raw message object returned by the user's JS query. - * Expected shape: `{ content?: string; tool_calls?: any[] }` - */ -export function parseResponse(response: { - content?: string; - tool_calls?: unknown[]; -}): ParsedAutomatorResponse { - const { content, tool_calls } = response; - - if (Array.isArray(tool_calls) && tool_calls.length > 0) { - const result = parseToolCallResponse(tool_calls, content || undefined); - if (result.isStructured) return result; - } - - return parseAutomatorResponse(content || ""); -} diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts index abfd015ca5..e1a1236122 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts @@ -11,7 +11,8 @@ * * The orchestrator combines this prompt with: * - the actions catalog (what the model is allowed to emit) - * - the component cheatsheet (curated, small) + * - the component catalog (all registered Lowcoder components, with + * curated examples for common ones) * - the live editor snapshot (existing components, queries, canvas grid) * * before sending it to the user-defined Lowcoder query that proxies the LLM. @@ -53,9 +54,11 @@ Use this context to: # How to use the action catalog You will also see a JSON block titled "ACTIONS_CATALOG" listing the EXACT -set of actions you may emit, with their required and optional fields. You -MUST NOT use any action or component type that is not listed there. If -something is not possible with the catalog, explain why in plain text. +set of actions you may emit, with their required and optional fields. The +"COMPONENT_CATALOG" block lists every registered Lowcoder component type you +may place or nest. You MUST NOT use any action or component type that is not +listed there. If something is not possible with the catalog, explain why in +plain text. # Layout rules (short) diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts index 0e875ca6ac..b1d219031f 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts @@ -14,8 +14,8 @@ * call), which naturally replaces the old `"actions": []` convention. * * The tool definition is provider-agnostic: OpenAI, Groq, Together, and - * Ollama all accept the same `tools` shape. Anthropic needs a small - * remapping (documented in README.md). + * Ollama all accept the same `tools` shape. Other providers can map this + * schema in the selected query/backend bridge. */ import { ACTIONS_CATALOG } from "./actionsCatalog"; @@ -29,7 +29,7 @@ export interface OpenAIToolDefinition { }; } -function buildActionItemSchema(): Record { +function buildActionItemSchema(componentTypes?: string[]): Record { return { type: "object", properties: { @@ -40,8 +40,9 @@ function buildActionItemSchema(): Record { }, component: { type: "string", + ...(componentTypes && componentTypes.length > 0 ? { enum: componentTypes } : {}), description: - "Component type (e.g. 'button', 'input', 'table'). Required for place_component and nest_component.", + "Component type as registered in Lowcoder. Required for place_component and nest_component.", }, component_name: { type: "string", @@ -78,7 +79,7 @@ function buildActionItemSchema(): Record { * wrapper keeps the door open for future per-action tools if we want * tighter per-action schemas. */ -export function buildToolDefinitions(): OpenAIToolDefinition[] { +export function buildToolDefinitions(componentTypes?: string[]): OpenAIToolDefinition[] { const actionSummary = ACTIONS_CATALOG.map( (a) => ` - ${a.action}: ${a.purpose}` ).join("\n"); @@ -109,7 +110,7 @@ export function buildToolDefinitions(): OpenAIToolDefinition[] { actions: { type: "array", description: "Ordered list of actions to execute on the canvas.", - items: buildActionItemSchema(), + items: buildActionItemSchema(componentTypes), }, }, required: ["explanation", "actions"], diff --git a/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx b/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx index 1d096f898a..e0eccaee13 100644 --- a/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx +++ b/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx @@ -7,18 +7,16 @@ import { getPanelStyle, savePanelStyle } from "util/localStorageUtil"; import { BottomResultPanel } from "../../../components/resultPanel/BottomResultPanel"; import { AppState } from "../../../redux/reducers"; import { getUser } from "../../../redux/selectors/usersSelectors"; -import { connect } from "react-redux"; -import { Layers } from "constants/Layers"; -import Flex from "antd/es/flex"; -import type { MenuProps } from 'antd/es/menu'; -import { BuildOutlined, DatabaseOutlined, ThunderboltOutlined } from "@ant-design/icons"; -import Menu from "antd/es/menu/menu"; -import Select from "antd/es/select"; -import Switch from "antd/es/switch"; -import Tooltip from "antd/es/tooltip"; -import { AIGenerate } from "lowcoder-design"; -import { ChatPanel } from "@lowcoder-ee/comps/comps/chatComp/components/ChatPanel"; -import { EditorContext } from "comps/editorState"; +import { connect } from "react-redux"; +import { Layers } from "constants/Layers"; +import Flex from "antd/es/flex"; +import type { MenuProps } from 'antd/es/menu'; +import { DatabaseOutlined } from "@ant-design/icons"; +import Menu from "antd/es/menu/menu"; +import Select from "antd/es/select"; +import { AIGenerate } from "lowcoder-design"; +import { ChatPanel } from "@lowcoder-ee/comps/comps/chatComp/components/ChatPanel"; +import { EditorContext } from "comps/editorState"; type MenuItem = Required['items'][number]; @@ -70,13 +68,13 @@ const QuerySelectorWrapper = styled.div` gap: 12px; `; -const QueryLabel = styled.span` - font-size: 12px; - color: #8b8fa3; - white-space: nowrap; -`; - -const preventDefault = (e: any) => { +const QueryLabel = styled.span` + font-size: 12px; + color: #8b8fa3; + white-space: nowrap; +`; + +const preventDefault = (e: any) => { e.preventDefault(); }; @@ -98,12 +96,11 @@ function Bottom(props: any) { removeListener(); }; - const [bottomHeight, setBottomHeight] = useState(panelStyle.bottom.h); - const [currentOption, setCurrentOption] = useState("data"); - const [selectedQuery, setSelectedQuery] = useState(""); - const [automatorEnabled, setAutomatorEnabled] = useState(true); - - const editorState = useContext(EditorContext); + const [bottomHeight, setBottomHeight] = useState(panelStyle.bottom.h); + const [currentOption, setCurrentOption] = useState("data"); + const [selectedQuery, setSelectedQuery] = useState(""); + + const editorState = useContext(EditorContext); const queryOptions = useMemo(() => { if (!editorState) return []; @@ -144,34 +141,12 @@ function Bottom(props: any) { { currentOption === "ai" && ( - Lowcoder Automator - - - - - Automator - - - - Query: - setSelectedQuery(value || "")} @@ -181,15 +156,14 @@ function Bottom(props: any) { /> - - - )} - - + + + )} + + ); } From c82cfbe2245d1b2eb69d2241080d4cfbc3e41653 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 11 May 2026 23:57:31 +0500 Subject: [PATCH 06/33] fix UI issue for the composer automator assistant --- .../chatComp/components/ChatContainerStyles.ts | 15 +++++++++++++-- .../chatComp/components/ChatPanelContainer.tsx | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) 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 1f2d4580db..a16e67954c 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/ChatPanelContainer.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx index 3359106aa3..83b1028e1a 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx @@ -118,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; @@ -127,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 { From bf9196fa10d60652550c6347d02d2181dcc692d6 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 14 May 2026 00:31:31 +0500 Subject: [PATCH 07/33] add layout / style properties --- .../actions/automator/actionsCatalog.ts | 23 +- .../actions/automator/componentCatalog.ts | 772 +++++++++++++++++- .../actions/automator/systemPrompt.ts | 70 ++ .../preLoadComp/actions/componentStyling.ts | 275 +++++-- 4 files changed, 1024 insertions(+), 116 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts index 36ecfc054f..fbbdf54568 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts @@ -108,7 +108,8 @@ export const ACTIONS_CATALOG: ActionCatalogEntry[] = [ }, { action: "align_component", - purpose: "Align a component horizontally (left, center, right).", + purpose: + "Move a component horizontally on the canvas grid (left, center, right). This positions the COMPONENT in the canvas — it does NOT change text alignment INSIDE a component. For text/content alignment, use `set_properties` with the component's `horizontalAlignment` / `verticalAlignment` layoutProperties.", required: ["component_name", "action_parameters"], example: { action: "align_component", @@ -120,22 +121,32 @@ export const ACTIONS_CATALOG: ActionCatalogEntry[] = [ // ── Properties & Styling ────────────────────────────────────────── { action: "set_properties", - purpose: "Update properties on an existing component, addressed by name.", + purpose: + "Update top-level properties on an existing component (text, alignment, autoHeight, type, disabled, label, options, …). Use this for behaviour and layout-style props listed in the component's `layoutProperties`. Use `set_style` for CSS-like visual props.", required: ["component_name", "action_parameters"], example: { action: "set_properties", - component_name: "submitBtn", - action_parameters: { text: "Save", disabled: "false" }, + component_name: "title1", + action_parameters: { horizontalAlignment: "center", verticalAlignment: "center" }, }, }, { action: "set_style", - purpose: "Apply visual styles (colors, spacing, borders) to a component.", + purpose: + "Apply visual styles (color, font, spacing, border, animation, …) to a component. Pass a flat object — keys are auto-routed to the matching style namespace exposed by the component (`style`, `labelStyle`, `inputFieldStyle`, `disabledStyle`, `animationStyle`, `headerStyle`, `bodyStyle`, etc.). Use `_target: ''` only when the same key exists in multiple namespaces and you must disambiguate.", required: ["component_name", "action_parameters"], + optional: ["action_parameters._target"], example: { action: "set_style", component_name: "submitBtn", - action_parameters: { backgroundColor: "#1677ff", color: "#ffffff", borderRadius: "8px" }, + action_parameters: { + background: "#1677ff", + text: "#ffffff", + radius: "8px", + textSize: "14px", + textWeight: "600", + padding: "8px 16px", + }, }, }, diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts index 1eb6e23f1b..5221915026 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts @@ -11,6 +11,34 @@ import { uiCompRegistry, type UICompManifest } from "comps/uiCompRegistry"; * components like Chat, Chat Box, and Chat Controller. */ +/** + * Schema describing a top-level UI / layout property the model can set with + * `set_properties`. These are the props that drive component behavior or + * positional layout (text-align, vertical alignment, autoHeight, type, etc.) + * and live as direct children of the component, NOT inside the `style` + * namespace. + */ +export interface LayoutPropertyDescriptor { + /** Human-readable hint shown to the model. */ + description?: string; + /** Allowed string values when this property is a fixed enum. */ + enum?: readonly string[]; + /** Primitive type when the value is not enum-restricted. */ + type?: "string" | "number" | "boolean" | "object"; + /** Sample value the model can imitate verbatim. */ + example?: unknown; +} + +/** + * Map of style namespace → list of style keys that can be passed to + * `set_style`. Most components have a single `style` namespace; inputs and + * containers expose several (e.g. `labelStyle`, `inputFieldStyle`, + * `disabledStyle`, `animationStyle`). The `set_style` executor auto-routes + * each key to the matching namespace so models can usually pass a flat object + * without specifying `_target`. + */ +export type StylePropertyMap = Record; + export interface ComponentCatalogEntry { /** Component type as registered in `uiCompRegistry` */ type: string; @@ -34,8 +62,186 @@ export interface ComponentCatalogEntry { categories?: readonly string[]; /** Short manifest description when it is serialisable. */ description?: string; + /** + * Top-level UI / layout properties to be set with `set_properties`. + * Only properties controlling *behaviour or layout* (alignment, autoHeight, + * type, disabled, …) belong here — visual/CSS-like props live in + * `styleProperties` and are set with `set_style`. + */ + layoutProperties?: Record; + /** + * Style properties grouped by style namespace, used by `set_style`. + * Pass these as a flat object — `set_style` routes each key automatically. + * Use `_target: ""` only when the same key exists in multiple + * namespaces and you need to disambiguate. + */ + styleProperties?: StylePropertyMap; } +// ── Style key presets ──────────────────────────────────────────────────────── +// Mirror the field lists from `comps/controls/styleControlConstants.tsx` so the +// model knows what keys it can pass to `set_style`. Keep these compact — they +// are inlined into the system prompt. + +const COMMON_STYLE_KEYS = [ + "background", + "text", + "textTransform", + "textDecoration", + "textSize", + "textWeight", + "fontFamily", + "fontStyle", + "border", + "borderStyle", + "borderWidth", + "radius", + "margin", + "padding", + "lineHeight", + "rotation", +] as const; + +const CONTAINER_STYLE_KEYS = [ + "background", + "border", + "borderStyle", + "borderWidth", + "radius", + "margin", + "padding", + "boxShadow", + "boxShadowColor", + "opacity", + "rotation", + "backgroundImage", + "backgroundImageRepeat", + "backgroundImageSize", + "backgroundImagePosition", + "backgroundImageOrigin", +] as const; + +const INPUT_LIKE_STYLE_KEYS = [ + "background", + "boxShadow", + "boxShadowColor", + "text", + "textTransform", + "textDecoration", + "textSize", + "textWeight", + "fontFamily", + "fontStyle", + "border", + "borderStyle", + "borderWidth", + "radius", + "margin", + "padding", + "placeholder", + "accent", + "validate", +] as const; + +const LABEL_STYLE_KEYS = [ + "background", + "label", + "textTransform", + "textDecoration", + "textSize", + "textWeight", + "fontFamily", + "fontStyle", + "borderStyle", + "borderWidth", + "margin", + "padding", + "placeholder", + "accent", + "validate", +] as const; + +const ANIMATION_STYLE_KEYS = [ + "animation", + "animationDelay", + "animationDuration", + "animationIterationCount", +] as const; + +const DISABLED_STYLE_KEYS = [ + "disabledBackground", + "disabledText", + "disabledBorder", +] as const; + +const IMAGE_STYLE_KEYS = [ + "margin", + "padding", + "border", + "borderStyle", + "borderWidth", + "radius", + "opacity", + "boxShadow", + "boxShadowColor", + "rotation", +] as const; + +const NAVIGATION_STYLE_KEYS = [ + "background", + "border", + "borderStyle", + "borderWidth", + "radius", + "margin", + "padding", + "accent", +] as const; + +// ── Layout property presets ───────────────────────────────────────────────── + +const TEXT_HORIZONTAL_ALIGNMENT: LayoutPropertyDescriptor = { + description: "Horizontal text alignment inside the component.", + enum: ["left", "center", "right", "justify"], +}; + +const ALIGN_HORIZONTAL: LayoutPropertyDescriptor = { + description: "Horizontal alignment.", + enum: ["left", "center", "right"], +}; + +const VERTICAL_ALIGNMENT: LayoutPropertyDescriptor = { + description: "Vertical alignment.", + enum: ["flex-start", "center", "flex-end"], +}; + +const AUTO_HEIGHT: LayoutPropertyDescriptor = { + description: "Whether the component auto-sizes its height to its content.", + enum: ["auto", "fixed"], +}; + +const HIDDEN: LayoutPropertyDescriptor = { + description: "Hide the component at runtime.", + type: "boolean", +}; + +const DISABLED: LayoutPropertyDescriptor = { + description: "Disable the component at runtime.", + type: "boolean", +}; + +const LOADING: LayoutPropertyDescriptor = { + description: "Show a loading indicator on the component.", + type: "boolean", +}; + +const LABEL_OBJECT: LayoutPropertyDescriptor = { + description: + "Field label config: { text, position: 'row'|'column', align: 'left'|'center'|'right', width: number, hidden?: boolean, tooltip?: string }.", + type: "object", + example: { text: "Email", position: "row", align: "left" }, +}; + export const LOWCODER_COMPONENT_TYPES: string[] = [ "chart", "basicChart", @@ -156,50 +362,163 @@ const TEXT: ComponentCatalogEntry = { type: "text", defaultLayout: { w: 12, h: 4 }, required: ["text"], - optional: ["type"], + optional: [ + "type", + "horizontalAlignment", + "verticalAlignment", + "autoHeight", + "contentScrollBar", + "hidden", + ], example: { text: "## Hello", type: "markdown" }, - notes: "Use type:'markdown' for headings, links, formatted text.", + notes: + "Use type:'markdown' for headings, links, formatted text. For text alignment INSIDE the component, set the `horizontalAlignment` property (NOT the `align_component` action — that one moves the component on the canvas grid).", + layoutProperties: { + type: { + description: "Render mode for the value.", + enum: ["markdown", "text"], + }, + horizontalAlignment: TEXT_HORIZONTAL_ALIGNMENT, + verticalAlignment: VERTICAL_ALIGNMENT, + autoHeight: AUTO_HEIGHT, + contentScrollBar: { + description: "Show scrollbars when content overflows (only when autoHeight=fixed).", + type: "boolean", + }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS, "links"], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const BUTTON: ComponentCatalogEntry = { type: "button", defaultLayout: { w: 6, h: 5 }, required: ["text"], - optional: ["type", "disabled", "loading", "form", "prefixIcon"], - example: { text: "Submit", type: "primary" }, - notes: "Set type:'submit' and form:'' to submit a form.", + optional: [ + "type", + "disabled", + "loading", + "form", + "prefixIcon", + "suffixIcon", + "tooltip", + "tabIndex", + "hidden", + ], + example: { text: "Submit", type: "" }, + notes: + "Set type:'submit' and form:'' to submit a form. Leave type:'' for a default click-handler button.", + layoutProperties: { + type: { + description: "'' for default click-handler button, 'submit' for a form-submit button.", + enum: ["", "submit"], + }, + disabled: DISABLED, + loading: LOADING, + hidden: HIDDEN, + prefixIcon: { + description: + "Icon path string ('/icon:solid/check') shown before the text. Empty string clears it.", + type: "string", + }, + suffixIcon: { + description: "Icon shown after the text.", + type: "string", + }, + tooltip: { description: "Hover tooltip text.", type: "string" }, + tabIndex: { description: "Tab order for keyboard navigation.", type: "number" }, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS], + disabledStyle: [...DISABLED_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const INPUT: ComponentCatalogEntry = { type: "input", defaultLayout: { w: 12, h: 6 }, required: ["label", "placeholder"], - optional: ["value", "validationType", "required", "allowClear"], + optional: [ + "value", + "validationType", + "required", + "allowClear", + "showCount", + "readOnly", + "disabled", + "hidden", + "prefixIcon", + "suffixIcon", + ], example: { label: { text: "Name", position: "row", align: "left" }, placeholder: "Enter name", allowClear: true, }, + layoutProperties: { + label: LABEL_OBJECT, + placeholder: { description: "Placeholder text.", type: "string" }, + disabled: DISABLED, + hidden: HIDDEN, + showCount: { description: "Show character counter.", type: "boolean" }, + allowClear: { description: "Show a clear button.", type: "boolean" }, + readOnly: { description: "Read-only field.", type: "boolean" }, + required: { description: "Mark as required for form validation.", type: "boolean" }, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + labelStyle: [...LABEL_STYLE_KEYS], + inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], + disabledStyle: [...DISABLED_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const NUMBER_INPUT: ComponentCatalogEntry = { type: "numberInput", defaultLayout: { w: 12, h: 6 }, required: ["label"], - optional: ["value", "min", "max", "step", "placeholder"], + optional: [ + "value", + "min", + "max", + "step", + "placeholder", + "disabled", + "hidden", + "readOnly", + ], example: { label: { text: "Quantity", position: "row" }, value: 1, min: 0, max: 100, }, + layoutProperties: { + label: LABEL_OBJECT, + placeholder: { description: "Placeholder text.", type: "string" }, + disabled: DISABLED, + hidden: HIDDEN, + readOnly: { description: "Read-only field.", type: "boolean" }, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + labelStyle: [...LABEL_STYLE_KEYS], + inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], + disabledStyle: [...DISABLED_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const DROPDOWN: ComponentCatalogEntry = { type: "select", defaultLayout: { w: 12, h: 6 }, required: ["label", "options", "value"], - optional: ["allowClear"], + optional: ["allowClear", "disabled", "hidden", "showSearch", "placeholder"], example: { label: { text: "Status", position: "row" }, options: { @@ -213,17 +532,46 @@ const DROPDOWN: ComponentCatalogEntry = { }, value: "pending", }, + layoutProperties: { + label: LABEL_OBJECT, + placeholder: { description: "Placeholder text.", type: "string" }, + disabled: DISABLED, + hidden: HIDDEN, + allowClear: { description: "Show a clear button.", type: "boolean" }, + showSearch: { description: "Enable search filter.", type: "boolean" }, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS, "accent", "validate"], + labelStyle: [...LABEL_STYLE_KEYS], + childrenInputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const CHECKBOX: ComponentCatalogEntry = { type: "checkbox", defaultLayout: { w: 8, h: 5 }, required: ["label"], - optional: ["value", "options"], + optional: ["value", "options", "disabled", "hidden", "layout"], example: { label: { text: "I agree", position: "row" }, value: false, }, + layoutProperties: { + label: LABEL_OBJECT, + disabled: DISABLED, + hidden: HIDDEN, + layout: { + description: "Group layout direction for multi-option checkboxes.", + enum: ["horizontal", "vertical", "autoColumns"], + }, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS], + labelStyle: [...LABEL_STYLE_KEYS], + inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS, "checkedBackground", "uncheckedBackground", "uncheckedBorder", "hoverBackground"], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const FORM: ComponentCatalogEntry = { @@ -231,6 +579,7 @@ const FORM: ComponentCatalogEntry = { isContainer: true, defaultLayout: { w: 12, h: 30 }, required: ["container"], + optional: ["hidden", "disabled"], example: { container: { header: {}, @@ -248,6 +597,17 @@ const FORM: ComponentCatalogEntry = { }, notes: "Nest input/select/etc. under '.container.body.0.view'. Submit button goes under '.container.footer' with type:'submit', form:''.", + layoutProperties: { + hidden: HIDDEN, + disabled: DISABLED, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + headerStyle: [...CONTAINER_STYLE_KEYS], + bodyStyle: [...CONTAINER_STYLE_KEYS], + footerStyle: [...CONTAINER_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const CONTAINER: ComponentCatalogEntry = { @@ -255,6 +615,7 @@ const CONTAINER: ComponentCatalogEntry = { isContainer: true, defaultLayout: { w: 12, h: 20 }, required: ["container"], + optional: ["hidden"], example: { container: { header: {}, @@ -270,6 +631,16 @@ const CONTAINER: ComponentCatalogEntry = { }, notes: "Nest under '.container.body.0.view'.", + layoutProperties: { + hidden: HIDDEN, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + headerStyle: [...CONTAINER_STYLE_KEYS], + bodyStyle: [...CONTAINER_STYLE_KEYS], + footerStyle: [...CONTAINER_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const MODAL: ComponentCatalogEntry = { @@ -277,7 +648,7 @@ const MODAL: ComponentCatalogEntry = { isContainer: true, defaultLayout: { w: 12, h: 40 }, required: ["title", "container"], - optional: ["open"], + optional: ["open", "showMask", "maskClosable", "width", "hidden"], example: { title: "Add Item", open: false, @@ -285,6 +656,17 @@ const MODAL: ComponentCatalogEntry = { }, notes: "container MUST be empty {}. Children are nested under '.container' (no body/header/footer paths).", + layoutProperties: { + open: { description: "Whether the modal is visible.", type: "boolean" }, + showMask: { description: "Render the dim background mask.", type: "boolean" }, + maskClosable: { description: "Allow closing by clicking the mask.", type: "boolean" }, + width: { description: 'Modal width, e.g. "600px" or "60%".', type: "string" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const DRAWER: ComponentCatalogEntry = { @@ -292,17 +674,53 @@ const DRAWER: ComponentCatalogEntry = { isContainer: true, defaultLayout: { w: 12, h: 40 }, required: ["title", "container"], - optional: ["open", "placement"], + optional: ["open", "placement", "showMask", "maskClosable", "width", "hidden"], example: { title: "Edit", open: false, container: {} }, notes: "Same flat-container rule as modal. Nest under '.container'.", + layoutProperties: { + open: { description: "Whether the drawer is visible.", type: "boolean" }, + placement: { + description: "Edge from which the drawer slides in.", + enum: ["top", "right", "bottom", "left"], + }, + showMask: { description: "Render the dim background mask.", type: "boolean" }, + maskClosable: { description: "Allow closing by clicking the mask.", type: "boolean" }, + width: { description: 'Drawer width, e.g. "400px".', type: "string" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const TABLE: ComponentCatalogEntry = { type: "table", defaultLayout: { w: 24, h: 30 }, required: ["columns", "data"], - optional: ["pagination", "showRowGridBorder"], + optional: [ + "pagination", + "showRowGridBorder", + "showHeader", + "size", + "hidden", + "rowAutoHeight", + ], + layoutProperties: { + showHeader: { description: "Render the column header row.", type: "boolean" }, + showRowGridBorder: { description: "Outline each row.", type: "boolean" }, + size: { description: "Row density.", enum: ["small", "middle", "large"] }, + rowAutoHeight: { description: "Auto-size each row to content.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + headerStyle: [...COMMON_STYLE_KEYS], + rowStyle: [...COMMON_STYLE_KEYS], + cellStyle: [...COMMON_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, example: { columns: [ { @@ -329,7 +747,25 @@ const LIST_VIEW: ComponentCatalogEntry = { isContainer: true, defaultLayout: { w: 24, h: 30 }, required: ["container"], - optional: ["noOfRows", "itemIndexName", "itemDataName"], + optional: [ + "noOfRows", + "itemIndexName", + "itemDataName", + "noOfColumns", + "horizontal", + "scrollbars", + "hidden", + ], + layoutProperties: { + noOfColumns: { description: "Columns per row in the grid.", type: "number" }, + horizontal: { description: "Render rows horizontally.", type: "boolean" }, + scrollbars: { description: "Always show scrollbars.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, example: { container: {}, noOfRows: "3", @@ -344,67 +780,176 @@ const IMAGE: ComponentCatalogEntry = { type: "image", defaultLayout: { w: 8, h: 12 }, required: ["src"], - optional: ["autoHeight"], + optional: [ + "autoHeight", + "placement", + "enableOverflow", + "aspectRatio", + "supportPreview", + "hidden", + "clipPath", + ], example: { src: "https://images.unsplash.com/photo-1518770660439-4636190af475" }, notes: "src MUST be a real, publicly accessible URL.", + layoutProperties: { + autoHeight: AUTO_HEIGHT, + placement: { + description: "Where the image sits inside the cell.", + enum: [ + "top", + "bottom", + "left", + "right", + "top-left", + "top-right", + "bottom-left", + "bottom-right", + ], + }, + enableOverflow: { description: "Crop image to fit instead of contain.", type: "boolean" }, + aspectRatio: { description: 'CSS aspect-ratio (e.g. "16 / 9").', type: "string" }, + supportPreview: { description: "Allow click-to-preview at full size.", type: "boolean" }, + hidden: HIDDEN, + clipPath: { description: "CSS clip-path string.", type: "string" }, + }, + styleProperties: { + style: [...IMAGE_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const DIVIDER: ComponentCatalogEntry = { type: "divider", defaultLayout: { w: 24, h: 2 }, required: [], + optional: ["title", "align", "type", "dashed", "hidden"], example: {}, + layoutProperties: { + title: { description: "Optional label rendered in the divider.", type: "string" }, + align: ALIGN_HORIZONTAL, + type: { description: "Orientation.", enum: ["horizontal", "vertical"] }, + dashed: { description: "Render with a dashed line.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const DATE: ComponentCatalogEntry = { type: "date", defaultLayout: { w: 12, h: 6 }, required: ["label"], - optional: ["value", "format"], + optional: ["value", "format", "placeholder", "disabled", "hidden", "showTime"], example: { label: { text: "Due date", position: "row" }, format: "YYYY-MM-DD", }, + layoutProperties: { + label: LABEL_OBJECT, + placeholder: { description: "Placeholder text.", type: "string" }, + disabled: DISABLED, + hidden: HIDDEN, + showTime: { description: "Include time picker.", type: "boolean" }, + format: { description: 'Display/parse format, e.g. "YYYY-MM-DD".', type: "string" }, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS, "accent", "validate"], + labelStyle: [...LABEL_STYLE_KEYS], + inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const SWITCH: ComponentCatalogEntry = { type: "switch", defaultLayout: { w: 6, h: 5 }, required: ["label"], - optional: ["value"], + optional: ["value", "disabled", "hidden"], example: { label: { text: "Enabled", position: "row" }, value: true, }, + layoutProperties: { + label: LABEL_OBJECT, + disabled: DISABLED, + hidden: HIDDEN, + }, + styleProperties: { + style: ["handle", "unchecked", "checked", "margin", "padding"], + labelStyle: [...LABEL_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const TEXT_AREA: ComponentCatalogEntry = { type: "textArea", defaultLayout: { w: 12, h: 8 }, required: ["label"], - optional: ["placeholder", "value", "autoHeight"], + optional: [ + "placeholder", + "value", + "autoHeight", + "disabled", + "hidden", + "readOnly", + "showCount", + "allowClear", + ], example: { label: { text: "Description", position: "row" }, placeholder: "Enter description...", }, + layoutProperties: { + label: LABEL_OBJECT, + placeholder: { description: "Placeholder text.", type: "string" }, + autoHeight: AUTO_HEIGHT, + disabled: DISABLED, + hidden: HIDDEN, + readOnly: { description: "Read-only field.", type: "boolean" }, + showCount: { description: "Show character counter.", type: "boolean" }, + allowClear: { description: "Show a clear button.", type: "boolean" }, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + labelStyle: [...LABEL_STYLE_KEYS], + inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], + disabledStyle: [...DISABLED_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const PASSWORD: ComponentCatalogEntry = { type: "password", defaultLayout: { w: 12, h: 6 }, required: ["label"], - optional: ["placeholder"], + optional: ["placeholder", "disabled", "hidden", "visibilityToggle"], example: { label: { text: "Password", position: "row" }, placeholder: "Enter password", }, + layoutProperties: { + label: LABEL_OBJECT, + placeholder: { description: "Placeholder text.", type: "string" }, + disabled: DISABLED, + hidden: HIDDEN, + visibilityToggle: { description: "Show the eye toggle to reveal the password.", type: "boolean" }, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + labelStyle: [...LABEL_STYLE_KEYS], + inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], + disabledStyle: [...DISABLED_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const CHART: ComponentCatalogEntry = { type: "chart", defaultLayout: { w: 12, h: 20 }, required: ["chartType", "data"], - optional: ["title", "xAxisKey"], + optional: ["title", "xAxisKey", "hidden", "showLegend"], example: { chartType: "bar", data: '[{"category":"A","value":30},{"category":"B","value":50},{"category":"C","value":20}]', @@ -412,6 +957,36 @@ const CHART: ComponentCatalogEntry = { xAxisKey: "category", }, notes: "chartType: 'bar', 'line', 'pie', 'scatter'. data is a stringified JSON array.", + layoutProperties: { + chartType: { + description: "Visualisation kind.", + enum: ["bar", "line", "pie", "scatter"], + }, + title: { description: "Chart title.", type: "string" }, + showLegend: { description: "Display the legend.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [ + "chartBackgroundColor", + "chartGradientColor", + "chartShadowColor", + "chartBorderColor", + "chartTextColor", + "chartTextSize", + "chartTextWeight", + "chartFontFamily", + "chartFontStyle", + "chartBorderStyle", + "chartBorderRadius", + "chartBorderWidth", + "chartOpacity", + "chartBoxShadow", + "margin", + "padding", + ], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const CARD: ComponentCatalogEntry = { @@ -419,9 +994,22 @@ const CARD: ComponentCatalogEntry = { isContainer: true, defaultLayout: { w: 8, h: 15 }, required: ["title"], - optional: ["size"], + optional: ["size", "showTitle", "hoverable", "bordered", "hidden"], example: { title: "Card Title" }, notes: "Nest content inside '.container'.", + layoutProperties: { + size: { description: "Card density.", enum: ["default", "small"] }, + showTitle: { description: "Render the title bar.", type: "boolean" }, + hoverable: { description: "Lift on hover.", type: "boolean" }, + bordered: { description: "Show outer border.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS, "IconColor", "activateColor"], + headerStyle: [...COMMON_STYLE_KEYS], + bodyStyle: [...CONTAINER_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const TABBED_CONTAINER: ComponentCatalogEntry = { @@ -429,94 +1017,205 @@ const TABBED_CONTAINER: ComponentCatalogEntry = { isContainer: true, defaultLayout: { w: 24, h: 30 }, required: ["container"], - optional: ["tabs"], + optional: ["tabs", "tabPosition", "showHeader", "hidden"], example: { container: {} }, notes: "Nest content per tab. Tabs are managed via properties.", + layoutProperties: { + tabPosition: { + description: "Tab bar placement.", + enum: ["top", "right", "bottom", "left"], + }, + showHeader: { description: "Show the tabs bar.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + headerStyle: [...COMMON_STYLE_KEYS], + bodyStyle: [...CONTAINER_STYLE_KEYS], + tabsStyle: [...COMMON_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const VIDEO: ComponentCatalogEntry = { type: "video", defaultLayout: { w: 12, h: 15 }, required: ["src"], - optional: ["controls", "autoPlay"], + optional: ["controls", "autoPlay", "loop", "muted", "hidden"], example: { src: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", controls: true }, notes: "src must be a real URL. Set layout.h >= 10.", + layoutProperties: { + controls: { description: "Show native player controls.", type: "boolean" }, + autoPlay: { description: "Auto-play on mount (often requires muted=true).", type: "boolean" }, + loop: { description: "Loop the video.", type: "boolean" }, + muted: { description: "Start muted.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: ["margin", "padding"], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const AVATAR: ComponentCatalogEntry = { type: "avatar", defaultLayout: { w: 6, h: 6 }, required: ["icon", "iconSize"], - optional: ["src", "avatarLabel", "avatarCatption", "shape"], + optional: ["src", "avatarLabel", "avatarCatption", "shape", "hidden"], example: { icon: "/icon:solid/user", iconSize: "40", shape: "circle", avatarLabel: "John Doe", }, + layoutProperties: { + shape: { description: "Avatar shape.", enum: ["circle", "square"] }, + iconSize: { description: "Icon pixel size as a string, e.g. '40'.", type: "string" }, + hidden: HIDDEN, + }, + styleProperties: { + style: ["background", "fill"], + avatarLabelStyle: [...COMMON_STYLE_KEYS], + avatarContainerStyle: [...CONTAINER_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const PROGRESS: ComponentCatalogEntry = { type: "progress", defaultLayout: { w: 12, h: 4 }, required: ["value"], - optional: ["showInfo"], + optional: ["showInfo", "hidden"], example: { value: "75" }, + layoutProperties: { + showInfo: { description: "Display the percentage label.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: ["text", "textSize", "textWeight", "fontFamily", "fontStyle", "radius", "margin", "padding", "lineHeight", "track", "fill", "success"], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const RATING: ComponentCatalogEntry = { type: "rating", defaultLayout: { w: 8, h: 5 }, required: ["label"], - optional: ["value", "max", "allowHalf"], + optional: ["value", "max", "allowHalf", "disabled", "hidden"], example: { label: { text: "Rating", position: "row" }, value: "3", max: "5", }, + layoutProperties: { + label: LABEL_OBJECT, + allowHalf: { description: "Allow half-star ratings.", type: "boolean" }, + disabled: DISABLED, + hidden: HIDDEN, + }, + styleProperties: { + style: ["checked", "unchecked", "margin", "padding"], + labelStyle: [...LABEL_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const SLIDER: ComponentCatalogEntry = { type: "slider", defaultLayout: { w: 12, h: 5 }, required: ["label"], - optional: ["value", "min", "max", "step"], + optional: ["value", "min", "max", "step", "disabled", "hidden", "vertical"], example: { label: { text: "Volume", position: "row" }, value: "50", min: "0", max: "100", }, + layoutProperties: { + label: LABEL_OBJECT, + disabled: DISABLED, + hidden: HIDDEN, + vertical: { description: "Render vertically.", type: "boolean" }, + }, + styleProperties: { + style: ["fill", "thumb", "thumbBorder", "track", "margin", "padding"], + labelStyle: [...LABEL_STYLE_KEYS], + disabledStyle: ["disabledFill", "disabledTrack"], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const NAVIGATION: ComponentCatalogEntry = { type: "navigation", defaultLayout: { w: 24, h: 5 }, required: ["items"], - optional: ["logoUrl"], + optional: ["logoUrl", "horizontalAlignment", "hidden"], example: { items: [ { label: "Home", hidden: false }, { label: "About", hidden: false }, ], }, + layoutProperties: { + horizontalAlignment: ALIGN_HORIZONTAL, + hidden: HIDDEN, + logoUrl: { description: "Optional logo image URL.", type: "string" }, + }, + styleProperties: { + style: [...NAVIGATION_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const TIMELINE: ComponentCatalogEntry = { type: "timeline", defaultLayout: { w: 12, h: 15 }, required: ["value"], + optional: ["mode", "reverse", "hidden"], example: { value: '[{"title":"Step 1","subTitle":"Started"},{"title":"Step 2","subTitle":"In Progress"}]', }, notes: "value must be a stringified JSON array of timeline entries.", + layoutProperties: { + mode: { + description: "Layout mode for entries.", + enum: ["left", "alternate", "right"], + }, + reverse: { description: "Reverse entry order.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [ + "background", + "titleColor", + "subTitleColor", + "labelColor", + "margin", + "padding", + "radius", + ], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const STEP: ComponentCatalogEntry = { type: "step", defaultLayout: { w: 24, h: 6 }, required: ["value", "options"], - optional: ["initialValue"], + optional: ["initialValue", "direction", "size", "hidden"], + layoutProperties: { + direction: { + description: "Step bar orientation.", + enum: ["horizontal", "vertical"], + }, + size: { description: "Step density.", enum: ["default", "small"] }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS], + disabledStyle: [...DISABLED_STYLE_KEYS], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, example: { value: "1", initialValue: "1", @@ -538,7 +1237,7 @@ const RADIO: ComponentCatalogEntry = { type: "radio", defaultLayout: { w: 12, h: 5 }, required: ["label", "options"], - optional: ["value"], + optional: ["value", "disabled", "hidden", "layout"], example: { label: { text: "Priority", position: "row" }, options: { @@ -553,6 +1252,21 @@ const RADIO: ComponentCatalogEntry = { }, value: "medium", }, + layoutProperties: { + label: LABEL_OBJECT, + disabled: DISABLED, + hidden: HIDDEN, + layout: { + description: "Group layout direction.", + enum: ["horizontal", "vertical", "autoColumns"], + }, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS], + labelStyle: [...LABEL_STYLE_KEYS], + inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS, "checkedBackground", "uncheckedBackground", "uncheckedBorder", "hoverBackground"], + animationStyle: [...ANIMATION_STYLE_KEYS], + }, }; const CHAT: ComponentCatalogEntry = { diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts index e1a1236122..59daedb145 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts @@ -73,6 +73,73 @@ plain text. - Populate data-driven components (table, listView, grid) with 3+ realistic sample rows. Stringify JSON for the \`data\` field of \`table\`. +# Styling & layout edits + +There are TWO families of UI edits, and each has its own action: + +1. **\`set_properties\`** — top-level UI / behaviour properties exposed as + direct children of the component. Use this for things controlled by the + component's own controls (alignment, autoHeight, type, label, placeholder, + options, disabled, hidden, loading, placement, …). For each component the + \`layoutProperties\` field in COMPONENT_CATALOG lists the exact keys and + their allowed values. + +2. **\`set_style\`** — visual / CSS-like properties living inside the + component's style namespaces (\`style\`, \`labelStyle\`, \`inputFieldStyle\`, + \`disabledStyle\`, \`animationStyle\`, \`headerStyle\`, \`bodyStyle\`, …). + Pass a flat object — keys are auto-routed to the matching namespace. + For each component the \`styleProperties\` field in COMPONENT_CATALOG + lists which keys live in which namespace. + + When the same key exists in multiple namespaces (e.g. \`text\` in + \`labelStyle\` and \`inputFieldStyle\`) include a \`_target\` field to + disambiguate, e.g. + \`{ "_target": "labelStyle", "text": "#1677ff" }\`. + + Common style-key vocabulary: + - text/colour: \`text\` (foreground), \`background\`, \`links\`, \`accent\` + - typography: \`textSize\`, \`textWeight\`, \`fontFamily\`, \`fontStyle\`, + \`textTransform\`, \`textDecoration\`, \`lineHeight\` + - box model: \`margin\`, \`padding\`, \`border\`, \`borderStyle\`, + \`borderWidth\`, \`radius\`, \`opacity\`, \`boxShadow\`, \`boxShadowColor\`, + \`rotation\` + - background image: \`backgroundImage\`, \`backgroundImageRepeat\`, + \`backgroundImageSize\`, \`backgroundImagePosition\`, + \`backgroundImageOrigin\` + - animation (in \`animationStyle\`): \`animation\`, \`animationDelay\`, + \`animationDuration\`, \`animationIterationCount\` + - disabled state (in \`disabledStyle\`): \`disabledBackground\`, + \`disabledText\`, \`disabledBorder\` + +3. **\`align_component\`** — moves the COMPONENT to the left/center/right of + the canvas grid. It does NOT change text or content alignment inside the + component. For "center the text" / "right-align this label" use + \`set_properties\` with \`horizontalAlignment\`. + +# Common UI recipes + +- Center text inside a Text component: + set_properties { horizontalAlignment: "center" } +- Larger heading text: + set_style { textSize: "24px", textWeight: "700", lineHeight: "1.3" } +- Coloured primary button: + set_style { background: "#1677ff", text: "#ffffff", radius: "8px", + padding: "8px 16px", textWeight: "600" } +- Accent input border + larger label: + set_style { _target: "inputFieldStyle", border: "#1677ff", + borderWidth: "2px", radius: "6px" } + set_style { _target: "labelStyle", textSize: "14px", textWeight: "600" } +- Soft card with shadow: + set_style { background: "#ffffff", radius: "12px", border: "#e5e7eb", + borderWidth: "1px", padding: "16px", + boxShadow: "0 4px 12px", boxShadowColor: "rgba(0,0,0,0.08)" } +- Animate a component on mount: + set_style { animation: "fadeIn", animationDuration: "0.6s", + animationIterationCount: "1" } +- Hide / disable a component: + set_properties { hidden: true } + set_properties { disabled: true } + # UX defaults - Apps that show data: title (text) → filters (input/dropdown) → primary @@ -88,6 +155,9 @@ plain text. \`component_name\`. - Component names must be unique across the app. If reusing an existing component referenced in EDITOR_CONTEXT, use its existing name. +- Prefer the per-component \`layoutProperties\` / \`styleProperties\` listed in + COMPONENT_CATALOG over invented keys. If a property is not listed and you + are unsure it exists, ask the user instead of guessing. `.trim(); /** diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts index dbe6297a0b..0af2b3a334 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts @@ -1,9 +1,8 @@ import { message } from "antd"; import { ActionConfig, ActionExecuteParams } from "../types"; -import { getEditorComponentInfo } from "../utils"; -// Fallback constant style object to apply -// This wil be replaced by a JSON object returned by the AI model. +// Fallback constant style object to apply if the model returns invalid JSON. +// This is a safety net only — real values always come from the LLM payload. const FALLBACK_STYLE_OBJECT = { fontSize: "10px", fontWeight: "500", @@ -11,103 +10,217 @@ const FALLBACK_STYLE_OBJECT = { backgroundColor: "#ffffff", padding: "8px", borderRadius: "4px", - border: "1px solid #ddd" + border: "1px solid #ddd", }; +// Reserved keys in the style payload that are NOT real style fields. Used to +// disambiguate which `*Style` namespace a flat object should be routed to. +const RESERVED_KEYS = new Set(["_target", "_namespace"]); + +/** + * Resolve the children object that holds the component's style namespaces. + * + * Most leaf components store their style children directly on + * `comp.children.comp.children` (e.g. `style`, `labelStyle`, `animationStyle`). + * Some composite components (form/list/etc.) wrap them under a sub-component + * keyed by the `compType` value, so we look there as a fallback. + */ +function resolveStyleChildrenRoot(comp: any): any { + const innerChildren = comp?.children?.comp?.children; + if (!innerChildren) return null; + + if (innerChildren.style) return innerChildren; + + const compType = comp?.children?.compType?.getView?.(); + const wrapper = compType ? innerChildren[compType] : null; + if (wrapper?.children) return wrapper.children; + + return innerChildren; +} + +/** + * Collect every style-like child container exposed by the component, keyed by + * its name. Recognises `style` plus any sibling whose name ends with `Style` + * (e.g. `labelStyle`, `inputFieldStyle`, `disabledStyle`, `animationStyle`, + * `headerStyle`, `bodyStyle`, …). + */ +function collectStyleNamespaces(rootChildren: any): Record { + if (!rootChildren) return {}; + const out: Record = {}; + for (const key of Object.keys(rootChildren)) { + if (key === "style" || key.endsWith("Style")) { + out[key] = rootChildren[key]; + } + } + return out; +} + +/** + * Apply a single style key/value to the first namespace that owns it. + * + * Routing order: + * 1. The explicit `_target` namespace (when provided). + * 2. `style` (if present and contains the key). + * 3. Any other `*Style` namespace, in declaration order. + * + * Returns `true` when the value was applied, `false` otherwise so the caller + * can collect a useful warning. + */ +function applyStyleKey( + namespaces: Record, + styleKey: string, + styleValue: unknown, + preferredTarget?: string +): { applied: boolean; namespace?: string } { + const tryNamespace = (nsName: string): boolean => { + const ns = namespaces[nsName]; + if (!ns) return false; + + // Most style controls expose nested `children[styleKey]`. + const nested = ns.children?.[styleKey]; + if (nested?.dispatchChangeValueAction) { + nested.dispatchChangeValueAction(styleValue); + return true; + } + // Older style controls expose the key directly on the namespace object. + const direct = ns[styleKey]; + if (direct?.dispatchChangeValueAction) { + direct.dispatchChangeValueAction(styleValue); + return true; + } + return false; + }; + + if (preferredTarget && namespaces[preferredTarget]) { + if (tryNamespace(preferredTarget)) { + return { applied: true, namespace: preferredTarget }; + } + } + + if (namespaces.style && tryNamespace("style")) { + return { applied: true, namespace: "style" }; + } + + for (const nsName of Object.keys(namespaces)) { + if (nsName === "style") continue; + if (preferredTarget && nsName === preferredTarget) continue; + if (tryNamespace(nsName)) { + return { applied: true, namespace: nsName }; + } + } + + return { applied: false }; +} + export const applyStyleAction: ActionConfig = { - key: 'apply-style', - label: 'Apply style to component', - category: 'styling', + key: "apply-style", + label: "Apply style to component", + category: "styling", requiresEditorComponentSelection: true, requiresStyle: true, requiresInput: true, - inputPlaceholder: 'Enter CSS styles (JSON format)', - inputType: 'textarea', + inputPlaceholder: "Enter CSS styles (JSON format)", + inputType: "textarea", validation: (value: string) => { - if (!value.trim()) return 'Styles are required' - else return null; + if (!value.trim()) return "Styles are required"; + return null; }, execute: async (params: ActionExecuteParams) => { const { selectedEditorComponent, actionValue, editorState } = params; - + if (!selectedEditorComponent || !editorState) { - message.error('Component and editor state are required'); + message.error("Component and editor state are required"); return; } - // A fallback constant is currently used to style the component. - // This is a temporary solution and will be removed once we integrate the AI model with the component styling. + let styleObject: Record = {}; + let usingFallback = false; + try { - let styleObject: Record = {}; - let usingFallback = false; - - try { - if (typeof actionValue === 'string') { - styleObject = JSON.parse(actionValue); - } else { - styleObject = actionValue; - } - } catch (e) { - styleObject = FALLBACK_STYLE_OBJECT; - usingFallback = true; - } - - const comp = editorState.getUICompByName(selectedEditorComponent); - - if (!comp) { - message.error(`Component "${selectedEditorComponent}" not found`); - return; + if (typeof actionValue === "string") { + styleObject = JSON.parse(actionValue); + } else { + styleObject = (actionValue as any) || {}; } + } catch (e) { + styleObject = FALLBACK_STYLE_OBJECT; + usingFallback = true; + } - const appliedStyles: string[] = []; - - for (const [styleKey, styleValue] of Object.entries(styleObject)) { - try { - const { children } = comp.children.comp; - const compType = comp.children.compType.getView(); - - // This method is used in LeftLayersContent.tsx to style the component. - if (!children.style) { - if (children[compType]?.children?.style?.children?.[styleKey]) { - children[compType].children.style.children[styleKey].dispatchChangeValueAction(styleValue); - appliedStyles.push(styleKey); - } else if (children[compType]?.children?.[styleKey]) { - children[compType].children[styleKey].dispatchChangeValueAction(styleValue); - appliedStyles.push(styleKey); - } else { - console.warn(`Style property ${styleKey} not found in component ${selectedEditorComponent}`); - } - } else { - if (children.style.children?.[styleKey]) { - children.style.children[styleKey].dispatchChangeValueAction(styleValue); - appliedStyles.push(styleKey); - } else if (children.style[styleKey]) { - children.style[styleKey].dispatchChangeValueAction(styleValue); - appliedStyles.push(styleKey); - } else { - console.warn(`Style property ${styleKey} not found in style object`); - } - } - } catch (error) { - console.error(`Error applying style ${styleKey}:`, error); - } - } + if (!styleObject || typeof styleObject !== "object") { + message.error("Invalid style payload"); + return; + } + + const comp = editorState.getUICompByName(selectedEditorComponent); + if (!comp) { + message.error(`Component "${selectedEditorComponent}" not found`); + return; + } + + const rootChildren = resolveStyleChildrenRoot(comp); + const namespaces = collectStyleNamespaces(rootChildren); + + if (Object.keys(namespaces).length === 0) { + message.warning( + `Component "${selectedEditorComponent}" has no style controls.` + ); + return; + } + + const preferredTarget = + typeof styleObject._target === "string" + ? styleObject._target + : typeof styleObject._namespace === "string" + ? styleObject._namespace + : undefined; - if (appliedStyles.length > 0) { - editorState.setSelectedCompNames(new Set([selectedEditorComponent]), "applyStyle"); - - if (usingFallback) { - message.success(`Applied ${appliedStyles.length} fallback style(s) to component "${selectedEditorComponent}": ${appliedStyles.join(', ')}`); + const appliedStyles: string[] = []; + const skipped: string[] = []; + + for (const [styleKey, styleValue] of Object.entries(styleObject)) { + if (RESERVED_KEYS.has(styleKey)) continue; + try { + const { applied } = applyStyleKey( + namespaces, + styleKey, + styleValue, + preferredTarget + ); + if (applied) { + appliedStyles.push(styleKey); } else { - message.success(`Applied ${appliedStyles.length} style(s) to component "${selectedEditorComponent}": ${appliedStyles.join(', ')}`); + skipped.push(styleKey); } - } else { - message.warning('No styles were applied. Check if the component supports styling.'); + } catch (error) { + console.error(`Error applying style ${styleKey}:`, error); + skipped.push(styleKey); } - - } catch (error) { - console.error('Error applying styles:', error); - message.error('Failed to apply styles. Please try again.'); } - } -}; \ No newline at end of file + + if (skipped.length > 0) { + console.warn( + `[applyStyleAction] keys not found on "${selectedEditorComponent}":`, + skipped, + "available namespaces:", + Object.keys(namespaces) + ); + } + + if (appliedStyles.length > 0) { + editorState.setSelectedCompNames( + new Set([selectedEditorComponent]), + "applyStyle" + ); + + const prefix = usingFallback ? "fallback " : ""; + message.success( + `Applied ${appliedStyles.length} ${prefix}style(s) to "${selectedEditorComponent}": ${appliedStyles.join(", ")}` + ); + } else { + message.warning( + "No styles were applied. Check if the keys match the component's style fields." + ); + } + }, +}; From d2fe7b1c474f12f4f3b54e0bf48f63c76c4e4d64 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 14 May 2026 21:22:07 +0500 Subject: [PATCH 08/33] refactor componentStyling + improve system prompt --- .../components/ChatPanelContainer.tsx | 1 - .../actions/automator/actionsCatalog.ts | 20 +- .../actions/automator/componentCatalog.ts | 68 +----- .../actions/automator/systemPrompt.ts | 72 +++--- .../actions/componentConfiguration.ts | 5 +- .../preLoadComp/actions/componentStyling.ts | 231 +++--------------- 6 files changed, 92 insertions(+), 305 deletions(-) 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 83b1028e1a..f4688cf0d7 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx @@ -86,7 +86,6 @@ function buildExecuteParams( 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; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts index fbbdf54568..4f7fbee779 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts @@ -53,7 +53,8 @@ export const ACTIONS_CATALOG: ActionCatalogEntry[] = [ }, { action: "nest_component", - purpose: "Place a new component inside an existing container.", + purpose: + "Create a NEW component inside an existing container. Do not use this to move or reparent an existing component; if the requested component already exists, ask/explain instead of duplicating it.", required: ["component", "component_name", "parent_component_name", "layout", "action_parameters"], example: { action: "nest_component", @@ -133,19 +134,20 @@ export const ACTIONS_CATALOG: ActionCatalogEntry[] = [ { action: "set_style", purpose: - "Apply visual styles (color, font, spacing, border, animation, …) to a component. Pass a flat object — keys are auto-routed to the matching style namespace exposed by the component (`style`, `labelStyle`, `inputFieldStyle`, `disabledStyle`, `animationStyle`, `headerStyle`, `bodyStyle`, etc.). Use `_target: ''` only when the same key exists in multiple namespaces and you must disambiguate.", + "Apply basic visual styles to a component. Pass an object grouped by explicit style namespace (`style`, `labelStyle`, `inputFieldStyle`, `headerStyle`, `bodyStyle`, etc.). Do not pass flat style keys and do not use animation styles.", required: ["component_name", "action_parameters"], - optional: ["action_parameters._target"], example: { action: "set_style", component_name: "submitBtn", action_parameters: { - background: "#1677ff", - text: "#ffffff", - radius: "8px", - textSize: "14px", - textWeight: "600", - padding: "8px 16px", + style: { + background: "#1677ff", + text: "#ffffff", + radius: "8px", + textSize: "14px", + textWeight: "600", + padding: "8px 16px", + }, }, }, }, diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts index 5221915026..eca465052c 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts @@ -33,9 +33,8 @@ export interface LayoutPropertyDescriptor { * Map of style namespace → list of style keys that can be passed to * `set_style`. Most components have a single `style` namespace; inputs and * containers expose several (e.g. `labelStyle`, `inputFieldStyle`, - * `disabledStyle`, `animationStyle`). The `set_style` executor auto-routes - * each key to the matching namespace so models can usually pass a flat object - * without specifying `_target`. + * `headerStyle`, `bodyStyle`). The `set_style` executor expects values grouped + * by explicit namespace. */ export type StylePropertyMap = Record; @@ -71,9 +70,8 @@ export interface ComponentCatalogEntry { layoutProperties?: Record; /** * Style properties grouped by style namespace, used by `set_style`. - * Pass these as a flat object — `set_style` routes each key automatically. - * Use `_target: ""` only when the same key exists in multiple - * namespaces and you need to disambiguate. + * Pass these grouped by namespace, e.g. + * `{ style: { background: "#fff" }, labelStyle: { label: "#111" } }`. */ styleProperties?: StylePropertyMap; } @@ -99,7 +97,6 @@ const COMMON_STYLE_KEYS = [ "margin", "padding", "lineHeight", - "rotation", ] as const; const CONTAINER_STYLE_KEYS = [ @@ -113,12 +110,6 @@ const CONTAINER_STYLE_KEYS = [ "boxShadow", "boxShadowColor", "opacity", - "rotation", - "backgroundImage", - "backgroundImageRepeat", - "backgroundImageSize", - "backgroundImagePosition", - "backgroundImageOrigin", ] as const; const INPUT_LIKE_STYLE_KEYS = [ @@ -161,19 +152,6 @@ const LABEL_STYLE_KEYS = [ "validate", ] as const; -const ANIMATION_STYLE_KEYS = [ - "animation", - "animationDelay", - "animationDuration", - "animationIterationCount", -] as const; - -const DISABLED_STYLE_KEYS = [ - "disabledBackground", - "disabledText", - "disabledBorder", -] as const; - const IMAGE_STYLE_KEYS = [ "margin", "padding", @@ -184,7 +162,6 @@ const IMAGE_STYLE_KEYS = [ "opacity", "boxShadow", "boxShadowColor", - "rotation", ] as const; const NAVIGATION_STYLE_KEYS = [ @@ -389,7 +366,6 @@ const TEXT: ComponentCatalogEntry = { }, styleProperties: { style: [...COMMON_STYLE_KEYS, "links"], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -433,8 +409,6 @@ const BUTTON: ComponentCatalogEntry = { }, styleProperties: { style: [...COMMON_STYLE_KEYS], - disabledStyle: [...DISABLED_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -473,8 +447,6 @@ const INPUT: ComponentCatalogEntry = { style: [...CONTAINER_STYLE_KEYS], labelStyle: [...LABEL_STYLE_KEYS], inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], - disabledStyle: [...DISABLED_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -509,8 +481,6 @@ const NUMBER_INPUT: ComponentCatalogEntry = { style: [...CONTAINER_STYLE_KEYS], labelStyle: [...LABEL_STYLE_KEYS], inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], - disabledStyle: [...DISABLED_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -544,7 +514,6 @@ const DROPDOWN: ComponentCatalogEntry = { style: [...COMMON_STYLE_KEYS, "accent", "validate"], labelStyle: [...LABEL_STYLE_KEYS], childrenInputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -570,7 +539,6 @@ const CHECKBOX: ComponentCatalogEntry = { style: [...COMMON_STYLE_KEYS], labelStyle: [...LABEL_STYLE_KEYS], inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS, "checkedBackground", "uncheckedBackground", "uncheckedBorder", "hoverBackground"], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -606,7 +574,6 @@ const FORM: ComponentCatalogEntry = { headerStyle: [...CONTAINER_STYLE_KEYS], bodyStyle: [...CONTAINER_STYLE_KEYS], footerStyle: [...CONTAINER_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -639,7 +606,6 @@ const CONTAINER: ComponentCatalogEntry = { headerStyle: [...CONTAINER_STYLE_KEYS], bodyStyle: [...CONTAINER_STYLE_KEYS], footerStyle: [...CONTAINER_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -665,7 +631,6 @@ const MODAL: ComponentCatalogEntry = { }, styleProperties: { style: [...CONTAINER_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -691,7 +656,6 @@ const DRAWER: ComponentCatalogEntry = { }, styleProperties: { style: [...CONTAINER_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -719,7 +683,6 @@ const TABLE: ComponentCatalogEntry = { headerStyle: [...COMMON_STYLE_KEYS], rowStyle: [...COMMON_STYLE_KEYS], cellStyle: [...COMMON_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, example: { columns: [ @@ -764,7 +727,6 @@ const LIST_VIEW: ComponentCatalogEntry = { }, styleProperties: { style: [...CONTAINER_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, example: { container: {}, @@ -814,7 +776,6 @@ const IMAGE: ComponentCatalogEntry = { }, styleProperties: { style: [...IMAGE_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -833,7 +794,6 @@ const DIVIDER: ComponentCatalogEntry = { }, styleProperties: { style: [...COMMON_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -858,7 +818,6 @@ const DATE: ComponentCatalogEntry = { style: [...COMMON_STYLE_KEYS, "accent", "validate"], labelStyle: [...LABEL_STYLE_KEYS], inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -879,7 +838,6 @@ const SWITCH: ComponentCatalogEntry = { styleProperties: { style: ["handle", "unchecked", "checked", "margin", "padding"], labelStyle: [...LABEL_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -915,8 +873,6 @@ const TEXT_AREA: ComponentCatalogEntry = { style: [...CONTAINER_STYLE_KEYS], labelStyle: [...LABEL_STYLE_KEYS], inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], - disabledStyle: [...DISABLED_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -940,8 +896,6 @@ const PASSWORD: ComponentCatalogEntry = { style: [...CONTAINER_STYLE_KEYS], labelStyle: [...LABEL_STYLE_KEYS], inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], - disabledStyle: [...DISABLED_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -985,7 +939,6 @@ const CHART: ComponentCatalogEntry = { "margin", "padding", ], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -1008,7 +961,6 @@ const CARD: ComponentCatalogEntry = { style: [...CONTAINER_STYLE_KEYS, "IconColor", "activateColor"], headerStyle: [...COMMON_STYLE_KEYS], bodyStyle: [...CONTAINER_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -1033,7 +985,6 @@ const TABBED_CONTAINER: ComponentCatalogEntry = { headerStyle: [...COMMON_STYLE_KEYS], bodyStyle: [...CONTAINER_STYLE_KEYS], tabsStyle: [...COMMON_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -1053,7 +1004,6 @@ const VIDEO: ComponentCatalogEntry = { }, styleProperties: { style: ["margin", "padding"], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -1077,7 +1027,6 @@ const AVATAR: ComponentCatalogEntry = { style: ["background", "fill"], avatarLabelStyle: [...COMMON_STYLE_KEYS], avatarContainerStyle: [...CONTAINER_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -1093,7 +1042,6 @@ const PROGRESS: ComponentCatalogEntry = { }, styleProperties: { style: ["text", "textSize", "textWeight", "fontFamily", "fontStyle", "radius", "margin", "padding", "lineHeight", "track", "fill", "success"], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -1116,7 +1064,6 @@ const RATING: ComponentCatalogEntry = { styleProperties: { style: ["checked", "unchecked", "margin", "padding"], labelStyle: [...LABEL_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -1140,8 +1087,6 @@ const SLIDER: ComponentCatalogEntry = { styleProperties: { style: ["fill", "thumb", "thumbBorder", "track", "margin", "padding"], labelStyle: [...LABEL_STYLE_KEYS], - disabledStyle: ["disabledFill", "disabledTrack"], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -1163,7 +1108,6 @@ const NAVIGATION: ComponentCatalogEntry = { }, styleProperties: { style: [...NAVIGATION_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -1194,7 +1138,6 @@ const TIMELINE: ComponentCatalogEntry = { "padding", "radius", ], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; @@ -1213,8 +1156,6 @@ const STEP: ComponentCatalogEntry = { }, styleProperties: { style: [...COMMON_STYLE_KEYS], - disabledStyle: [...DISABLED_STYLE_KEYS], - animationStyle: [...ANIMATION_STYLE_KEYS], }, example: { value: "1", @@ -1265,7 +1206,6 @@ const RADIO: ComponentCatalogEntry = { style: [...COMMON_STYLE_KEYS], labelStyle: [...LABEL_STYLE_KEYS], inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS, "checkedBackground", "uncheckedBackground", "uncheckedBorder", "hoverBackground"], - animationStyle: [...ANIMATION_STYLE_KEYS], }, }; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts index 59daedb145..a0d43685b4 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts @@ -26,6 +26,25 @@ canvas. # How to respond +The conversation history may include older user requests and older assistant +explanations. Treat them as background only. The latest user message is the +only instruction you should execute now. + +Do NOT replay, repeat, combine, or re-emit actions for previous turns. Previous +changes are already represented in EDITOR_CONTEXT. Return only the actions +needed for the latest user message. + +Use \`place_component\` / \`nest_component\` only when the latest user message +clearly asks to create, add, place, or insert a new component. For follow-up +edits like "make it", "set it", "change this", or "move the component", update +the existing selected or named component from EDITOR_CONTEXT instead of creating +a duplicate. + +\`nest_component\` also creates a NEW component. Do not use it to move an +existing component into a container. If the user asks to put an existing +component inside a container and there is no action for reparenting it, explain +that limitation instead of creating a duplicate component. + You have a tool called \`execute_automator_actions\`. Use it when you are ready to modify the canvas. When the request is ambiguous or you need clarification, respond with plain text instead — do NOT call the tool with @@ -84,32 +103,30 @@ There are TWO families of UI edits, and each has its own action: \`layoutProperties\` field in COMPONENT_CATALOG lists the exact keys and their allowed values. -2. **\`set_style\`** — visual / CSS-like properties living inside the +2. **\`set_style\`** — basic visual / CSS-like properties living inside the component's style namespaces (\`style\`, \`labelStyle\`, \`inputFieldStyle\`, - \`disabledStyle\`, \`animationStyle\`, \`headerStyle\`, \`bodyStyle\`, …). - Pass a flat object — keys are auto-routed to the matching namespace. + \`headerStyle\`, \`bodyStyle\`, …). Always group values by explicit + namespace. Do NOT pass flat style keys. + + Correct: + \`{ "style": { "background": "#1677ff", "text": "#ffffff" } }\` + + Correct for an input label: + \`{ "labelStyle": { "label": "#1677ff", "textSize": "14px" } }\` + + Incorrect: + \`{ "background": "#1677ff", "text": "#ffffff" }\` + For each component the \`styleProperties\` field in COMPONENT_CATALOG lists which keys live in which namespace. - When the same key exists in multiple namespaces (e.g. \`text\` in - \`labelStyle\` and \`inputFieldStyle\`) include a \`_target\` field to - disambiguate, e.g. - \`{ "_target": "labelStyle", "text": "#1677ff" }\`. - Common style-key vocabulary: - - text/colour: \`text\` (foreground), \`background\`, \`links\`, \`accent\` + - text/colour: \`text\` (foreground), \`label\`, \`background\`, \`links\`, \`accent\` - typography: \`textSize\`, \`textWeight\`, \`fontFamily\`, \`fontStyle\`, \`textTransform\`, \`textDecoration\`, \`lineHeight\` - box model: \`margin\`, \`padding\`, \`border\`, \`borderStyle\`, - \`borderWidth\`, \`radius\`, \`opacity\`, \`boxShadow\`, \`boxShadowColor\`, - \`rotation\` - - background image: \`backgroundImage\`, \`backgroundImageRepeat\`, - \`backgroundImageSize\`, \`backgroundImagePosition\`, - \`backgroundImageOrigin\` - - animation (in \`animationStyle\`): \`animation\`, \`animationDelay\`, - \`animationDuration\`, \`animationIterationCount\` - - disabled state (in \`disabledStyle\`): \`disabledBackground\`, - \`disabledText\`, \`disabledBorder\` + \`borderWidth\`, \`radius\`, \`opacity\`, \`boxShadow\`, \`boxShadowColor\` + - input hints: \`placeholder\`, \`validate\` 3. **\`align_component\`** — moves the COMPONENT to the left/center/right of the canvas grid. It does NOT change text or content alignment inside the @@ -121,21 +138,18 @@ There are TWO families of UI edits, and each has its own action: - Center text inside a Text component: set_properties { horizontalAlignment: "center" } - Larger heading text: - set_style { textSize: "24px", textWeight: "700", lineHeight: "1.3" } + set_style { style: { textSize: "24px", textWeight: "700", lineHeight: "1.3" } } - Coloured primary button: - set_style { background: "#1677ff", text: "#ffffff", radius: "8px", - padding: "8px 16px", textWeight: "600" } + set_style { style: { background: "#1677ff", text: "#ffffff", radius: "8px", + padding: "8px 16px", textWeight: "600" } } - Accent input border + larger label: - set_style { _target: "inputFieldStyle", border: "#1677ff", - borderWidth: "2px", radius: "6px" } - set_style { _target: "labelStyle", textSize: "14px", textWeight: "600" } + set_style { inputFieldStyle: { border: "#1677ff", + borderWidth: "2px", radius: "6px" } } + set_style { labelStyle: { textSize: "14px", textWeight: "600" } } - Soft card with shadow: - set_style { background: "#ffffff", radius: "12px", border: "#e5e7eb", + set_style { style: { background: "#ffffff", radius: "12px", border: "#e5e7eb", borderWidth: "1px", padding: "16px", - boxShadow: "0 4px 12px", boxShadowColor: "rgba(0,0,0,0.08)" } -- Animate a component on mount: - set_style { animation: "fadeIn", animationDuration: "0.6s", - animationIterationCount: "1" } + boxShadow: "0 4px 12px", boxShadowColor: "rgba(0,0,0,0.08)" } } - Hide / disable a component: set_properties { hidden: true } set_properties { disabled: true } diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentConfiguration.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentConfiguration.ts index ca493f62f3..5bd20ffc88 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentConfiguration.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentConfiguration.ts @@ -1,4 +1,5 @@ import { message } from "antd"; +import merge from "lodash/merge"; import { ActionConfig, ActionExecuteParams } from "../types"; export const configureComponentAction: ActionConfig = { @@ -41,7 +42,7 @@ export const configureComponentAction: ActionConfig = { } const itemComp = comp.children.comp; - const config = { ...itemComp.toJsonValue(), ...compProperties }; + const config = merge({}, itemComp.toJsonValue(), compProperties); itemComp.dispatchChangeValueAction(config); message.success(`Properties updated on "${componentName}"`); @@ -50,4 +51,4 @@ export const configureComponentAction: ActionConfig = { message.error("Failed to set component properties"); } } -}; \ No newline at end of file +}; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts index 0af2b3a334..3eb5841b86 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts @@ -1,117 +1,7 @@ import { message } from "antd"; +import merge from "lodash/merge"; import { ActionConfig, ActionExecuteParams } from "../types"; -// Fallback constant style object to apply if the model returns invalid JSON. -// This is a safety net only — real values always come from the LLM payload. -const FALLBACK_STYLE_OBJECT = { - fontSize: "10px", - fontWeight: "500", - color: "#333333", - backgroundColor: "#ffffff", - padding: "8px", - borderRadius: "4px", - border: "1px solid #ddd", -}; - -// Reserved keys in the style payload that are NOT real style fields. Used to -// disambiguate which `*Style` namespace a flat object should be routed to. -const RESERVED_KEYS = new Set(["_target", "_namespace"]); - -/** - * Resolve the children object that holds the component's style namespaces. - * - * Most leaf components store their style children directly on - * `comp.children.comp.children` (e.g. `style`, `labelStyle`, `animationStyle`). - * Some composite components (form/list/etc.) wrap them under a sub-component - * keyed by the `compType` value, so we look there as a fallback. - */ -function resolveStyleChildrenRoot(comp: any): any { - const innerChildren = comp?.children?.comp?.children; - if (!innerChildren) return null; - - if (innerChildren.style) return innerChildren; - - const compType = comp?.children?.compType?.getView?.(); - const wrapper = compType ? innerChildren[compType] : null; - if (wrapper?.children) return wrapper.children; - - return innerChildren; -} - -/** - * Collect every style-like child container exposed by the component, keyed by - * its name. Recognises `style` plus any sibling whose name ends with `Style` - * (e.g. `labelStyle`, `inputFieldStyle`, `disabledStyle`, `animationStyle`, - * `headerStyle`, `bodyStyle`, …). - */ -function collectStyleNamespaces(rootChildren: any): Record { - if (!rootChildren) return {}; - const out: Record = {}; - for (const key of Object.keys(rootChildren)) { - if (key === "style" || key.endsWith("Style")) { - out[key] = rootChildren[key]; - } - } - return out; -} - -/** - * Apply a single style key/value to the first namespace that owns it. - * - * Routing order: - * 1. The explicit `_target` namespace (when provided). - * 2. `style` (if present and contains the key). - * 3. Any other `*Style` namespace, in declaration order. - * - * Returns `true` when the value was applied, `false` otherwise so the caller - * can collect a useful warning. - */ -function applyStyleKey( - namespaces: Record, - styleKey: string, - styleValue: unknown, - preferredTarget?: string -): { applied: boolean; namespace?: string } { - const tryNamespace = (nsName: string): boolean => { - const ns = namespaces[nsName]; - if (!ns) return false; - - // Most style controls expose nested `children[styleKey]`. - const nested = ns.children?.[styleKey]; - if (nested?.dispatchChangeValueAction) { - nested.dispatchChangeValueAction(styleValue); - return true; - } - // Older style controls expose the key directly on the namespace object. - const direct = ns[styleKey]; - if (direct?.dispatchChangeValueAction) { - direct.dispatchChangeValueAction(styleValue); - return true; - } - return false; - }; - - if (preferredTarget && namespaces[preferredTarget]) { - if (tryNamespace(preferredTarget)) { - return { applied: true, namespace: preferredTarget }; - } - } - - if (namespaces.style && tryNamespace("style")) { - return { applied: true, namespace: "style" }; - } - - for (const nsName of Object.keys(namespaces)) { - if (nsName === "style") continue; - if (preferredTarget && nsName === preferredTarget) continue; - if (tryNamespace(nsName)) { - return { applied: true, namespace: nsName }; - } - } - - return { applied: false }; -} - export const applyStyleAction: ActionConfig = { key: "apply-style", label: "Apply style to component", @@ -119,108 +9,49 @@ export const applyStyleAction: ActionConfig = { requiresEditorComponentSelection: true, requiresStyle: true, requiresInput: true, - inputPlaceholder: "Enter CSS styles (JSON format)", - inputType: "textarea", + inputPlaceholder: "Enter namespaced styles as JSON", + inputType: "json", validation: (value: string) => { if (!value.trim()) return "Styles are required"; - return null; - }, - execute: async (params: ActionExecuteParams) => { - const { selectedEditorComponent, actionValue, editorState } = params; - - if (!selectedEditorComponent || !editorState) { - message.error("Component and editor state are required"); - return; - } - - let styleObject: Record = {}; - let usingFallback = false; - try { - if (typeof actionValue === "string") { - styleObject = JSON.parse(actionValue); - } else { - styleObject = (actionValue as any) || {}; - } - } catch (e) { - styleObject = FALLBACK_STYLE_OBJECT; - usingFallback = true; - } - - if (!styleObject || typeof styleObject !== "object") { - message.error("Invalid style payload"); - return; + JSON.parse(value); + return null; + } catch { + return "Invalid JSON format"; } - - const comp = editorState.getUICompByName(selectedEditorComponent); - if (!comp) { - message.error(`Component "${selectedEditorComponent}" not found`); + }, + execute: async (params: ActionExecuteParams) => { + const { actionPayload, editorState } = params; + const componentName = + actionPayload?.component_name || params.selectedEditorComponent; + const stylePatch = { ...(actionPayload?.action_parameters || {}) }; + delete stylePatch.animationStyle; + + if (!componentName) { + message.error("No component name provided for set_style"); return; } - const rootChildren = resolveStyleChildrenRoot(comp); - const namespaces = collectStyleNamespaces(rootChildren); - - if (Object.keys(namespaces).length === 0) { - message.warning( - `Component "${selectedEditorComponent}" has no style controls.` - ); + if (!editorState) { + message.error("Editor state is required"); return; } - const preferredTarget = - typeof styleObject._target === "string" - ? styleObject._target - : typeof styleObject._namespace === "string" - ? styleObject._namespace - : undefined; - - const appliedStyles: string[] = []; - const skipped: string[] = []; - - for (const [styleKey, styleValue] of Object.entries(styleObject)) { - if (RESERVED_KEYS.has(styleKey)) continue; - try { - const { applied } = applyStyleKey( - namespaces, - styleKey, - styleValue, - preferredTarget - ); - if (applied) { - appliedStyles.push(styleKey); - } else { - skipped.push(styleKey); - } - } catch (error) { - console.error(`Error applying style ${styleKey}:`, error); - skipped.push(styleKey); + try { + const comp = editorState.getUICompByName(componentName); + if (!comp) { + message.error(`Component "${componentName}" not found`); + return; } - } - - if (skipped.length > 0) { - console.warn( - `[applyStyleAction] keys not found on "${selectedEditorComponent}":`, - skipped, - "available namespaces:", - Object.keys(namespaces) - ); - } - if (appliedStyles.length > 0) { - editorState.setSelectedCompNames( - new Set([selectedEditorComponent]), - "applyStyle" - ); + const itemComp = comp.children.comp; + const config = merge({}, itemComp.toJsonValue(), stylePatch); + itemComp.dispatchChangeValueAction(config); - const prefix = usingFallback ? "fallback " : ""; - message.success( - `Applied ${appliedStyles.length} ${prefix}style(s) to "${selectedEditorComponent}": ${appliedStyles.join(", ")}` - ); - } else { - message.warning( - "No styles were applied. Check if the keys match the component's style fields." - ); + message.success(`Styles updated on "${componentName}"`); + } catch (error) { + console.error("Error setting styles:", error); + message.error("Failed to set component styles"); } }, }; From 76fce5ef5deb94cdc1fe6402773c18c606df204f Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 14 May 2026 23:55:49 +0500 Subject: [PATCH 09/33] refactor componentCatlog --- .../entries.ts} | 535 ++---------------- .../automator/automatorComponents/index.ts | 18 + .../automator/automatorComponents/presets.ts | 144 +++++ .../automator/automatorComponents/types.ts | 44 ++ .../actions/automator/editorSnapshot.ts | 58 -- .../preLoadComp/actions/automator/index.ts | 12 +- .../actions/automator/orchestrator.ts | 27 +- .../actions/automator/systemPrompt.ts | 26 +- .../actions/automator/toolDefinitions.ts | 2 +- 9 files changed, 289 insertions(+), 577 deletions(-) rename client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/{componentCatalog.ts => automatorComponents/entries.ts} (64%) create mode 100644 client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/index.ts create mode 100644 client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/presets.ts create mode 100644 client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/types.ts diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/entries.ts similarity index 64% rename from client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts rename to client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/entries.ts index eca465052c..03d2b66d06 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/entries.ts @@ -1,341 +1,22 @@ -// client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/componentCatalog.ts - -import { uiCompRegistry, type UICompManifest } from "comps/uiCompRegistry"; - -/** - * Component reference for the Automator. - * - * Curated entries provide known-good property examples for common components. - * Every registered Lowcoder component is then added from `uiCompRegistry` so - * the Automator can discover the full insertion panel, including newer - * components like Chat, Chat Box, and Chat Controller. - */ - -/** - * Schema describing a top-level UI / layout property the model can set with - * `set_properties`. These are the props that drive component behavior or - * positional layout (text-align, vertical alignment, autoHeight, type, etc.) - * and live as direct children of the component, NOT inside the `style` - * namespace. - */ -export interface LayoutPropertyDescriptor { - /** Human-readable hint shown to the model. */ - description?: string; - /** Allowed string values when this property is a fixed enum. */ - enum?: readonly string[]; - /** Primitive type when the value is not enum-restricted. */ - type?: "string" | "number" | "boolean" | "object"; - /** Sample value the model can imitate verbatim. */ - example?: unknown; -} - -/** - * Map of style namespace → list of style keys that can be passed to - * `set_style`. Most components have a single `style` namespace; inputs and - * containers expose several (e.g. `labelStyle`, `inputFieldStyle`, - * `headerStyle`, `bodyStyle`). The `set_style` executor expects values grouped - * by explicit namespace. - */ -export type StylePropertyMap = Record; - -export interface ComponentCatalogEntry { - /** Component type as registered in `uiCompRegistry` */ - type: string; - /** Whether the component can have children nested under `.container`. */ - isContainer?: boolean; - /** Default grid layout (w / h) for sensible initial sizing. */ - defaultLayout: { w: number; h: number }; - /** Required property keys for `action_parameters`. */ - required: string[]; - /** Optional property keys worth knowing about. */ - optional?: string[]; - /** Realistic example `action_parameters` payload. */ - example: Record; - /** Notes the model should heed. */ - notes?: string; - /** Display name shown in the Lowcoder component panel. */ - name?: string; - /** English display name from the component manifest. */ - enName?: string; - /** Component panel categories. Empty means hidden from normal insertion UI. */ - categories?: readonly string[]; - /** Short manifest description when it is serialisable. */ - description?: string; - /** - * Top-level UI / layout properties to be set with `set_properties`. - * Only properties controlling *behaviour or layout* (alignment, autoHeight, - * type, disabled, …) belong here — visual/CSS-like props live in - * `styleProperties` and are set with `set_style`. - */ - layoutProperties?: Record; - /** - * Style properties grouped by style namespace, used by `set_style`. - * Pass these grouped by namespace, e.g. - * `{ style: { background: "#fff" }, labelStyle: { label: "#111" } }`. - */ - styleProperties?: StylePropertyMap; -} - -// ── Style key presets ──────────────────────────────────────────────────────── -// Mirror the field lists from `comps/controls/styleControlConstants.tsx` so the -// model knows what keys it can pass to `set_style`. Keep these compact — they -// are inlined into the system prompt. - -const COMMON_STYLE_KEYS = [ - "background", - "text", - "textTransform", - "textDecoration", - "textSize", - "textWeight", - "fontFamily", - "fontStyle", - "border", - "borderStyle", - "borderWidth", - "radius", - "margin", - "padding", - "lineHeight", -] as const; - -const CONTAINER_STYLE_KEYS = [ - "background", - "border", - "borderStyle", - "borderWidth", - "radius", - "margin", - "padding", - "boxShadow", - "boxShadowColor", - "opacity", -] as const; - -const INPUT_LIKE_STYLE_KEYS = [ - "background", - "boxShadow", - "boxShadowColor", - "text", - "textTransform", - "textDecoration", - "textSize", - "textWeight", - "fontFamily", - "fontStyle", - "border", - "borderStyle", - "borderWidth", - "radius", - "margin", - "padding", - "placeholder", - "accent", - "validate", -] as const; - -const LABEL_STYLE_KEYS = [ - "background", - "label", - "textTransform", - "textDecoration", - "textSize", - "textWeight", - "fontFamily", - "fontStyle", - "borderStyle", - "borderWidth", - "margin", - "padding", - "placeholder", - "accent", - "validate", -] as const; - -const IMAGE_STYLE_KEYS = [ - "margin", - "padding", - "border", - "borderStyle", - "borderWidth", - "radius", - "opacity", - "boxShadow", - "boxShadowColor", -] as const; - -const NAVIGATION_STYLE_KEYS = [ - "background", - "border", - "borderStyle", - "borderWidth", - "radius", - "margin", - "padding", - "accent", -] as const; - -// ── Layout property presets ───────────────────────────────────────────────── - -const TEXT_HORIZONTAL_ALIGNMENT: LayoutPropertyDescriptor = { - description: "Horizontal text alignment inside the component.", - enum: ["left", "center", "right", "justify"], -}; - -const ALIGN_HORIZONTAL: LayoutPropertyDescriptor = { - description: "Horizontal alignment.", - enum: ["left", "center", "right"], -}; - -const VERTICAL_ALIGNMENT: LayoutPropertyDescriptor = { - description: "Vertical alignment.", - enum: ["flex-start", "center", "flex-end"], -}; - -const AUTO_HEIGHT: LayoutPropertyDescriptor = { - description: "Whether the component auto-sizes its height to its content.", - enum: ["auto", "fixed"], -}; - -const HIDDEN: LayoutPropertyDescriptor = { - description: "Hide the component at runtime.", - type: "boolean", -}; - -const DISABLED: LayoutPropertyDescriptor = { - description: "Disable the component at runtime.", - type: "boolean", -}; - -const LOADING: LayoutPropertyDescriptor = { - description: "Show a loading indicator on the component.", - type: "boolean", -}; - -const LABEL_OBJECT: LayoutPropertyDescriptor = { - description: - "Field label config: { text, position: 'row'|'column', align: 'left'|'center'|'right', width: number, hidden?: boolean, tooltip?: string }.", - type: "object", - example: { text: "Email", position: "row", align: "left" }, -}; - -export const LOWCODER_COMPONENT_TYPES: string[] = [ - "chart", - "basicChart", - "barChart", - "lineChart", - "pieChart", - "scatterChart", - "candleStickChart", - "funnelChart", - "gaugeChart", - "graphChart", - "heatmapChart", - "radarChart", - "sankeyChart", - "sunburstChart", - "themeriverChart", - "treeChart", - "treemapChart", - "openLayersGeoMap", - "chartsGeoMap", - "table", - "tableLite", - "pivotTable", - "mermaid", - "timeline", - "responsiveLayout", - "pageLayout", - "columnLayout", - "splitLayout", - "floatTextContainer", - "card", - "tabbedContainer", - "collapsibleContainer", - "container", - "listView", - "grid", - "multiTags", - "modal", - "drawer", - "toast", - "divider", - "navigation", - "step", - "cascader", - "link", - "floatingButton", - "calendar", - "timer", - "sharingcomponent", - "videocomponent", - "meeting", - "avatar", - "avatarGroup", - "comment", - "mention", - "chatController", - "chatBox", - "form", - "jsonSchemaForm", - "jsonEditor", - "jsonExplorer", - "richTextEditor", - "input", - "password", - "numberInput", - "textArea", - "autocomplete", - "switch", - "checkbox", - "radio", - "date", - "dateRange", - "time", - "timeRange", - "slider", - "rangeSlider", - "button", - "controlButton", - "dropdown", - "toggleButton", - "segmentedControl", - "rating", - "ganttChart", - "kanban", - "hillchart", - "bpmnEditor", - "progress", - "progressCircle", - "file", - "fileViewer", - "image", - "carousel", - "audio", - "video", - "shape", - "jsonLottie", - "icon", - "imageEditor", - "colorPicker", - "qrCode", - "scanner", - "signature", - "select", - "tour", - "multiSelect", - "tree", - "treeSelect", - "transfer", - "turnstileCaptcha", - "chat", - "iframe", - "custom", - "module", - "text", -]; - -const TEXT: ComponentCatalogEntry = { +import type { AutomatorComponentEntry } from "./types"; +import { + ALIGN_HORIZONTAL, + AUTO_HEIGHT, + COMMON_STYLE_KEYS, + CONTAINER_STYLE_KEYS, + DISABLED, + HIDDEN, + IMAGE_STYLE_KEYS, + INPUT_LIKE_STYLE_KEYS, + LABEL_OBJECT, + LABEL_STYLE_KEYS, + LOADING, + NAVIGATION_STYLE_KEYS, + TEXT_HORIZONTAL_ALIGNMENT, + VERTICAL_ALIGNMENT, +} from "./presets"; + +const TEXT: AutomatorComponentEntry = { type: "text", defaultLayout: { w: 12, h: 4 }, required: ["text"], @@ -369,7 +50,7 @@ const TEXT: ComponentCatalogEntry = { }, }; -const BUTTON: ComponentCatalogEntry = { +const BUTTON: AutomatorComponentEntry = { type: "button", defaultLayout: { w: 6, h: 5 }, required: ["text"], @@ -412,7 +93,7 @@ const BUTTON: ComponentCatalogEntry = { }, }; -const INPUT: ComponentCatalogEntry = { +const INPUT: AutomatorComponentEntry = { type: "input", defaultLayout: { w: 12, h: 6 }, required: ["label", "placeholder"], @@ -450,7 +131,7 @@ const INPUT: ComponentCatalogEntry = { }, }; -const NUMBER_INPUT: ComponentCatalogEntry = { +const NUMBER_INPUT: AutomatorComponentEntry = { type: "numberInput", defaultLayout: { w: 12, h: 6 }, required: ["label"], @@ -484,7 +165,7 @@ const NUMBER_INPUT: ComponentCatalogEntry = { }, }; -const DROPDOWN: ComponentCatalogEntry = { +const DROPDOWN: AutomatorComponentEntry = { type: "select", defaultLayout: { w: 12, h: 6 }, required: ["label", "options", "value"], @@ -517,7 +198,7 @@ const DROPDOWN: ComponentCatalogEntry = { }, }; -const CHECKBOX: ComponentCatalogEntry = { +const CHECKBOX: AutomatorComponentEntry = { type: "checkbox", defaultLayout: { w: 8, h: 5 }, required: ["label"], @@ -542,7 +223,7 @@ const CHECKBOX: ComponentCatalogEntry = { }, }; -const FORM: ComponentCatalogEntry = { +const FORM: AutomatorComponentEntry = { type: "form", isContainer: true, defaultLayout: { w: 12, h: 30 }, @@ -577,7 +258,7 @@ const FORM: ComponentCatalogEntry = { }, }; -const CONTAINER: ComponentCatalogEntry = { +const CONTAINER: AutomatorComponentEntry = { type: "container", isContainer: true, defaultLayout: { w: 12, h: 20 }, @@ -609,7 +290,7 @@ const CONTAINER: ComponentCatalogEntry = { }, }; -const MODAL: ComponentCatalogEntry = { +const MODAL: AutomatorComponentEntry = { type: "modal", isContainer: true, defaultLayout: { w: 12, h: 40 }, @@ -634,7 +315,7 @@ const MODAL: ComponentCatalogEntry = { }, }; -const DRAWER: ComponentCatalogEntry = { +const DRAWER: AutomatorComponentEntry = { type: "drawer", isContainer: true, defaultLayout: { w: 12, h: 40 }, @@ -659,7 +340,7 @@ const DRAWER: ComponentCatalogEntry = { }, }; -const TABLE: ComponentCatalogEntry = { +const TABLE: AutomatorComponentEntry = { type: "table", defaultLayout: { w: 24, h: 30 }, required: ["columns", "data"], @@ -705,7 +386,7 @@ const TABLE: ComponentCatalogEntry = { "`data` MUST be a stringified JSON array. Use {{currentCell}} in render unless told otherwise.", }; -const LIST_VIEW: ComponentCatalogEntry = { +const LIST_VIEW: AutomatorComponentEntry = { type: "listView", isContainer: true, defaultLayout: { w: 24, h: 30 }, @@ -738,7 +419,7 @@ const LIST_VIEW: ComponentCatalogEntry = { "container is the per-item template. Nest item components directly under '.container' (flat). Do NOT use body/header/footer.", }; -const IMAGE: ComponentCatalogEntry = { +const IMAGE: AutomatorComponentEntry = { type: "image", defaultLayout: { w: 8, h: 12 }, required: ["src"], @@ -779,7 +460,7 @@ const IMAGE: ComponentCatalogEntry = { }, }; -const DIVIDER: ComponentCatalogEntry = { +const DIVIDER: AutomatorComponentEntry = { type: "divider", defaultLayout: { w: 24, h: 2 }, required: [], @@ -797,7 +478,7 @@ const DIVIDER: ComponentCatalogEntry = { }, }; -const DATE: ComponentCatalogEntry = { +const DATE: AutomatorComponentEntry = { type: "date", defaultLayout: { w: 12, h: 6 }, required: ["label"], @@ -821,7 +502,7 @@ const DATE: ComponentCatalogEntry = { }, }; -const SWITCH: ComponentCatalogEntry = { +const SWITCH: AutomatorComponentEntry = { type: "switch", defaultLayout: { w: 6, h: 5 }, required: ["label"], @@ -841,7 +522,7 @@ const SWITCH: ComponentCatalogEntry = { }, }; -const TEXT_AREA: ComponentCatalogEntry = { +const TEXT_AREA: AutomatorComponentEntry = { type: "textArea", defaultLayout: { w: 12, h: 8 }, required: ["label"], @@ -876,7 +557,7 @@ const TEXT_AREA: ComponentCatalogEntry = { }, }; -const PASSWORD: ComponentCatalogEntry = { +const PASSWORD: AutomatorComponentEntry = { type: "password", defaultLayout: { w: 12, h: 6 }, required: ["label"], @@ -899,7 +580,7 @@ const PASSWORD: ComponentCatalogEntry = { }, }; -const CHART: ComponentCatalogEntry = { +const CHART: AutomatorComponentEntry = { type: "chart", defaultLayout: { w: 12, h: 20 }, required: ["chartType", "data"], @@ -942,7 +623,7 @@ const CHART: ComponentCatalogEntry = { }, }; -const CARD: ComponentCatalogEntry = { +const CARD: AutomatorComponentEntry = { type: "card", isContainer: true, defaultLayout: { w: 8, h: 15 }, @@ -964,7 +645,7 @@ const CARD: ComponentCatalogEntry = { }, }; -const TABBED_CONTAINER: ComponentCatalogEntry = { +const TABBED_CONTAINER: AutomatorComponentEntry = { type: "tabbedContainer", isContainer: true, defaultLayout: { w: 24, h: 30 }, @@ -988,7 +669,7 @@ const TABBED_CONTAINER: ComponentCatalogEntry = { }, }; -const VIDEO: ComponentCatalogEntry = { +const VIDEO: AutomatorComponentEntry = { type: "video", defaultLayout: { w: 12, h: 15 }, required: ["src"], @@ -1007,7 +688,7 @@ const VIDEO: ComponentCatalogEntry = { }, }; -const AVATAR: ComponentCatalogEntry = { +const AVATAR: AutomatorComponentEntry = { type: "avatar", defaultLayout: { w: 6, h: 6 }, required: ["icon", "iconSize"], @@ -1030,7 +711,7 @@ const AVATAR: ComponentCatalogEntry = { }, }; -const PROGRESS: ComponentCatalogEntry = { +const PROGRESS: AutomatorComponentEntry = { type: "progress", defaultLayout: { w: 12, h: 4 }, required: ["value"], @@ -1045,7 +726,7 @@ const PROGRESS: ComponentCatalogEntry = { }, }; -const RATING: ComponentCatalogEntry = { +const RATING: AutomatorComponentEntry = { type: "rating", defaultLayout: { w: 8, h: 5 }, required: ["label"], @@ -1067,7 +748,7 @@ const RATING: ComponentCatalogEntry = { }, }; -const SLIDER: ComponentCatalogEntry = { +const SLIDER: AutomatorComponentEntry = { type: "slider", defaultLayout: { w: 12, h: 5 }, required: ["label"], @@ -1090,7 +771,7 @@ const SLIDER: ComponentCatalogEntry = { }, }; -const NAVIGATION: ComponentCatalogEntry = { +const NAVIGATION: AutomatorComponentEntry = { type: "navigation", defaultLayout: { w: 24, h: 5 }, required: ["items"], @@ -1111,7 +792,7 @@ const NAVIGATION: ComponentCatalogEntry = { }, }; -const TIMELINE: ComponentCatalogEntry = { +const TIMELINE: AutomatorComponentEntry = { type: "timeline", defaultLayout: { w: 12, h: 15 }, required: ["value"], @@ -1141,7 +822,7 @@ const TIMELINE: ComponentCatalogEntry = { }, }; -const STEP: ComponentCatalogEntry = { +const STEP: AutomatorComponentEntry = { type: "step", defaultLayout: { w: 24, h: 6 }, required: ["value", "options"], @@ -1174,7 +855,7 @@ const STEP: ComponentCatalogEntry = { notes: "Step values must be numbers starting from 1.", }; -const RADIO: ComponentCatalogEntry = { +const RADIO: AutomatorComponentEntry = { type: "radio", defaultLayout: { w: 12, h: 5 }, required: ["label", "options"], @@ -1209,7 +890,7 @@ const RADIO: ComponentCatalogEntry = { }, }; -const CHAT: ComponentCatalogEntry = { +const CHAT: AutomatorComponentEntry = { type: "chat", defaultLayout: { w: 12, h: 20 }, required: [], @@ -1221,7 +902,7 @@ const CHAT: ComponentCatalogEntry = { notes: "AI chat component for embedding a conversational assistant in the app.", }; -const CHAT_BOX: ComponentCatalogEntry = { +const CHAT_BOX: AutomatorComponentEntry = { type: "chatBox", defaultLayout: { w: 12, h: 24 }, required: [], @@ -1230,7 +911,7 @@ const CHAT_BOX: ComponentCatalogEntry = { notes: "Chat UI for displaying messages and sending user input. Pair with chatController for realtime typing/presence.", }; -const CHAT_CONTROLLER: ComponentCatalogEntry = { +const CHAT_CONTROLLER: AutomatorComponentEntry = { type: "chatController", defaultLayout: { w: 12, h: 5 }, required: [], @@ -1239,7 +920,7 @@ const CHAT_CONTROLLER: ComponentCatalogEntry = { notes: "Realtime chat controller hook. Use with chatBox for presence and typing indicators.", }; -const CURATED_CATALOG: ComponentCatalogEntry[] = [ +export const AUTOMATOR_COMPONENTS: AutomatorComponentEntry[] = [ TEXT, BUTTON, INPUT, @@ -1275,115 +956,3 @@ const CURATED_CATALOG: ComponentCatalogEntry[] = [ CHAT_CONTROLLER, ]; -const CURATED_BY_TYPE = new Map(CURATED_CATALOG.map((entry) => [entry.type, entry])); - -function serialiseDescription(description: UICompManifest["description"]): string | undefined { - if (typeof description === "string") return description; - if (typeof description === "number") return String(description); - return undefined; -} - -function fallbackEntry(type: string, manifest: UICompManifest): ComponentCatalogEntry { - const layout = manifest.layoutInfo ?? { w: 6, h: 5 }; - return { - type, - name: manifest.name, - enName: manifest.enName, - categories: manifest.categories, - description: serialiseDescription(manifest.description), - isContainer: manifest.isContainer, - defaultLayout: { - w: layout.w, - h: layout.h, - }, - required: [], - optional: [], - example: {}, - notes: - "Registered Lowcoder component. Use an empty action_parameters object when no property shape is listed, or set properties afterward with set_properties.", - }; -} - -function typeOnlyFallbackEntry(type: string): ComponentCatalogEntry { - const curated = CURATED_BY_TYPE.get(type); - if (curated) return curated; - - return { - type, - defaultLayout: { w: 6, h: 5 }, - required: [], - optional: [], - example: {}, - notes: - "Lowcoder component listed in comps/index.tsx. Use an empty action_parameters object when no property shape is listed, or set properties afterward with set_properties.", - }; -} - -function mergeManifestMetadata( - entry: ComponentCatalogEntry, - manifest: UICompManifest -): ComponentCatalogEntry { - return { - ...entry, - name: manifest.name, - enName: manifest.enName, - categories: manifest.categories, - description: serialiseDescription(manifest.description), - isContainer: entry.isContainer ?? manifest.isContainer, - defaultLayout: entry.defaultLayout ?? manifest.layoutInfo ?? { w: 6, h: 5 }, - }; -} - -function buildFullCatalog(): ComponentCatalogEntry[] { - const registryEntries = Object.entries(uiCompRegistry); - const registryTypes = new Set(registryEntries.map(([type]) => type)); - const knownTypes = new Set(LOWCODER_COMPONENT_TYPES); - - const listedEntries = LOWCODER_COMPONENT_TYPES.map((type) => { - const manifest = uiCompRegistry[type]; - if (!manifest) return typeOnlyFallbackEntry(type); - - const curated = CURATED_BY_TYPE.get(type); - return curated - ? mergeManifestMetadata(curated, manifest) - : fallbackEntry(type, manifest); - }); - - const extraRegistryEntries = registryEntries - .filter(([type]) => !knownTypes.has(type)) - .map(([type, manifest]) => { - const curated = CURATED_BY_TYPE.get(type); - return curated - ? mergeManifestMetadata(curated, manifest) - : fallbackEntry(type, manifest); - }); - - const curatedOnlyEntries = CURATED_CATALOG.filter( - (entry) => !registryTypes.has(entry.type) && !knownTypes.has(entry.type) - ); - - return [...listedEntries, ...extraRegistryEntries, ...curatedOnlyEntries].sort((a, b) => { - const categoryA = a.categories?.[0] ?? ""; - const categoryB = b.categories?.[0] ?? ""; - if (categoryA !== categoryB) return categoryA.localeCompare(categoryB); - return (a.enName || a.name || a.type).localeCompare(b.enName || b.name || b.type); - }); -} - -/** - * Returns all registered Lowcoder components. If `onlyTypes` is provided we - * return those entries first and keep the remaining catalog afterward, so the - * model sees the user's requested component names without losing access to the - * rest of Lowcoder's palette. - */ -export function getComponentCatalog(onlyTypes?: string[]): ComponentCatalogEntry[] { - const fullCatalog = buildFullCatalog(); - if (!onlyTypes || onlyTypes.length === 0) return fullCatalog; - - const requested = new Set(onlyTypes); - const mentioned = fullCatalog.filter((c) => requested.has(c.type)); - const remaining = fullCatalog.filter((c) => !requested.has(c.type)); - return mentioned.length > 0 ? [...mentioned, ...remaining] : fullCatalog; -} - -export const COMPONENT_TYPES_DEFAULT: string[] = buildFullCatalog().map((c) => c.type); diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/index.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/index.ts new file mode 100644 index 0000000000..b6df97a977 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/index.ts @@ -0,0 +1,18 @@ +import { AUTOMATOR_COMPONENTS } from "./entries"; +import type { AutomatorComponentEntry } from "./types"; + +/** Returns the curated set of components Automator is allowed to use. */ +export function getAutomatorComponents(): AutomatorComponentEntry[] { + return [...AUTOMATOR_COMPONENTS]; +} + +export const AUTOMATOR_COMPONENT_TYPES: string[] = AUTOMATOR_COMPONENTS.map( + (component) => component.type +); + +export { AUTOMATOR_COMPONENTS } from "./entries"; +export type { + AutomatorComponentEntry, + AutomatorLayoutPropertyDescriptor, + AutomatorStylePropertyMap, +} from "./types"; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/presets.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/presets.ts new file mode 100644 index 0000000000..8adf1ff28d --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/presets.ts @@ -0,0 +1,144 @@ +import type { AutomatorLayoutPropertyDescriptor } from "./types"; + +// ── Style key presets ──────────────────────────────────────────────────────── +// Mirror the field lists from `comps/controls/styleControlConstants.tsx` so the +// model knows what keys it can pass to `set_style`. Keep these compact — they +// are inlined into the system prompt. + +export const COMMON_STYLE_KEYS = [ + "background", + "text", + "textTransform", + "textDecoration", + "textSize", + "textWeight", + "fontFamily", + "fontStyle", + "border", + "borderStyle", + "borderWidth", + "radius", + "margin", + "padding", + "lineHeight", +] as const; + +export const CONTAINER_STYLE_KEYS = [ + "background", + "border", + "borderStyle", + "borderWidth", + "radius", + "margin", + "padding", + "boxShadow", + "boxShadowColor", + "opacity", +] as const; + +export const INPUT_LIKE_STYLE_KEYS = [ + "background", + "boxShadow", + "boxShadowColor", + "text", + "textTransform", + "textDecoration", + "textSize", + "textWeight", + "fontFamily", + "fontStyle", + "border", + "borderStyle", + "borderWidth", + "radius", + "margin", + "padding", + "placeholder", + "accent", + "validate", +] as const; + +export const LABEL_STYLE_KEYS = [ + "background", + "label", + "textTransform", + "textDecoration", + "textSize", + "textWeight", + "fontFamily", + "fontStyle", + "borderStyle", + "borderWidth", + "margin", + "padding", + "placeholder", + "accent", + "validate", +] as const; + +export const IMAGE_STYLE_KEYS = [ + "margin", + "padding", + "border", + "borderStyle", + "borderWidth", + "radius", + "opacity", + "boxShadow", + "boxShadowColor", +] as const; + +export const NAVIGATION_STYLE_KEYS = [ + "background", + "border", + "borderStyle", + "borderWidth", + "radius", + "margin", + "padding", + "accent", +] as const; + +// ── Layout property presets ───────────────────────────────────────────────── + +export const TEXT_HORIZONTAL_ALIGNMENT: AutomatorLayoutPropertyDescriptor = { + description: "Horizontal text alignment inside the component.", + enum: ["left", "center", "right", "justify"], +}; + +export const ALIGN_HORIZONTAL: AutomatorLayoutPropertyDescriptor = { + description: "Horizontal alignment.", + enum: ["left", "center", "right"], +}; + +export const VERTICAL_ALIGNMENT: AutomatorLayoutPropertyDescriptor = { + description: "Vertical alignment.", + enum: ["flex-start", "center", "flex-end"], +}; + +export const AUTO_HEIGHT: AutomatorLayoutPropertyDescriptor = { + description: "Whether the component auto-sizes its height to its content.", + enum: ["auto", "fixed"], +}; + +export const HIDDEN: AutomatorLayoutPropertyDescriptor = { + description: "Hide the component at runtime.", + type: "boolean", +}; + +export const DISABLED: AutomatorLayoutPropertyDescriptor = { + description: "Disable the component at runtime.", + type: "boolean", +}; + +export const LOADING: AutomatorLayoutPropertyDescriptor = { + description: "Show a loading indicator on the component.", + type: "boolean", +}; + +export const LABEL_OBJECT: AutomatorLayoutPropertyDescriptor = { + description: + "Field label config: { text, position: 'row'|'column', align: 'left'|'center'|'right', width: number, hidden?: boolean, tooltip?: string }.", + type: "object", + example: { text: "Email", position: "row", align: "left" }, +}; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/types.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/types.ts new file mode 100644 index 0000000000..a30d120d4d --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/types.ts @@ -0,0 +1,44 @@ +/** + * Component instructions exposed to the Automator model. + * + * These are curated references, not a dump of every Lowcoder component. If a + * component appears here, Automator is allowed to place it and has enough shape + * information to configure it with reasonable confidence. + */ +export interface AutomatorLayoutPropertyDescriptor { + /** Human-readable hint shown to the model. */ + description?: string; + /** Allowed string values when this property is a fixed enum. */ + enum?: readonly string[]; + /** Primitive type when the value is not enum-restricted. */ + type?: "string" | "number" | "boolean" | "object"; + /** Sample value the model can imitate verbatim. */ + example?: unknown; +} + +/** + * Map of style namespace -> list of style keys that can be passed to + * `set_style`. The executor expects values grouped by explicit namespace. + */ +export type AutomatorStylePropertyMap = Record; + +export interface AutomatorComponentEntry { + /** Component type used by Lowcoder action executors. */ + type: string; + /** Whether the component can have children nested under `.container`. */ + isContainer?: boolean; + /** Default grid layout (w / h) for sensible initial sizing. */ + defaultLayout: { w: number; h: number }; + /** Required property keys for `action_parameters`. */ + required: string[]; + /** Optional property keys worth knowing about. */ + optional?: string[]; + /** Realistic example `action_parameters` payload. */ + example: Record; + /** Notes the model should heed. */ + notes?: string; + /** Top-level UI / behavior properties for `set_properties`. */ + layoutProperties?: Record; + /** Style properties grouped by style namespace, used by `set_style`. */ + styleProperties?: AutomatorStylePropertyMap; +} diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/editorSnapshot.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/editorSnapshot.ts index de7575f077..3d5638c676 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/editorSnapshot.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/editorSnapshot.ts @@ -1,9 +1,6 @@ // client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/editorSnapshot.ts import type { EditorState } from "@lowcoder-ee/comps/editorState"; -import { uiCompRegistry } from "comps/uiCompRegistry"; -import { LOWCODER_COMPONENT_TYPES } from "./componentCatalog"; - /** * A compact, JSON-serialisable view of the live editor state. * @@ -203,58 +200,3 @@ export function buildEditorSnapshot(editorState: EditorState | null | undefined) transformers, }; } - -/** - * Best-effort guess of what component types the user just mentioned in a - * free-form prompt. Used to slim down the component catalog we send to the - * model so it stays under token budgets. - * - * Returns an empty list if no obvious match — caller should keep the full - * component catalog in its default registry order. - */ -export function inferMentionedComponentTypes(prompt: string): string[] { - if (!prompt) return []; - const lower = prompt.toLowerCase(); - const registryTypes = Object.keys(uiCompRegistry); - const candidates = Array.from(new Set([...LOWCODER_COMPONENT_TYPES, ...registryTypes])); - const aliases: Record = { - chatbox: "chatBox", - "chat box": "chatBox", - "chat-box": "chatBox", - "chat controller": "chatController", - "chat-controller": "chatController", - "ai chat": "chat", - dropdown: "select", - "list view": "listView", - "list-view": "listView", - "number input": "numberInput", - "text area": "textArea", - textarea: "textArea", - textbox: "input", - "text field": "input", - img: "image", - pic: "image", - picture: "image", - tabs: "tabbedContainer", - "tab container": "tabbedContainer", - "tabbed container": "tabbedContainer", - graph: "chart", - nav: "navigation", - navbar: "navigation", - stepper: "step", - }; - const found = new Set(); - for (const c of candidates) { - const manifest = uiCompRegistry[c]; - const names = [ - c, - manifest?.name, - manifest?.enName, - ].filter(Boolean) as string[]; - if (names.some((name) => lower.includes(name.toLowerCase()))) found.add(c); - } - for (const [alias, real] of Object.entries(aliases)) { - if (lower.includes(alias)) found.add(real); - } - return Array.from(found); -} diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts index 76ebcb3900..3e4a1feec9 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts @@ -8,13 +8,15 @@ export { type ActionCatalogEntry, } from "./actionsCatalog"; export { - getComponentCatalog, - COMPONENT_TYPES_DEFAULT, - type ComponentCatalogEntry, -} from "./componentCatalog"; + AUTOMATOR_COMPONENTS, + AUTOMATOR_COMPONENT_TYPES, + getAutomatorComponents, + type AutomatorComponentEntry, + type AutomatorLayoutPropertyDescriptor, + type AutomatorStylePropertyMap, +} from "./automatorComponents"; export { buildEditorSnapshot, - inferMentionedComponentTypes, type EditorSnapshot, type ComponentSnapshot, type QuerySnapshot, diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts index 3f4f855119..6caf2933bd 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts @@ -2,12 +2,11 @@ import type { EditorState } from "@lowcoder-ee/comps/editorState"; import { ACTIONS_CATALOG } from "./actionsCatalog"; +import { buildEditorSnapshot, EditorSnapshot } from "./editorSnapshot"; import { - buildEditorSnapshot, - inferMentionedComponentTypes, - EditorSnapshot, -} from "./editorSnapshot"; -import { getComponentCatalog, ComponentCatalogEntry } from "./componentCatalog"; + getAutomatorComponents, + type AutomatorComponentEntry, +} from "./automatorComponents"; import { composeSystemMessage } from "./systemPrompt"; import { buildToolDefinitions, OpenAIToolDefinition } from "./toolDefinitions"; @@ -42,8 +41,8 @@ export interface OrchestratorOutput { context: EditorSnapshot; /** The actions catalog passed to the model. */ actionsCatalog: typeof ACTIONS_CATALOG; - /** The (optionally trimmed) component catalog passed to the model. */ - componentCatalog: ComponentCatalogEntry[]; + /** The curated Automator component instructions passed to the model. */ + automatorComponents: AutomatorComponentEntry[]; } /** @@ -55,21 +54,15 @@ export function buildAutomatorPayload(input: OrchestratorInput): OrchestratorOut const { history, editorState } = input; const context = buildEditorSnapshot(editorState); - - // Slim down the component catalog based on the *latest* user message so - // the requested components appear first. We still include the full - // Lowcoder registry so the Automator can add any component from the panel. - const lastUser = [...history].reverse().find((m) => m.role === "user"); - const mentioned = inferMentionedComponentTypes(lastUser?.content ?? ""); - const componentCatalog = getComponentCatalog(mentioned); + const automatorComponents = getAutomatorComponents(); const system = composeSystemMessage({ actionsCatalog: ACTIONS_CATALOG, - componentCatalog, + automatorComponents, editorContext: context, }); - const tools = buildToolDefinitions(componentCatalog.map((component) => component.type)); + const tools = buildToolDefinitions(automatorComponents.map((component) => component.type)); const messages: LLMMessage[] = [{ role: "system", content: system }]; for (const m of history) { @@ -82,6 +75,6 @@ export function buildAutomatorPayload(input: OrchestratorInput): OrchestratorOut system, context, actionsCatalog: ACTIONS_CATALOG, - componentCatalog, + automatorComponents, }; } diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts index a0d43685b4..a8eb303ff2 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts @@ -11,8 +11,7 @@ * * The orchestrator combines this prompt with: * - the actions catalog (what the model is allowed to emit) - * - the component catalog (all registered Lowcoder components, with - * curated examples for common ones) + * - Automator components (curated component instructions and examples) * - the live editor snapshot (existing components, queries, canvas grid) * * before sending it to the user-defined Lowcoder query that proxies the LLM. @@ -74,10 +73,10 @@ Use this context to: You will also see a JSON block titled "ACTIONS_CATALOG" listing the EXACT set of actions you may emit, with their required and optional fields. The -"COMPONENT_CATALOG" block lists every registered Lowcoder component type you +"AUTOMATOR_COMPONENTS" block lists the curated Lowcoder component types you may place or nest. You MUST NOT use any action or component type that is not -listed there. If something is not possible with the catalog, explain why in -plain text. +listed there. If something is not possible with the available Automator +components, explain why in plain text. # Layout rules (short) @@ -100,7 +99,7 @@ There are TWO families of UI edits, and each has its own action: direct children of the component. Use this for things controlled by the component's own controls (alignment, autoHeight, type, label, placeholder, options, disabled, hidden, loading, placement, …). For each component the - \`layoutProperties\` field in COMPONENT_CATALOG lists the exact keys and + \`layoutProperties\` field in AUTOMATOR_COMPONENTS lists the exact keys and their allowed values. 2. **\`set_style\`** — basic visual / CSS-like properties living inside the @@ -117,7 +116,7 @@ There are TWO families of UI edits, and each has its own action: Incorrect: \`{ "background": "#1677ff", "text": "#ffffff" }\` - For each component the \`styleProperties\` field in COMPONENT_CATALOG + For each component the \`styleProperties\` field in AUTOMATOR_COMPONENTS lists which keys live in which namespace. Common style-key vocabulary: @@ -164,13 +163,14 @@ There are TWO families of UI edits, and each has its own action: # Reminders -- All field names match the catalog exactly (snake_case where shown). +- All field names match the Automator component instructions exactly + (snake_case where shown). - Every action MUST include \`action\` and (when relevant) \`component\` and \`component_name\`. - Component names must be unique across the app. If reusing an existing component referenced in EDITOR_CONTEXT, use its existing name. - Prefer the per-component \`layoutProperties\` / \`styleProperties\` listed in - COMPONENT_CATALOG over invented keys. If a property is not listed and you + AUTOMATOR_COMPONENTS over invented keys. If a property is not listed and you are unsure it exists, ask the user instead of guessing. `.trim(); @@ -183,10 +183,10 @@ There are TWO families of UI edits, and each has its own action: */ export function composeSystemMessage(args: { actionsCatalog: unknown; - componentCatalog: unknown; + automatorComponents: unknown; editorContext: unknown; }): string { - const { actionsCatalog, componentCatalog, editorContext } = args; + const { actionsCatalog, automatorComponents, editorContext } = args; return [ AUTOMATOR_SYSTEM_PROMPT, @@ -196,9 +196,9 @@ export function composeSystemMessage(args: { JSON.stringify(actionsCatalog, null, 2), "```", "", - "COMPONENT_CATALOG:", + "AUTOMATOR_COMPONENTS:", "```json", - JSON.stringify(componentCatalog, null, 2), + JSON.stringify(automatorComponents, null, 2), "```", "", "EDITOR_CONTEXT:", diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts index b1d219031f..1d089266a5 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts @@ -66,7 +66,7 @@ function buildActionItemSchema(componentTypes?: string[]): Record Date: Fri, 15 May 2026 20:42:46 +0500 Subject: [PATCH 10/33] fix automator for modules + add delete query functionality --- .../components/ChatPanelContainer.tsx | 2 + .../actions/automator/actionsCatalog.ts | 11 +++++ .../actions/automator/systemPrompt.ts | 7 +-- .../actions/automator/toolDefinitions.ts | 6 ++- .../actions/componentManagement.ts | 6 +-- .../preLoadComp/actions/queryManagement.ts | 49 +++++++++++++++++++ .../comps/comps/preLoadComp/components.tsx | 6 +-- 7 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/comps/preLoadComp/actions/queryManagement.ts 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 f4688cf0d7..123a8dc3d8 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx @@ -47,6 +47,7 @@ import { import { applyStyleAction } from "../../preLoadComp/actions/componentStyling"; import { addEventHandlerAction } from "../../preLoadComp/actions/componentEvents"; import { alignComponentAction } from "../../preLoadComp/actions/componentLayout"; +import { deleteQueryAction } from "../../preLoadComp/actions/queryManagement"; // ============================================================================ // ACTION REGISTRY — maps LLM action names to their executor configs. @@ -59,6 +60,7 @@ const ACTION_REGISTRY: Record = { move_component: moveComponentAction, resize_component: resizeComponentAction, delete_component: deleteComponentAction, + delete_query: deleteQueryAction, rename_component: renameComponentAction, set_properties: configureComponentAction, set_style: applyStyleAction, diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts index 4f7fbee779..4c813a4a03 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts @@ -17,6 +17,7 @@ export type AutomatorActionName = | "move_component" | "resize_component" | "delete_component" + | "delete_query" | "rename_component" | "set_properties" | "set_style" @@ -97,6 +98,16 @@ export const ACTIONS_CATALOG: ActionCatalogEntry[] = [ component_name: "oldButton", }, }, + { + action: "delete_query", + purpose: + "Delete an existing bottom-panel data query by name. This action only needs the query name and does not read the query configuration or body.", + required: ["query_name"], + example: { + action: "delete_query", + query_name: "getUsers", + }, + }, { action: "rename_component", purpose: "Rename an existing component.", diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts index a8eb303ff2..46e2884b92 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts @@ -45,9 +45,9 @@ component inside a container and there is no action for reparenting it, explain that limitation instead of creating a duplicate component. You have a tool called \`execute_automator_actions\`. Use it when you are -ready to modify the canvas. When the request is ambiguous or you need -clarification, respond with plain text instead — do NOT call the tool with -an empty actions array. +ready to modify the canvas or supported bottom-panel resources. When the +request is ambiguous or you need clarification, respond with plain text +instead — do NOT call the tool with an empty actions array. If the user explicitly says "go ahead", "do it", "build it", "implement", or similar approval after a clarification round, call the tool. @@ -67,6 +67,7 @@ Use this context to: - reuse component names that already exist - place new components without overlapping existing ones - reference existing queries instead of creating duplicates + - delete an existing query by name with \`delete_query\` when explicitly asked - generate unique, descriptive component names # How to use the action catalog diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts index 1d089266a5..176360a65e 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts @@ -48,6 +48,10 @@ function buildActionItemSchema(componentTypes?: string[]): Record { + const { editorState } = params; + const queryName = getQueryName(params); + + if (!editorState) { + message.error("Editor state is required"); + return; + } + + if (!queryName) { + message.error("Query name is required"); + return; + } + + const queriesComp = editorState.getQueriesComp?.(); + const queryExists = queriesComp + ?.getView?.() + ?.some((query: any) => query?.children?.name?.getView?.() === queryName); + + if (!queryExists) { + message.error(`Query "${queryName}" not found`); + return; + } + + queriesComp.delete(queryName); + }, +}; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/components.tsx b/client/packages/lowcoder/src/comps/comps/preLoadComp/components.tsx index 28f4629d95..1e0904b79e 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/components.tsx +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/components.tsx @@ -17,6 +17,7 @@ import log from "loglevel"; import { JSLibraryTree } from "components/JSLibraryTree"; import { fetchJSLibrary } from "util/jsLibraryUtils"; import { RunAndClearable } from "./types"; +import { runScript } from "./utils"; export class LibsCompBase extends list(SimpleStringControl) implements RunAndClearable { success: Record = {}; @@ -118,15 +119,12 @@ export class ScriptComp extends CodeTextControl implements RunAndClearable Date: Sat, 16 May 2026 01:36:40 +0500 Subject: [PATCH 11/33] add card component improvment + add delay in the action --- .../components/ChatPanelContainer.tsx | 2 +- .../automator/automatorComponents/entries.ts | 41 ++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) 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 123a8dc3d8..55358acce9 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx @@ -207,7 +207,7 @@ function ChatPanelView({ messageHandler, placeholder, onMessageUpdate }: Omit setTimeout(r, 200)); + await new Promise((r) => setTimeout(r, 500)); } console.log(`[Automator] done: ${executed}/${actions.length} succeeded`); diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/entries.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/entries.ts index 03d2b66d06..1aa61d12d0 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/entries.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/entries.ts @@ -628,14 +628,46 @@ const CARD: AutomatorComponentEntry = { isContainer: true, defaultLayout: { w: 8, h: 15 }, required: ["title"], - optional: ["size", "showTitle", "hoverable", "bordered", "hidden"], - example: { title: "Card Title" }, - notes: "Nest content inside '.container'.", + optional: [ + "size", + "showTitle", + "extraTitle", + "cardType", + "CoverImg", + "imgSrc", + "imgHeight", + "showMeta", + "metaTitle", + "metaDesc", + "hoverable", + "showActionIcon", + "hidden", + ], + example: { + title: "Card Title", + cardType: "common", + CoverImg: true, + imgSrc: "https://images.unsplash.com/photo-1518770660439-4636190af475", + imgHeight: "180px", + showMeta: true, + metaTitle: "Project name", + metaDesc: "Short project description", + }, + notes: + "Nest content inside '.container'. For the built-in card cover image, use `set_properties` on the card with `imgSrc` (not `src`) and set `CoverImg: true`; `src` is only for standalone Image components.", layoutProperties: { size: { description: "Card density.", enum: ["default", "small"] }, showTitle: { description: "Render the title bar.", type: "boolean" }, + extraTitle: { description: "Text rendered in the card header extra link.", type: "string" }, + cardType: { description: "Card mode. Built-in cover/meta fields require common.", enum: ["common", "custom"] }, + CoverImg: { description: "Show the built-in cover image.", type: "boolean" }, + imgSrc: { description: "Built-in card cover image URL. Use this for card cover image edits.", type: "string" }, + imgHeight: { description: 'Built-in card cover image height, e.g. "180px" or "auto".', type: "string" }, + showMeta: { description: "Show built-in card meta title and description.", type: "boolean" }, + metaTitle: { description: "Built-in card meta title.", type: "string" }, + metaDesc: { description: "Built-in card meta description.", type: "string" }, hoverable: { description: "Lift on hover.", type: "boolean" }, - bordered: { description: "Show outer border.", type: "boolean" }, + showActionIcon: { description: "Show built-in card action icons.", type: "boolean" }, hidden: HIDDEN, }, styleProperties: { @@ -955,4 +987,3 @@ export const AUTOMATOR_COMPONENTS: AutomatorComponentEntry[] = [ CHAT_BOX, CHAT_CONTROLLER, ]; - From 57f7c54bc15e110693804bb1be5ec79e8475b1f4 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 19 May 2026 21:32:22 +0500 Subject: [PATCH 12/33] fix: threadview height for ai chat --- .../comps/comps/chatComp/components/ChatContainerStyles.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 a16e67954c..ccc9f20638 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainerStyles.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainerStyles.ts @@ -18,6 +18,7 @@ export interface StyledChatContainerProps { export const StyledChatContainer = styled.div` display: flex; + align-items: stretch; height: ${(props) => (props.$autoHeight ? "auto" : "100%")}; min-height: ${(props) => (props.$autoHeight ? "300px" : "unset")}; min-width: 0; @@ -42,6 +43,7 @@ export const StyledChatContainer = styled.div` /* Sidebar Styles */ .aui-thread-list-root { + align-self: stretch; width: ${(props) => props.$sidebarWidth || "250px"}; background-color: ${(props) => props.$sidebarStyle?.sidebarBackground || "#fff"}; padding: 10px; @@ -56,14 +58,16 @@ export const StyledChatContainer = styled.div` /* Messages Window Styles */ .aui-thread-root { flex: 1 1 auto; + align-self: stretch; min-width: 0; min-height: 0; background-color: ${(props) => props.$messagesStyle?.messagesBackground || "#f9fafb"}; - height: 100%; + height: ${(props) => (props.$autoHeight ? "auto" : "100%")}; overflow: hidden; } .aui-thread-viewport { + flex: 1 1 auto; min-height: 0; } From 6fbcedfc425aeaeee86a59b5dcc3e686e0a412df Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 20 May 2026 18:57:20 +0500 Subject: [PATCH 13/33] add new AUI components template --- .../components/assistant-ui/markdown-text.tsx | 261 +++++++++- .../components/assistant-ui/reasoning.tsx | 284 +++++++++++ .../components/assistant-ui/thread-list.tsx | 116 +++++ .../components/assistant-ui/thread.tsx | 460 +++++++++++++++++- .../components/assistant-ui/tool-fallback.tsx | 326 +++++++++++++ .../components/assistant-ui/tool-group.tsx | 231 +++++++++ .../assistant-ui/tooltip-icon-button.tsx | 56 ++- .../chatComp/components/ui/attachment.tsx | 233 ++++++++- 8 files changed, 1963 insertions(+), 4 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/comps/chatComp/components/assistant-ui/reasoning.tsx create mode 100644 client/packages/lowcoder/src/comps/comps/chatComp/components/assistant-ui/tool-fallback.tsx create mode 100644 client/packages/lowcoder/src/comps/comps/chatComp/components/assistant-ui/tool-group.tsx diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/assistant-ui/markdown-text.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/assistant-ui/markdown-text.tsx index bbf2e5648a..b2ec2f035b 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/assistant-ui/markdown-text.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/assistant-ui/markdown-text.tsx @@ -127,4 +127,263 @@ const defaultComponents = memoizeMarkdownComponents({ ); }, CodeHeader, -}); \ No newline at end of file +}); + + + + +// ================ AUI NEW AUI COMPONENTS ================ + + + +// "use client"; + +// import "@assistant-ui/react-markdown/styles/dot.css"; + +// import { +// type CodeHeaderProps, +// MarkdownTextPrimitive, +// unstable_memoizeMarkdownComponents as memoizeMarkdownComponents, +// useIsMarkdownCodeBlock, +// } from "@assistant-ui/react-markdown"; +// import remarkGfm from "remark-gfm"; +// import { type FC, memo, useState } from "react"; +// import { CheckIcon, CopyIcon } from "lucide-react"; + +// import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; +// import { cn } from "@/lib/utils"; + +// const MarkdownTextImpl = () => { +// return ( +// +// ); +// }; + +// export const MarkdownText = memo(MarkdownTextImpl); + +// const CodeHeader: FC = ({ language, code }) => { +// const { isCopied, copyToClipboard } = useCopyToClipboard(); +// const onCopy = () => { +// if (!code || isCopied) return; +// copyToClipboard(code); +// }; + +// return ( +//
+// +// {language} +// +// +// {!isCopied && } +// {isCopied && } +// +//
+// ); +// }; + +// const useCopyToClipboard = ({ +// copiedDuration = 3000, +// }: { +// copiedDuration?: number; +// } = {}) => { +// const [isCopied, setIsCopied] = useState(false); + +// const copyToClipboard = (value: string) => { +// if (!value || typeof navigator === "undefined" || !navigator.clipboard) { +// return; +// } + +// navigator.clipboard.writeText(value).then( +// () => { +// setIsCopied(true); +// setTimeout(() => setIsCopied(false), copiedDuration); +// }, +// () => {}, +// ); +// }; + +// return { isCopied, copyToClipboard }; +// }; + +// const defaultComponents = memoizeMarkdownComponents({ +// h1: ({ className, ...props }) => ( +//

+// ), +// h2: ({ className, ...props }) => ( +//

+// ), +// h3: ({ className, ...props }) => ( +//

+// ), +// h4: ({ className, ...props }) => ( +//

+// ), +// h5: ({ className, ...props }) => ( +//

+// ), +// h6: ({ className, ...props }) => ( +//
+// ), +// p: ({ className, ...props }) => ( +//

+// ), +// a: ({ className, ...props }) => ( +// +// ), +// blockquote: ({ className, ...props }) => ( +//

+// ), +// ul: ({ className, ...props }) => ( +//