diff --git a/client/packages/lowcoder-design/src/components/ExternalLink.tsx b/client/packages/lowcoder-design/src/components/ExternalLink.tsx index 51fcb3c8ed..f22da58a78 100644 --- a/client/packages/lowcoder-design/src/components/ExternalLink.tsx +++ b/client/packages/lowcoder-design/src/components/ExternalLink.tsx @@ -1,9 +1,11 @@ import { ActiveTextColor, GreyTextColor } from "constants/style"; import { DocIcon } from "icons"; import styled from "styled-components"; +import { ToolTipLabel } from "./toolTip"; export const ExternalLink = styled.a` font-size: 13px; + font-weight: 400; line-height: 13px; color: ${GreyTextColor}; display: inline-flex; @@ -20,14 +22,27 @@ const StyledDocIcon = styled(DocIcon)` margin-right: 4px; `; -export function DocLink(props: React.AnchorHTMLAttributes) { +type DocLinkProps = React.AnchorHTMLAttributes & { + tooltipZIndex?: number; +}; + +export function DocLink(props: DocLinkProps) { if (!props.href) { return <>; } - return ( - + const { title, children, rel, tooltipZIndex, ...rest } = props; + const link = ( + - {props.children} + {children} ); + if (!title) { + return link; + } + return ( + + {link} + + ); } diff --git a/client/packages/lowcoder/src/components/ai-helper/AIHelperModal.tsx b/client/packages/lowcoder/src/components/ai-helper/AIHelperModal.tsx index 64bbee42f9..b44374a65d 100644 --- a/client/packages/lowcoder/src/components/ai-helper/AIHelperModal.tsx +++ b/client/packages/lowcoder/src/components/ai-helper/AIHelperModal.tsx @@ -7,7 +7,9 @@ import { SparklesIcon, XIcon } from "lucide-react"; import { useSelector } from "react-redux"; import styled from "styled-components"; +import { DocLink } from "lowcoder-design"; import { EditorContext } from "comps/editorState"; +import { trans } from "i18n"; import { getDataSourceStructures } from "redux/selectors/datasourceSelectors"; import { getSelectedAIQueryName } from "util/localStorageUtil"; @@ -175,6 +177,13 @@ export function AIHelperModal() { AI Helper + + {trans("comp.menuViewDocs")} + {target?.label && ( {target.label} diff --git a/client/packages/lowcoder/src/components/assistant-ui/thread-welcome.tsx b/client/packages/lowcoder/src/components/assistant-ui/thread-welcome.tsx index 538edc48c2..c9aa412129 100644 --- a/client/packages/lowcoder/src/components/assistant-ui/thread-welcome.tsx +++ b/client/packages/lowcoder/src/components/assistant-ui/thread-welcome.tsx @@ -1,8 +1,39 @@ import { ThreadPrimitive } from "@assistant-ui/react"; -import type { FC } from "react"; +import { useState, type FC } from "react"; import { trans } from "i18n"; -export const ThreadWelcome: FC = () => { +const DEMO_SUGGESTION_KEYS = [ + "chat.suggestionTimeTracking", + "chat.suggestionCrm", + "chat.suggestionTodo", + "chat.suggestionInventory", + "chat.suggestionExpenseTracker", + "chat.suggestionProjectManagement", + "chat.suggestionCustomerSupport", + "chat.suggestionEmployeeDirectory", +] as const; + +const CHAT_SUGGESTION_KEYS = [ + "chat.suggestionChatCapabilities", + "chat.suggestionChatIdeas", +] as const; + +const pickDemoSuggestions = () => { + const firstIndex = Math.floor(Math.random() * DEMO_SUGGESTION_KEYS.length); + let secondIndex = Math.floor(Math.random() * (DEMO_SUGGESTION_KEYS.length - 1)); + + if (secondIndex >= firstIndex) { + secondIndex += 1; + } + + return [DEMO_SUGGESTION_KEYS[firstIndex], DEMO_SUGGESTION_KEYS[secondIndex]]; +}; + +interface ThreadWelcomeProps { + suggestionMode: "chat" | "automator"; +} + +export const ThreadWelcome: FC = ({ suggestionMode }) => { return (
@@ -12,38 +43,34 @@ export const ThreadWelcome: FC = () => {
- + ); }; -const ThreadSuggestions: FC = () => { +const ThreadSuggestions: FC = ({ suggestionMode }) => { + const [suggestionKeys] = useState(() => + suggestionMode === "automator" ? pickDemoSuggestions() : CHAT_SUGGESTION_KEYS + ); + return (
-
- - - {trans("chat.suggestionWeather")} - - -
-
- - - {trans("chat.suggestionAssistant")} - - -
+ {suggestionKeys.map((suggestionKey) => { + const suggestion = trans(suggestionKey); + + return ( +
+ + {suggestion} + +
+ ); + })}
); }; diff --git a/client/packages/lowcoder/src/components/assistant-ui/thread.tsx b/client/packages/lowcoder/src/components/assistant-ui/thread.tsx index 69032704ef..74fcffae1f 100644 --- a/client/packages/lowcoder/src/components/assistant-ui/thread.tsx +++ b/client/packages/lowcoder/src/components/assistant-ui/thread.tsx @@ -13,12 +13,14 @@ interface ThreadProps { placeholder?: string; showAttachments?: boolean; autoHeight?: boolean; + suggestionMode?: "chat" | "automator"; } export const Thread: FC = ({ placeholder = trans("chat.composerPlaceholder"), showAttachments = true, autoHeight = false, + suggestionMode = "chat", }) => { return ( = ({ >
s.thread.isEmpty}> - +
diff --git a/client/packages/lowcoder/src/comps/comps/chatBoxComponent/chatBoxComp.tsx b/client/packages/lowcoder/src/comps/comps/chatBoxComponent/chatBoxComp.tsx index 9c293f1e37..af256c502f 100644 --- a/client/packages/lowcoder/src/comps/comps/chatBoxComponent/chatBoxComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatBoxComponent/chatBoxComp.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import { Section, sectionNames, controlItem } from "lowcoder-design"; +import { PropertyViewDocLink } from "comps/utils/propertyViewDocLink"; import { default as Segmented } from "antd/es/segmented"; import { UICompBuilder, withDefault, stateComp } from "../../generators"; import { changeValueAction, multiChangeAction } from "lowcoder-core"; @@ -193,6 +194,7 @@ const ChatBoxPropertyView = React.memo((props: { children: any }) => { return ( <> +
{children.chatTitle.propertyView({ label: trans("chatBox.chatTitleLabel"), diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx index a32a3ea6b2..84a5c0a70d 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx @@ -13,11 +13,11 @@ import { ChatContainer } from "./components/ChatContainer"; import { ChatProvider } from "./components/context/ChatContext"; import { ChatPropertyView } from "./chatPropertyView"; import { createChatStorage } from "./utils/storageFactory"; -import { QueryHandler } from "./handlers/messageHandlers"; -import { useMemo, useRef } from "react"; -import { changeChildAction } from "lowcoder-core"; +import { QueryHandler } from "./handlers/messageHandlers"; +import { useMemo, useRef } from "react"; +import { changeChildAction } from "lowcoder-core"; import { ChatMessage } from "./types/chatTypes"; -import { getTextFromThreadContent } from "./utils/assistantMessages"; +import { addSystemPromptToHistory } from "./utils/assistantMessages"; import { trans } from "i18n"; import { TooltipProvider } from "@radix-ui/react-tooltip"; import { styleControl } from "comps/controls/styleControl"; @@ -86,59 +86,7 @@ const ChatEventOptions = [ export const ChatEventHandlerControl = eventHandlerControl(ChatEventOptions); -// ============================================================================ -// SIMPLIFIED CHILDREN MAP - WITH EVENT HANDLERS -// ============================================================================ - - -export function addSystemPromptToHistory( - conversationHistory: ChatMessage[], - systemPrompt: string -): Array<{ role: string; content: string; timestamp: number; attachments?: any[] }> { - // Format conversation history for use in queries - const formattedHistory = conversationHistory.map(msg => { - const baseMessage = { - role: msg.role, - content: getTextFromThreadContent(msg.content), - timestamp: msg.createdAt.getTime() - }; - - // Include attachment metadata if present (for API calls and external integrations) - if (msg.attachments && msg.attachments.length > 0) { - return { - ...baseMessage, - attachments: msg.attachments.map(att => ({ - id: att.id, - type: att.type, - name: att.name, - contentType: att.contentType, - // Include content for images (base64 data URLs are useful for APIs) - ...(att.type === "image" && att.content && { - content: att.content.map(c => ({ - type: c.type, - ...(c.type === "image" && { image: c.image }) - })) - }) - })) - }; - } - - return baseMessage; - }); - - // Create system message (always exists since we have default) - const systemMessage = [{ - role: "system" as const, - content: systemPrompt, - timestamp: Date.now() - 1000000 // Ensure it's always first chronologically - }]; - - // Return complete history with system prompt prepended - return [...systemMessage, ...formattedHistory]; -} - - -function generateUniqueTableName(): string { +function generateUniqueTableName(): string { return `chat${Math.floor(1000 + Math.random() * 9000)}`; } @@ -213,14 +161,16 @@ const ChatTmpComp = new UICompBuilder( // Create message handler (Query only) const messageHandler = useMemo(() => { - return new QueryHandler({ - chatQuery: props.chatQuery.value, - dispatch, - }); - }, [ - props.chatQuery, - dispatch, - ]); + return new QueryHandler({ + chatQuery: props.chatQuery.value, + dispatch, + systemPrompt: props.systemPrompt, + }); + }, [ + props.chatQuery, + props.systemPrompt, + dispatch, + ]); // Handle message updates for exposed variable // Using Lowcoder pattern: props.currentMessage.onChange() @@ -297,6 +247,6 @@ const ChatCompWithAutoHeight = class extends ChatTmpComp { export const ChatComp = withExposingConfigs(ChatCompWithAutoHeight, [ new NameConfig("currentMessage", "Current user message"), // conversationHistory is now a proper array (not JSON string) - supports setConversationHistory(), clearConversationHistory(), resetConversationHistory() - new NameConfig("conversationHistory", "Full conversation history array with system prompt (use directly in API calls, no JSON.parse needed)"), + new NameConfig("conversationHistory", "Reactive conversation history array with system prompt (no JSON.parse needed)"), new NameConfig("databaseName", "Database name for SQL queries (ChatDB_)"), ]); diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx index b12aafd41d..d6d44f1e67 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx @@ -1,10 +1,9 @@ // client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx import React, { useMemo } from "react"; -import { Section, sectionNames, DocLink } from "lowcoder-design"; +import { Section, sectionNames, controlItem } from "lowcoder-design"; import { trans } from "i18n"; -import { hiddenPropertyView } from "comps/utils/propertyUtils"; -import { controlItem } from "lowcoder-design"; +import { PropertyViewDocLink } from "comps/utils/propertyViewDocLink"; // ============================================================================ // PROPERTY VIEW @@ -15,16 +14,7 @@ export const ChatPropertyView = React.memo((props: any) => { return useMemo(() => ( <> - {/* Help & Documentation - Outside of Section */} -
- - πŸ“– View Documentation - -
+ {/* Message Handler Configuration */}
diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx index c483e4f6c1..964b606de5 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx @@ -49,10 +49,8 @@ function ChatContainerView(props: ChatCoreProps) { const currentMessages = actions.getCurrentMessages(); useEffect(() => { - if (currentMessages.length > 0) { - onConversationUpdateRef.current?.(currentMessages); - } - }, [currentMessages]); + onConversationUpdateRef.current?.(currentMessages); + }, [state.currentThreadId, currentMessages]); useEffect(() => { onEventRef.current?.("componentLoad"); @@ -82,13 +80,17 @@ function ChatContainerView(props: ChatCoreProps) { } const userMessage = createUserMessage(text, completeAttachments); + const conversationHistory = [...currentMessages, userMessage]; await actions.addMessage(state.currentThreadId, userMessage); await updateInitialThreadTitle(userMessage); setIsRunning(true); try { - const assistantMessage = await props.messageHandler.sendMessage(userMessage); + const assistantMessage = await props.messageHandler.sendMessage( + userMessage, + conversationHistory + ); props.onMessageUpdate?.(getTextFromThreadContent(userMessage.content)); await actions.addMessage(state.currentThreadId, assistantMessage); @@ -122,7 +124,10 @@ function ChatContainerView(props: ChatCoreProps) { setIsRunning(true); try { - const assistantMessage = await props.messageHandler.sendMessage(editedMessage); + const assistantMessage = await props.messageHandler.sendMessage( + editedMessage, + newMessages + ); props.onMessageUpdate?.(getTextFromThreadContent(editedMessage.content)); newMessages.push(assistantMessage); 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 671b4de283..f200d53351 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx @@ -359,7 +359,11 @@ function ChatPanelView({ messageHandler, placeholder, onMessageUpdate }: Omit - + ); diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/context/ChatContext.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/context/ChatContext.tsx index e733727f38..e1dc2fdd85 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/context/ChatContext.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/context/ChatContext.tsx @@ -2,7 +2,9 @@ import React, { createContext, useContext, useReducer, useEffect, ReactNode } from "react"; import { ChatStorage, ChatMessage, ChatThread } from "../../types/chatTypes"; -import { trans } from "i18n"; +import { trans } from "i18n"; + +const EMPTY_MESSAGES: ChatMessage[] = []; // ============================================================================ // UPDATED CONTEXT WITH CLEAN TYPES @@ -353,10 +355,10 @@ export function ChatProvider({ children, storage }: { } }; - // Utility functions - const getCurrentMessages = (): ChatMessage[] => { - return state.threads.get(state.currentThreadId) || []; - }; + // Utility functions + const getCurrentMessages = (): ChatMessage[] => { + return state.threads.get(state.currentThreadId) || EMPTY_MESSAGES; + }; // Auto-initialize on mount useEffect(() => { @@ -395,4 +397,4 @@ export function useChatContext() { } // Re-export types for convenience -export type { ChatMessage, ChatThread }; \ No newline at end of file +export type { ChatMessage, ChatThread }; 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 424666669a..8c409b571b 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/handlers/messageHandlers.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/handlers/messageHandlers.ts @@ -1,10 +1,11 @@ -// client/packages/lowcoder/src/comps/comps/chatComp/handlers/messageHandlers.ts - +// client/packages/lowcoder/src/comps/comps/chatComp/handlers/messageHandlers.ts + import { AIAssistantMessageHandler, MessageHandler, QueryHandlerConfig, ChatMessage } from "../types/chatTypes"; import { routeByNameAction, executeQueryAction } from "lowcoder-core"; import { getPromiseAfterDispatch } from "util/promiseUtils"; import { buildAutomatorPayload } from "../../preLoadComp/actions/automator"; import { + buildChatQueryArgs, getTextFromThreadContent, toAssistantMessage, } from "../utils/assistantMessages"; @@ -24,16 +25,19 @@ function buildAutomatorQueryArgs( }, }; } - -// ============================================================================ -// QUERY HANDLER -// ============================================================================ - -export class QueryHandler implements MessageHandler { - constructor(private config: QueryHandlerConfig) {} - - async sendMessage(message: ChatMessage): Promise { - const { chatQuery, dispatch} = this.config; + +// ============================================================================ +// QUERY HANDLER +// ============================================================================ + +export class QueryHandler implements MessageHandler { + constructor(private config: QueryHandlerConfig) {} + + async sendMessage( + message: ChatMessage, + conversationHistory: ChatMessage[] + ): Promise { + const { chatQuery, dispatch, systemPrompt = "" } = this.config; if (!chatQuery) { throw new Error("Select a query before sending a message"); @@ -42,33 +46,33 @@ export class QueryHandler implements MessageHandler { if (!dispatch) { throw new Error("Query dispatch is unavailable"); } - - try { - console.log("Executing query:", chatQuery); - const result: any = await getPromiseAfterDispatch( - dispatch, - routeByNameAction( - chatQuery, - executeQueryAction({ - // Pass the full message object so attachments are available in queries - args: { - message: { value: message }, - prompt: { value: getTextFromThreadContent(message.content) }, - }, - }) - ) - ); - console.log("Query result:", result); + + try { + console.log("Executing query:", chatQuery); + const result: any = await getPromiseAfterDispatch( + dispatch, + routeByNameAction( + chatQuery, + executeQueryAction({ + args: buildChatQueryArgs( + message, + conversationHistory, + systemPrompt + ), + }) + ) + ); + console.log("Query result:", result); return toAssistantMessage(result); - } catch (e: any) { - throw new Error(e?.message || "Query execution failed"); - } - } -} - -// ============================================================================ -// AI ASSISTANT QUERY HANDLER (bottom panel) -// ---------------------------------------------------------------------------- + } catch (e: any) { + throw new Error(e?.message || "Query execution failed"); + } + } +} + +// ============================================================================ +// AI ASSISTANT QUERY HANDLER (bottom panel) +// ---------------------------------------------------------------------------- // This handler owns the Lowcoder side of the Automator flow: // 1. snapshot the current editor state, // 2. build the system prompt, tools, catalogs, and live context, @@ -77,8 +81,8 @@ export class QueryHandler implements MessageHandler { // // Provider-specific parsing belongs in the selected query/backend bridge. // ============================================================================ - -export class AIAssistantQueryHandler implements AIAssistantMessageHandler { + +export class AIAssistantQueryHandler implements AIAssistantMessageHandler { constructor(private config: QueryHandlerConfig) {} async sendMessage( @@ -94,7 +98,7 @@ export class AIAssistantQueryHandler implements AIAssistantMessageHandler { role: msg.role, content: getTextFromThreadContent(msg.content), })); - + if (!chatQuery) { throw new Error("Select an Automator query before sending a message"); } @@ -116,12 +120,12 @@ export class AIAssistantQueryHandler implements AIAssistantMessageHandler { 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, + contextQueries: payload.context.queries.length, + messageCount: payload.messages.length, + }); + + const result: any = await getPromiseAfterDispatch( + dispatch, routeByNameAction( chatQuery, executeQueryAction({ @@ -134,9 +138,9 @@ export class AIAssistantQueryHandler implements AIAssistantMessageHandler { } catch (e: any) { throw new Error(e?.message || "AI assistant query execution failed"); } - } -} - + } +} + // ============================================================================ // HANDLER FACTORY (creates the right handler based on type) // ============================================================================ @@ -147,7 +151,7 @@ export function createMessageHandler( ): MessageHandler { switch (type) { case "query": - return new QueryHandler(config); + 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 0a8035a30c..47f3ebeb55 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts @@ -42,10 +42,13 @@ export type ChatMessage = Omit< // ============================================================================ // MESSAGE HANDLER INTERFACE (new clean abstraction) - // ============================================================================ - + // ============================================================================ + export interface MessageHandler { - sendMessage(message: ChatMessage, sessionId?: string): Promise; + sendMessage( + message: ChatMessage, + conversationHistory: ChatMessage[] + ): Promise; // Future: sendMessageStream?(message: ChatMessage): AsyncGenerator; } @@ -57,9 +60,10 @@ export type ChatMessage = Omit< // CONFIGURATION TYPES (simplified) // ============================================================================ - export interface QueryHandlerConfig { - chatQuery: string; - dispatch: any; + export interface QueryHandlerConfig { + chatQuery: string; + dispatch: any; + systemPrompt?: string; /** * Snapshot accessor for the live editor state. The handler calls this * lazily on every send so it always has the *current* canvas state. diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/utils/assistantMessages.test.ts b/client/packages/lowcoder/src/comps/comps/chatComp/utils/assistantMessages.test.ts new file mode 100644 index 0000000000..d63d6ff4c4 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/chatComp/utils/assistantMessages.test.ts @@ -0,0 +1,76 @@ +import type { ChatMessage } from "../types/chatTypes"; +import { + addSystemPromptToHistory, + buildChatQueryArgs, +} from "./assistantMessages"; + +const imageDataUrl = "data:image/png;base64,aW1hZ2U="; + +const userMessage = { + id: "user-1", + role: "user", + content: [{ type: "text", text: "Explain this image" }], + createdAt: new Date("2026-09-08T00:00:00.000Z"), + attachments: [ + { + id: "image-1", + type: "image", + name: "example.png", + contentType: "image/png", + content: [{ type: "image", image: imageDataUrl }], + status: { type: "complete" }, + }, + ], +} as ChatMessage; + +describe("AI Chat query context", () => { + test("serializes the system prompt, text, and image attachment", () => { + const history = addSystemPromptToHistory( + [userMessage], + "Describe images accurately", + ); + + expect(history).toEqual([ + expect.objectContaining({ + role: "system", + content: "Describe images accurately", + }), + { + role: "user", + content: "Explain this image", + timestamp: userMessage.createdAt.getTime(), + attachments: [ + { + id: "image-1", + type: "image", + name: "example.png", + contentType: "image/png", + content: [{ type: "image", image: imageDataUrl }], + }, + ], + }, + ]); + }); + + test("passes the same current message in the prompt and history arguments", () => { + const args = buildChatQueryArgs( + userMessage, + [userMessage], + "Describe images accurately", + ); + + expect(args.prompt.value).toBe("Explain this image"); + expect(args.message.value).toBe(userMessage); + const lastMessage = + args.conversationHistory.value[args.conversationHistory.value.length - 1]; + expect(lastMessage).toEqual( + expect.objectContaining({ + role: "user", + content: "Explain this image", + attachments: expect.arrayContaining([ + expect.objectContaining({ id: "image-1" }), + ]), + }), + ); + }); +}); diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/utils/assistantMessages.ts b/client/packages/lowcoder/src/comps/comps/chatComp/utils/assistantMessages.ts index 63f5271b00..5c50a50489 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/utils/assistantMessages.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/utils/assistantMessages.ts @@ -25,6 +25,61 @@ export const getTextFromThreadContent = ( .trim(); }; +export const addSystemPromptToHistory = ( + conversationHistory: ChatMessage[], + systemPrompt: string +) => { + const messages = conversationHistory.map((message) => { + const baseMessage = { + role: message.role, + content: getTextFromThreadContent(message.content), + timestamp: message.createdAt.getTime(), + }; + + if (!message.attachments?.length) { + return baseMessage; + } + + return { + ...baseMessage, + attachments: message.attachments.map((attachment) => ({ + id: attachment.id, + type: attachment.type, + name: attachment.name, + contentType: attachment.contentType, + ...(attachment.type === "image" && + attachment.content && { + content: attachment.content.map((part) => ({ + type: part.type, + ...(part.type === "image" && { image: part.image }), + })), + }), + })), + }; + }); + + return [ + { + role: "system" as const, + content: systemPrompt, + timestamp: Date.now() - 1_000_000, + }, + ...messages, + ]; +}; + +export const buildChatQueryArgs = ( + message: ChatMessage, + conversationHistory: ChatMessage[], + systemPrompt: string +) => ({ + message: { value: message }, + prompt: { value: getTextFromThreadContent(message.content) }, + conversationHistory: { + value: addSystemPromptToHistory(conversationHistory, systemPrompt), + }, +}); + export const generateThreadTitle = (message: ChatMessage) => { const text = getTextFromThreadContent(message.content) .replace(/\s+/g, " ") diff --git a/client/packages/lowcoder/src/comps/comps/iframeComp.tsx b/client/packages/lowcoder/src/comps/comps/iframeComp.tsx index 9f18bd00a6..dd72271a1d 100644 --- a/client/packages/lowcoder/src/comps/comps/iframeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/iframeComp.tsx @@ -8,7 +8,6 @@ import { styleControl } from "comps/controls/styleControl"; import { AnimationStyle, AnimationStyleType, IframeStyle, IframeStyleType } from "comps/controls/styleControlConstants"; import { hiddenPropertyView, showDataLoadingIndicatorsPropertyView } from "comps/utils/propertyUtils"; import { trans } from "i18n"; -import log from "loglevel"; import { useEditorStore } from "comps/editorStore"; @@ -33,8 +32,28 @@ ${props=>props.$animationStyle} } `; -const regex = - /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)/g; +export function getIframeSrc(url: string): string { + const value = url.trim(); + if (!value) { + return "about:blank"; + } + + const hasScheme = /^[a-z][a-z\d+.-]*:/i.test(value); + const isRelativeUrl = + value.startsWith("/") || value.startsWith("./") || value.startsWith("../"); + if (!hasScheme && !isRelativeUrl) { + return "about:blank"; + } + + try { + const parsedUrl = new URL(value, "https://lowcoder.local"); + return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:" + ? value + : "about:blank"; + } catch { + return "about:blank"; + } +} let IFrameCompBase = new UICompBuilder( { @@ -57,8 +76,7 @@ let IFrameCompBase = new UICompBuilder( props.allowCamera && allow.push("camera"); props.allowMicrophone && allow.push("microphone"); - const src = regex.test(props.url) ? props.url : "about:blank"; - log.log(props.url, regex.test(props.url) ? props.url : "about:blank", src); + const src = getIframeSrc(props.url); return (