Skip to content

Make Chat Component Flexible #1850

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: feat/assistant
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion client/packages/lowcoder/src/components/ResCreatePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { BottomResTypeEnum } from "types/bottomRes";
import { LargeBottomResIconWrapper } from "util/bottomResUtils";
import type { PageType } from "../constants/pageConstants";
import type { SizeType } from "antd/es/config-provider/SizeContext";
import { Datasource } from "constants/datasourceConstants";
import { Datasource, QUICK_SSE_HTTP_API_ID } from "constants/datasourceConstants";
import {
QUICK_GRAPHQL_ID,
QUICK_REST_API_ID,
Expand Down Expand Up @@ -172,13 +172,22 @@ const ResButton = (props: {
compType: "streamApi",
},
},

alasql: {
label: trans("query.quickAlasql"),
type: BottomResTypeEnum.Query,
extra: {
compType: "alasql",
},
},
sseHttpApi: {
label: trans("query.quickSseHttpAPI"),
type: BottomResTypeEnum.Query,
extra: {
compType: "sseHttpApi",
dataSourceId: QUICK_SSE_HTTP_API_ID,
},
},
graphql: {
label: trans("query.quickGraphql"),
type: BottomResTypeEnum.Query,
Expand Down Expand Up @@ -339,6 +348,7 @@ export function ResCreatePanel(props: ResCreateModalProps) {
<DataSourceListWrapper $placement={placement}>
<ResButton size={buttonSize} identifier={"restApi"} onSelect={onSelect} />
<ResButton size={buttonSize} identifier={"streamApi"} onSelect={onSelect} />
<ResButton size={buttonSize} identifier={"sseHttpApi"} onSelect={onSelect} />
<ResButton size={buttonSize} identifier={"alasql"} onSelect={onSelect} />
<ResButton size={buttonSize} identifier={"graphql"} onSelect={onSelect} />
<DataSourceButton size={buttonSize} onClick={() => setCurlModalVisible(true)}>
Expand Down
211 changes: 166 additions & 45 deletions client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,167 @@
// client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx
import { UICompBuilder } from "comps/generators";
import { NameConfig, withExposingConfigs } from "comps/generators/withExposing";
import { chatChildrenMap } from "./chatCompTypes";
import { ChatView } from "./chatView";
import { ChatPropertyView } from "./chatPropertyView";
import { useEffect, useState } from "react";
import { changeChildAction } from "lowcoder-core";

// Build the component
let ChatTmpComp = new UICompBuilder(
chatChildrenMap,
(props, dispatch) => {
useEffect(() => {
if (Boolean(props.tableName)) return;

// Generate a unique database name for this ChatApp instance
const generateUniqueTableName = () => {
const timestamp = Date.now();
const randomId = Math.random().toString(36).substring(2, 15);
return `TABLE_${timestamp}`;
};

const tableName = generateUniqueTableName();
dispatch(changeChildAction('tableName', tableName, true));
}, [props.tableName]);

if (!props.tableName) {
return null; // Don't render until we have a unique DB name
}
return <ChatView {...props} chatQuery={props.chatQuery.value} />;
}
)
.setPropertyViewFn((children) => <ChatPropertyView children={children} />)
.build();

ChatTmpComp = class extends ChatTmpComp {
override autoHeight(): boolean {
return this.children.autoHeight.getView();
}
};

// Export the component
export const ChatComp = withExposingConfigs(ChatTmpComp, [
new NameConfig("text", "Chat component text"),
// client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx

import { UICompBuilder } from "comps/generators";
import { NameConfig, withExposingConfigs } from "comps/generators/withExposing";
import { StringControl } from "comps/controls/codeControl";
import { arrayObjectExposingStateControl, stringExposingStateControl } from "comps/controls/codeStateControl";
import { withDefault } from "comps/generators";
import { BoolControl } from "comps/controls/boolControl";
import { dropdownControl } from "comps/controls/dropdownControl";
import QuerySelectControl from "comps/controls/querySelectControl";
import { ChatCore } from "./components/ChatCore";
import { ChatPropertyView } from "./chatPropertyView";
import { createChatStorage } from "./utils/storageFactory";
import { QueryHandler, createMessageHandler } from "./handlers/messageHandlers";
import { useMemo, useRef, useEffect } from "react";
import { changeChildAction } from "lowcoder-core";

import "@assistant-ui/styles/index.css";
import "@assistant-ui/styles/markdown.css";

// ============================================================================
// SIMPLIFIED CHILDREN MAP - ONLY ESSENTIAL PROPS
// ============================================================================

function generateUniqueTableName(): string {
return `chat${Math.floor(1000 + Math.random() * 9000)}`;
}

const ModelTypeOptions = [
{ label: "Query", value: "query" },
{ label: "N8N Workflow", value: "n8n" },
] as const;

export const chatChildrenMap = {
// Storage
// Storage (add the hidden property here)
_internalDbName: withDefault(StringControl, ""),
// Message Handler Configuration
handlerType: dropdownControl(ModelTypeOptions, "query"),
chatQuery: QuerySelectControl, // Only used for "query" type
modelHost: withDefault(StringControl, ""), // Only used for "n8n" type
systemPrompt: withDefault(StringControl, "You are a helpful assistant."),
streaming: BoolControl.DEFAULT_TRUE,

// UI Configuration
placeholder: withDefault(StringControl, "Chat Component"),

// Database Information (read-only)
databaseName: withDefault(StringControl, ""),

// Exposed Variables (not shown in Property View)
currentMessage: stringExposingStateControl("currentMessage", ""),
conversationHistory: stringExposingStateControl("conversationHistory", "[]"),
};

// ============================================================================
// CLEAN CHATCOMP - USES NEW ARCHITECTURE
// ============================================================================

const ChatTmpComp = new UICompBuilder(
chatChildrenMap,
(props, dispatch) => {

const uniqueTableName = useRef<string>();

// Generate unique table name once (with persistence)
if (!uniqueTableName.current) {
// Use persisted name if exists, otherwise generate new one
uniqueTableName.current = props._internalDbName || generateUniqueTableName();

// Save the name for future refreshes
if (!props._internalDbName) {
dispatch(changeChildAction("_internalDbName", uniqueTableName.current, false));
}

// Update the database name in the props for display
const dbName = `ChatDB_${uniqueTableName.current}`;
dispatch(changeChildAction("databaseName", dbName, false));
}
// Create storage with unique table name
const storage = useMemo(() =>
createChatStorage(uniqueTableName.current!),
[]
);

// Create message handler based on type
const messageHandler = useMemo(() => {
const handlerType = props.handlerType;

if (handlerType === "query") {
return new QueryHandler({
chatQuery: props.chatQuery.value,
dispatch,
streaming: props.streaming
});
} else if (handlerType === "n8n") {
return createMessageHandler("n8n", {
modelHost: props.modelHost,
systemPrompt: props.systemPrompt,
streaming: props.streaming
});
} else {
// Fallback to mock handler
return createMessageHandler("mock", {
chatQuery: props.chatQuery.value,
dispatch,
streaming: props.streaming
});
}
}, [
props.handlerType,
props.chatQuery,
props.modelHost,
props.systemPrompt,
props.streaming,
dispatch
]);

// Handle message updates for exposed variable
const handleMessageUpdate = (message: string) => {
dispatch(changeChildAction("currentMessage", message, false));
};

// Handle conversation history updates for exposed variable
const handleConversationUpdate = (conversationHistory: any[]) => {
// Format conversation history for use in queries
const formattedHistory = conversationHistory.map(msg => ({
role: msg.role,
content: msg.text,
timestamp: msg.timestamp
}));
dispatch(changeChildAction("conversationHistory", JSON.stringify(formattedHistory), false));
};

// Cleanup on unmount
useEffect(() => {
console.log("cleanup on unmount");
return () => {
console.log("cleanup on unmount");
const tableName = uniqueTableName.current;
if (tableName) {
storage.cleanup();
}
};
}, []);

return (
<ChatCore
storage={storage}
messageHandler={messageHandler}
onMessageUpdate={handleMessageUpdate}
onConversationUpdate={handleConversationUpdate}
/>
);
}
)
.setPropertyViewFn((children) => <ChatPropertyView children={children} />)
.build();

// ============================================================================
// EXPORT WITH EXPOSED VARIABLES
// ============================================================================

export const ChatComp = withExposingConfigs(ChatTmpComp, [
new NameConfig("currentMessage", "Current user message"),
new NameConfig("conversationHistory", "Full conversation history as JSON array"),
]);
65 changes: 26 additions & 39 deletions client/packages/lowcoder/src/comps/comps/chatComp/chatCompTypes.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,26 @@
// client/packages/lowcoder/src/comps/comps/chatComp/chatCompTypes.ts
import { StringControl, NumberControl } from "comps/controls/codeControl";
import { withDefault } from "comps/generators";
import { BoolControl } from "comps/controls/boolControl";
import { dropdownControl } from "comps/controls/dropdownControl";
import QuerySelectControl from "comps/controls/querySelectControl";
import { AutoHeightControl } from "@lowcoder-ee/comps/controls/autoHeightControl";

// Model type dropdown options
const ModelTypeOptions = [
{ label: "Direct LLM", value: "direct-llm" },
{ label: "n8n Workflow", value: "n8n" },
] as const;

export const chatChildrenMap = {
text: withDefault(StringControl, "Chat Component Placeholder"),
modelType: dropdownControl(ModelTypeOptions, "direct-llm"),
modelHost: withDefault(StringControl, ""),
streaming: BoolControl.DEFAULT_TRUE,
systemPrompt: withDefault(StringControl, "You are a helpful assistant."),
agent: BoolControl,
maxInteractions: withDefault(NumberControl, 10),
chatQuery: QuerySelectControl,
autoHeight: AutoHeightControl,
tableName: withDefault(StringControl, ""),
};

export type ChatCompProps = {
text?: string;
chatQuery?: string;
modelType?: string;
streaming?: boolean;
systemPrompt?: string;
agent?: boolean;
maxInteractions?: number;
modelHost?: string;
autoHeight?: boolean;
tableName?: string;
};
// client/packages/lowcoder/src/comps/comps/chatComp/chatCompTypes.ts

// ============================================================================
// CLEAN CHATCOMP TYPES - SIMPLIFIED AND FOCUSED
// ============================================================================

export type ChatCompProps = {
// Storage
tableName: string;

// Message Handler
handlerType: "query" | "n8n";
chatQuery: string; // Only used when handlerType === "query"
modelHost: string; // Only used when handlerType === "n8n"
systemPrompt: string;
streaming: boolean;

// UI
placeholder: string;

// Exposed Variables
currentMessage: string; // Read-only exposed variable
};

// Legacy export for backwards compatibility (if needed)
export type ChatCompLegacyProps = ChatCompProps;
Loading
Loading