From 158de18668342e3536e4b5f5cae6764ed72bb723 Mon Sep 17 00:00:00 2001 From: FARAN Date: Mon, 7 Jul 2025 18:27:25 +0500 Subject: [PATCH 1/2] [feat] replace mock data with query --- .../src/comps/comps/chatComp/chatComp.tsx | 14 +++- .../src/comps/comps/chatComp/chatCompTypes.ts | 3 + .../comps/comps/chatComp/chatPropertyView.tsx | 5 ++ .../src/comps/comps/chatComp/chatView.tsx | 11 ++- .../comps/chatComp/components/ChatApp.tsx | 11 ++- .../comps/chatComp/components/ChatMain.tsx | 77 +++++++++++++++---- 6 files changed, 101 insertions(+), 20 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx index 75de96494..9a6786843 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx @@ -7,13 +7,21 @@ import { ChatPropertyView } from "./chatPropertyView"; // Build the component const ChatTmpComp = new UICompBuilder( - chatChildrenMap, - (props) => + chatChildrenMap, + (props, dispatch) => ( + + ) ) .setPropertyViewFn((children) => ) .build(); -// Export the component +// Export the component with exposed variables export const ChatComp = withExposingConfigs(ChatTmpComp, [ new NameConfig("text", "Chat component text"), + new NameConfig("currentMessage", "Current user message"), ]); \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatCompTypes.ts b/client/packages/lowcoder/src/comps/comps/chatComp/chatCompTypes.ts index 79ba4c80d..78b775eab 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatCompTypes.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatCompTypes.ts @@ -1,5 +1,6 @@ // client/packages/lowcoder/src/comps/comps/chatComp/chatCompTypes.ts import { StringControl, NumberControl } from "comps/controls/codeControl"; +import { stringExposingStateControl } from "comps/controls/codeStateControl"; import { withDefault } from "comps/generators"; import { BoolControl } from "comps/controls/boolControl"; import { dropdownControl } from "comps/controls/dropdownControl"; @@ -14,6 +15,7 @@ const ModelTypeOptions = [ export const chatChildrenMap = { text: withDefault(StringControl, "Chat Component Placeholder"), chatQuery: QuerySelectControl, + currentMessage: stringExposingStateControl("currentMessage", ""), modelType: dropdownControl(ModelTypeOptions, "direct-llm"), streaming: BoolControl.DEFAULT_TRUE, systemPrompt: withDefault(StringControl, "You are a helpful assistant."), @@ -24,6 +26,7 @@ export const chatChildrenMap = { export type ChatCompProps = { text: string; chatQuery: string; + currentMessage: string; modelType: string; streaming: boolean; systemPrompt: string; diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx index b4f42c8e1..5c9c30561 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx @@ -9,6 +9,11 @@ export const ChatPropertyView = React.memo((props: any) => {
{children.text.propertyView({ label: "Text" })} {children.chatQuery.propertyView({ label: "Chat Query" })} + {children.currentMessage.propertyView({ + label: "Current Message (Dynamic)", + placeholder: "Shows the current user message", + disabled: true + })} {children.modelType.propertyView({ label: "Model Type" })} {children.streaming.propertyView({ label: "Enable Streaming" })} {children.systemPrompt.propertyView({ diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatView.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatView.tsx index 07383b48a..93b95af4c 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatView.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatView.tsx @@ -1,13 +1,20 @@ // client/packages/lowcoder/src/comps/comps/chatComp/chatView.tsx import React from "react"; import { ChatCompProps } from "./chatCompTypes"; +import { CompAction } from "lowcoder-core"; import { ChatApp } from "./components/ChatApp"; import "@assistant-ui/styles/index.css"; import "@assistant-ui/styles/markdown.css"; -export const ChatView = React.memo((props: ChatCompProps) => { - return ; +// Extend the props we receive so we can forward the redux dispatch +interface ChatViewProps extends ChatCompProps { + dispatch?: (action: CompAction) => void; +} + +export const ChatView = React.memo((props: ChatViewProps) => { + const { chatQuery, currentMessage, dispatch } = props; + return ; }); ChatView.displayName = 'ChatView'; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatApp.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatApp.tsx index e87ed8585..3353de689 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatApp.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatApp.tsx @@ -1,10 +1,17 @@ import { ChatProvider } from "./context/ChatContext"; import { ChatMain } from "./ChatMain"; +import { CompAction } from "lowcoder-core"; -export function ChatApp() { +interface ChatAppProps { + chatQuery: string; + currentMessage: string; + dispatch?: (action: CompAction) => void; +} + +export function ChatApp({ chatQuery, currentMessage, dispatch }: ChatAppProps) { return ( - + ); } diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatMain.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatMain.tsx index d0e151d88..0f5025d92 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatMain.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatMain.tsx @@ -16,6 +16,8 @@ import { ArchivedThreadData } from "./context/ChatContext"; import styled from "styled-components"; +import { routeByNameAction, executeQueryAction, CompAction, changeChildAction } from "lowcoder-core"; +import { getPromiseAfterDispatch } from "util/promiseUtils"; const ChatContainer = styled.div` display: flex; @@ -45,17 +47,57 @@ const ChatContainer = styled.div` const generateId = () => Math.random().toString(36).substr(2, 9); -const callYourAPI = async (text: string) => { - // Simulate API delay - await new Promise(resolve => setTimeout(resolve, 1500)); - - // Simple responses - return { - content: "This is a mock response from your backend. You typed: " + text - }; +// Helper to call the Lowcoder query system +const callQuery = async ( + queryName: string, + prompt: string, + dispatch?: (action: CompAction) => void +) => { + // If no query selected or dispatch unavailable, fallback with mock response + if (!queryName || !dispatch) { + await new Promise((res) => setTimeout(res, 500)); + return { content: "(mock) You typed: " + prompt }; + } + + try { + const result: any = await getPromiseAfterDispatch( + dispatch, + routeByNameAction( + queryName, + executeQueryAction({ + // Send the user prompt as variable named 'prompt' by default + args: { prompt: { value: prompt } }, + }) + ) + ); + + // Extract reply text from the query result + let reply: string; + if (typeof result === "string") { + reply = result; + } else if (result && typeof result === "object") { + reply = + (result as any).response ?? + (result as any).message ?? + (result as any).content ?? + JSON.stringify(result); + } else { + reply = String(result); + } + + return { content: reply }; + } catch (e: any) { + throw new Error(e?.message || "Query execution failed"); + } }; -export function ChatMain() { +interface ChatMainProps { + chatQuery: string; + currentMessage: string; + dispatch?: (action: CompAction) => void; +} + +export function ChatMain({ chatQuery, currentMessage, dispatch }: ChatMainProps) { const { state, actions } = useChatContext(); const [isRunning, setIsRunning] = useState(false); @@ -86,13 +128,18 @@ export function ChatMain() { timestamp: Date.now(), }; + // Update currentMessage state to expose to queries + if (dispatch) { + dispatch(changeChildAction("currentMessage", userMessage.text, false)); + } + // Update current thread with new user message await actions.addMessage(state.currentThreadId, userMessage); setIsRunning(true); try { - // Call mock API - const response = await callYourAPI(userMessage.text); + // Call selected query / fallback to mock + const response = await callQuery(chatQuery, userMessage.text, dispatch); const assistantMessage: MyMessage = { id: generateId(), @@ -140,13 +187,17 @@ export function ChatMain() { }; newMessages.push(editedMessage); + // Update currentMessage state to expose to queries + if (dispatch) { + dispatch(changeChildAction("currentMessage", editedMessage.text, false)); + } + // Update messages using the new context action await actions.updateMessages(state.currentThreadId, newMessages); setIsRunning(true); try { - // Generate new response - const response = await callYourAPI(editedMessage.text); + const response = await callQuery(chatQuery, editedMessage.text, dispatch); const assistantMessage: MyMessage = { id: generateId(), From c3beba5f86d2ac112c03863cfc838b691c44e075 Mon Sep 17 00:00:00 2001 From: FARAN Date: Mon, 7 Jul 2025 23:28:05 +0500 Subject: [PATCH 2/2] [Feat]: make chat component flexible --- .../src/comps/comps/chatComp/chatCompTypes.ts | 7 +- .../src/comps/comps/chatComp/chatView.tsx | 32 ++- .../comps/chatComp/components/ChatApp.tsx | 34 +++- .../comps/chatComp/components/ChatMain.tsx | 39 +++- .../components/context/ChatContext.tsx | 26 +-- .../chatComp/utils/chatStorageFactory.ts | 189 ++++++++++++++++++ .../comps/chatComp/utils/responseFactory.ts | 27 +++ .../comps/chatComp/utils/responseHandlers.ts | 99 +++++++++ .../src/pages/editor/bottom/BottomPanel.tsx | 12 +- 9 files changed, 436 insertions(+), 29 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/comps/chatComp/utils/chatStorageFactory.ts create mode 100644 client/packages/lowcoder/src/comps/comps/chatComp/utils/responseFactory.ts create mode 100644 client/packages/lowcoder/src/comps/comps/chatComp/utils/responseHandlers.ts diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatCompTypes.ts b/client/packages/lowcoder/src/comps/comps/chatComp/chatCompTypes.ts index bfbcf9f4d..3391a9be6 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatCompTypes.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatCompTypes.ts @@ -10,19 +10,20 @@ import QuerySelectControl from "comps/controls/querySelectControl"; const ModelTypeOptions = [ { label: "Direct LLM", value: "direct-llm" }, { label: "n8n Workflow", value: "n8n" }, + { label: "Query", value: "query" }, ] as const; export const chatChildrenMap = { text: withDefault(StringControl, "Chat Component Placeholder"), chatQuery: QuerySelectControl, currentMessage: stringExposingStateControl("currentMessage", ""), - modelType: dropdownControl(ModelTypeOptions, "direct-llm"), + modelType: dropdownControl(ModelTypeOptions, "query"), modelHost: withDefault(StringControl, ""), streaming: BoolControl.DEFAULT_TRUE, systemPrompt: withDefault(StringControl, "You are a helpful assistant."), agent: BoolControl, maxInteractions: withDefault(NumberControl, 10), - tableName: withDefault(StringControl, ""), + tableName: withDefault(StringControl, "default"), }; export type ChatCompProps = { @@ -30,8 +31,10 @@ export type ChatCompProps = { chatQuery: string; currentMessage: string; modelType: string; + modelHost: string; streaming: boolean; systemPrompt: string; agent: boolean; maxInteractions: number; + tableName: string; }; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatView.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatView.tsx index 93b95af4c..544e73e8d 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatView.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatView.tsx @@ -1,8 +1,9 @@ // client/packages/lowcoder/src/comps/comps/chatComp/chatView.tsx -import React from "react"; +import React, { useMemo } from "react"; import { ChatCompProps } from "./chatCompTypes"; import { CompAction } from "lowcoder-core"; import { ChatApp } from "./components/ChatApp"; +import { createChatStorage } from './utils/chatStorageFactory'; import "@assistant-ui/styles/index.css"; import "@assistant-ui/styles/markdown.css"; @@ -13,8 +14,33 @@ interface ChatViewProps extends ChatCompProps { } export const ChatView = React.memo((props: ChatViewProps) => { - const { chatQuery, currentMessage, dispatch } = props; - return ; + const { + chatQuery, + currentMessage, + dispatch, + modelType, + modelHost, + systemPrompt, + streaming, + tableName + } = props; + + // Create storage instance based on tableName + const storage = useMemo(() => createChatStorage(tableName || "default"), [tableName]); + + return ( + + ); }); ChatView.displayName = 'ChatView'; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatApp.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatApp.tsx index 3353de689..12ee0071f 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatApp.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatApp.tsx @@ -1,17 +1,43 @@ import { ChatProvider } from "./context/ChatContext"; import { ChatMain } from "./ChatMain"; import { CompAction } from "lowcoder-core"; +import { createChatStorage } from "../utils/chatStorageFactory"; interface ChatAppProps { chatQuery: string; currentMessage: string; dispatch?: (action: CompAction) => void; + modelType: string; + modelHost?: string; + systemPrompt?: string; + streaming?: boolean; + tableName: string; + storage: ReturnType; } -export function ChatApp({ chatQuery, currentMessage, dispatch }: ChatAppProps) { +export function ChatApp({ + chatQuery, + currentMessage, + dispatch, + modelType, + modelHost, + systemPrompt, + streaming, + tableName, + storage +}: ChatAppProps) { return ( - - + + ); -} +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatMain.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatMain.tsx index 5be9d08ec..0d814aaf1 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatMain.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatMain.tsx @@ -18,6 +18,9 @@ import { import styled from "styled-components"; import { routeByNameAction, executeQueryAction, CompAction, changeChildAction } from "lowcoder-core"; import { getPromiseAfterDispatch } from "util/promiseUtils"; +// ADD THIS IMPORT: +import { createResponseHandler } from '../utils/responseFactory'; +import { useMemo } from 'react'; // if not already imported const ChatContainer = styled.div` display: flex; @@ -95,16 +98,46 @@ const callQuery = async ( } }; +// AFTER: interface ChatMainProps { chatQuery: string; currentMessage: string; dispatch?: (action: CompAction) => void; + // Add new props for response handling + modelType: string; + modelHost?: string; + systemPrompt?: string; + streaming?: boolean; + tableName: string; } -export function ChatMain({ chatQuery, currentMessage, dispatch }: ChatMainProps) { +export function ChatMain({ + chatQuery, + currentMessage, + dispatch, + modelType, + modelHost, + systemPrompt, + streaming, + tableName }: ChatMainProps) { const { state, actions } = useChatContext(); const [isRunning, setIsRunning] = useState(false); +// Create response handler based on model type +const responseHandler = useMemo(() => { + const responseType = modelType === "n8n" ? "direct-api" : "query"; + + return createResponseHandler(responseType, { + // Query handler config + chatQuery, + dispatch, + // Direct API handler config + modelHost, + systemPrompt, + streaming + }); +}, [modelType, chatQuery, dispatch, modelHost, systemPrompt, streaming]); + console.log("STATE", state); // Get messages for current thread @@ -143,7 +176,7 @@ export function ChatMain({ chatQuery, currentMessage, dispatch }: ChatMainProps) try { // Call selected query / fallback to mock - const response = await callQuery(chatQuery, userMessage.text, dispatch); + const response = await responseHandler.sendMessage(userMessage.text); const assistantMessage: MyMessage = { id: generateId(), @@ -201,7 +234,7 @@ export function ChatMain({ chatQuery, currentMessage, dispatch }: ChatMainProps) setIsRunning(true); try { - const response = await callQuery(chatQuery, editedMessage.text, dispatch); + const response = await responseHandler.sendMessage(editedMessage.text); const assistantMessage: MyMessage = { id: generateId(), 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 41ef892af..68c4d4206 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 @@ -1,6 +1,5 @@ import React, { createContext, useContext, useReducer, useEffect, ReactNode } from "react"; -import { chatStorage, ThreadData as StoredThreadData } from "../../utils/chatStorage"; - +import { ThreadData as StoredThreadData } from "../../utils/chatStorageFactory"; // Define thread-specific message type export interface MyMessage { id: string; @@ -176,7 +175,8 @@ interface ChatContextType { const ChatContext = createContext(null); // Chat provider component -export function ChatProvider({ children }: { children: ReactNode }) { + export function ChatProvider({ children, storage }: { children: ReactNode, storage: ReturnType; + }) { const [state, dispatch] = useReducer(chatReducer, initialState); // Initialize data from storage @@ -184,10 +184,10 @@ export function ChatProvider({ children }: { children: ReactNode }) { dispatch({ type: "INITIALIZE_START" }); try { - await chatStorage.initialize(); + await storage.initialize(); // Load all threads from storage - const storedThreads = await chatStorage.getAllThreads(); + const storedThreads = await storage.getAllThreads(); if (storedThreads.length > 0) { // Convert stored threads to UI format @@ -200,7 +200,7 @@ export function ChatProvider({ children }: { children: ReactNode }) { // Load messages for each thread const threadMessages = new Map(); for (const thread of storedThreads) { - const messages = await chatStorage.getMessages(thread.threadId); + const messages = await storage.getMessages(thread.threadId); threadMessages.set(thread.threadId, messages); } @@ -228,7 +228,7 @@ export function ChatProvider({ children }: { children: ReactNode }) { createdAt: Date.now(), updatedAt: Date.now(), }; - await chatStorage.saveThread(defaultThread); + await storage.saveThread(defaultThread); dispatch({ type: "INITIALIZE_SUCCESS", @@ -268,7 +268,7 @@ export function ChatProvider({ children }: { children: ReactNode }) { createdAt: Date.now(), updatedAt: Date.now(), }; - await chatStorage.saveThread(storedThread); + await storage.saveThread(storedThread); dispatch({ type: "MARK_SAVED" }); } catch (error) { console.error("Failed to save new thread:", error); @@ -283,14 +283,14 @@ export function ChatProvider({ children }: { children: ReactNode }) { // Save to storage try { - const existingThread = await chatStorage.getThread(threadId); + const existingThread = await storage.getThread(threadId); if (existingThread) { const updatedThread: StoredThreadData = { ...existingThread, ...updates, updatedAt: Date.now(), }; - await chatStorage.saveThread(updatedThread); + await storage.saveThread(updatedThread); dispatch({ type: "MARK_SAVED" }); } } catch (error) { @@ -304,7 +304,7 @@ export function ChatProvider({ children }: { children: ReactNode }) { // Delete from storage try { - await chatStorage.deleteThread(threadId); + await storage.deleteThread(threadId); dispatch({ type: "MARK_SAVED" }); } catch (error) { console.error("Failed to delete thread:", error); @@ -318,7 +318,7 @@ export function ChatProvider({ children }: { children: ReactNode }) { // Save to storage try { - await chatStorage.saveMessage(message, threadId); + await storage.saveMessage(message, threadId); dispatch({ type: "MARK_SAVED" }); } catch (error) { console.error("Failed to save message:", error); @@ -331,7 +331,7 @@ export function ChatProvider({ children }: { children: ReactNode }) { // Save to storage try { - await chatStorage.saveMessages(messages, threadId); + await storage.saveMessages(messages, threadId); dispatch({ type: "MARK_SAVED" }); } catch (error) { console.error("Failed to save messages:", error); diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/utils/chatStorageFactory.ts b/client/packages/lowcoder/src/comps/comps/chatComp/utils/chatStorageFactory.ts new file mode 100644 index 000000000..e7f44a26c --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/chatComp/utils/chatStorageFactory.ts @@ -0,0 +1,189 @@ +// client/packages/lowcoder/src/comps/comps/chatComp/utils/chatStorageFactory.ts +import alasql from "alasql"; +import { MyMessage } from "../components/context/ChatContext"; + +// Thread data interface +export interface ThreadData { + threadId: string; + status: "regular" | "archived"; + title: string; + createdAt: number; + updatedAt: number; +} + +export const createChatStorage = (tableName: string) => { + const dbName = `ChatDB_${tableName}`; + const threadsTable = `${tableName}_threads`; + const messagesTable = `${tableName}_messages`; + + return { + async initialize() { + try { + // Create database with localStorage backend + await alasql.promise(`CREATE LOCALSTORAGE DATABASE IF NOT EXISTS ${dbName}`); + await alasql.promise(`ATTACH LOCALSTORAGE DATABASE ${dbName}`); + await alasql.promise(`USE ${dbName}`); + + // Create threads table + await alasql.promise(` + CREATE TABLE IF NOT EXISTS ${threadsTable} ( + threadId STRING PRIMARY KEY, + status STRING, + title STRING, + createdAt NUMBER, + updatedAt NUMBER + ) + `); + + // Create messages table + await alasql.promise(` + CREATE TABLE IF NOT EXISTS ${messagesTable} ( + id STRING PRIMARY KEY, + threadId STRING, + role STRING, + text STRING, + timestamp NUMBER + ) + `); + + console.log(`✅ Chat database initialized: ${dbName}`); + } catch (error) { + console.error(`Failed to initialize chat database ${dbName}:`, error); + throw error; + } + }, + + async saveThread(thread: ThreadData) { + try { + // Insert or replace thread + await alasql.promise(` + DELETE FROM ${threadsTable} WHERE threadId = ? + `, [thread.threadId]); + + await alasql.promise(` + INSERT INTO ${threadsTable} VALUES (?, ?, ?, ?, ?) + `, [thread.threadId, thread.status, thread.title, thread.createdAt, thread.updatedAt]); + } catch (error) { + console.error("Failed to save thread:", error); + throw error; + } + }, + + async getThread(threadId: string) { + try { + const result = await alasql.promise(` + SELECT * FROM ${threadsTable} WHERE threadId = ? + `, [threadId]) as ThreadData[]; + + return result && result.length > 0 ? result[0] : null; + } catch (error) { + console.error("Failed to get thread:", error); + return null; + } + }, + + async getAllThreads() { + try { + const result = await alasql.promise(` + SELECT * FROM ${threadsTable} ORDER BY updatedAt DESC + `) as ThreadData[]; + + return Array.isArray(result) ? result : []; + } catch (error) { + console.error("Failed to get threads:", error); + return []; + } + }, + + async deleteThread(threadId: string) { + try { + // Delete thread and all its messages + await alasql.promise(`DELETE FROM ${threadsTable} WHERE threadId = ?`, [threadId]); + await alasql.promise(`DELETE FROM ${messagesTable} WHERE threadId = ?`, [threadId]); + } catch (error) { + console.error("Failed to delete thread:", error); + throw error; + } + }, + + async saveMessage(message: MyMessage, threadId: string) { + try { + // Insert or replace message + await alasql.promise(` + DELETE FROM ${messagesTable} WHERE id = ? + `, [message.id]); + + await alasql.promise(` + INSERT INTO ${messagesTable} VALUES (?, ?, ?, ?, ?) + `, [message.id, threadId, message.role, message.text, message.timestamp]); + } catch (error) { + console.error("Failed to save message:", error); + throw error; + } + }, + + async saveMessages(messages: MyMessage[], threadId: string) { + try { + // Delete existing messages for this thread + await alasql.promise(`DELETE FROM ${messagesTable} WHERE threadId = ?`, [threadId]); + + // Insert all messages + for (const message of messages) { + await alasql.promise(` + INSERT INTO ${messagesTable} VALUES (?, ?, ?, ?, ?) + `, [message.id, threadId, message.role, message.text, message.timestamp]); + } + } catch (error) { + console.error("Failed to save messages:", error); + throw error; + } + }, + + async getMessages(threadId: string) { + try { + const result = await alasql.promise(` + SELECT id, role, text, timestamp FROM ${messagesTable} + WHERE threadId = ? ORDER BY timestamp ASC + `, [threadId]) as MyMessage[]; + + return Array.isArray(result) ? result : []; + } catch (error) { + console.error("Failed to get messages:", error); + return []; + } + }, + + async deleteMessages(threadId: string) { + try { + await alasql.promise(`DELETE FROM ${messagesTable} WHERE threadId = ?`, [threadId]); + } catch (error) { + console.error("Failed to delete messages:", error); + throw error; + } + }, + + async clearAllData() { + try { + await alasql.promise(`DELETE FROM ${threadsTable}`); + await alasql.promise(`DELETE FROM ${messagesTable}`); + } catch (error) { + console.error("Failed to clear all data:", error); + throw error; + } + }, + + async resetDatabase() { + try { + // Drop the entire database + await alasql.promise(`DROP LOCALSTORAGE DATABASE IF EXISTS ${dbName}`); + + // Reinitialize fresh + await this.initialize(); + console.log(`✅ Database reset and reinitialized: ${dbName}`); + } catch (error) { + console.error("Failed to reset database:", error); + throw error; + } + } + }; +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/utils/responseFactory.ts b/client/packages/lowcoder/src/comps/comps/chatComp/utils/responseFactory.ts new file mode 100644 index 000000000..91d479335 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/chatComp/utils/responseFactory.ts @@ -0,0 +1,27 @@ +// client/packages/lowcoder/src/comps/comps/chatComp/utils/responseFactory.ts +import { + queryResponseHandler, + directApiResponseHandler, + mockResponseHandler + } from './responseHandlers'; + + export const createResponseHandler = (type: string, config: any) => { + const sendMessage = async (message: string) => { + switch (type) { + case "query": + return await queryResponseHandler(message, config); + + case "direct-api": + case "n8n": + return await directApiResponseHandler(message, config); + + case "mock": + return await mockResponseHandler(message, config); + + default: + throw new Error(`Unknown response type: ${type}`); + } + }; + + return { sendMessage }; + }; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/utils/responseHandlers.ts b/client/packages/lowcoder/src/comps/comps/chatComp/utils/responseHandlers.ts new file mode 100644 index 000000000..ae384660c --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/chatComp/utils/responseHandlers.ts @@ -0,0 +1,99 @@ +// client/packages/lowcoder/src/comps/comps/chatComp/utils/responseHandlers.ts +import { CompAction, routeByNameAction, executeQueryAction } from "lowcoder-core"; +import { getPromiseAfterDispatch } from "util/promiseUtils"; + +// Query response handler (your current logic) +export const queryResponseHandler = async ( + message: string, + config: { chatQuery: string; dispatch?: (action: CompAction) => void } +) => { + const { chatQuery, dispatch } = config; + + // If no query selected or dispatch unavailable, fallback with mock response + if (!chatQuery || !dispatch) { + await new Promise((res) => setTimeout(res, 500)); + return { content: "(mock) You typed: " + message }; + } + + try { + const result: any = await getPromiseAfterDispatch( + dispatch, + routeByNameAction( + chatQuery, + executeQueryAction({ + // Send the user prompt as variable named 'prompt' by default + args: { prompt: { value: message } }, + }) + ) + ); + + // Extract reply text from the query result + let reply: string; + if (typeof result === "string") { + reply = result; + } else if (result && typeof result === "object") { + reply = + (result as any).response ?? + (result as any).message ?? + (result as any).content ?? + JSON.stringify(result); + } else { + reply = String(result); + } + + return { content: reply }; + } catch (e: any) { + throw new Error(e?.message || "Query execution failed"); + } +}; + +// Direct API response handler (for bottom panel usage) +export const directApiResponseHandler = async ( + message: string, + config: { modelHost: string; systemPrompt: string; streaming?: boolean } +) => { + const { modelHost, systemPrompt, streaming } = config; + + if (!modelHost) { + throw new Error("Model host is required for direct API calls"); + } + + try { + const response = await fetch(modelHost, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message, + systemPrompt: systemPrompt || "You are a helpful assistant.", + streaming: streaming || false + }) + }); + + if (!response.ok) { + throw new Error(`API call failed: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + + // 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(`Direct API call failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + } +}; + +// Mock response handler (for testing) +export const mockResponseHandler = async ( + message: string, + config: { delay?: number; prefix?: string } +) => { + const { delay = 1000, prefix = "Mock response" } = config; + + await new Promise(resolve => setTimeout(resolve, delay)); + + return { content: `${prefix}: ${message}` }; +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx b/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx index c71364470..8903ef237 100644 --- a/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx +++ b/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx @@ -121,11 +121,15 @@ function Bottom(props: any) { )}