Skip to content

[In Progress] [Feat]: Chat Component #1841

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

Merged
Merged
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/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
"main": "src/index.sdk.ts",
"types": "src/index.sdk.ts",
"dependencies": {
"@ai-sdk/openai": "^1.3.22",
"@ant-design/icons": "^5.3.0",
"@assistant-ui/react": "^0.10.24",
"@assistant-ui/react-ai-sdk": "^0.10.14",
"@assistant-ui/react-markdown": "^0.10.5",
"@assistant-ui/styles": "^0.1.13",
"@bany/curl-to-json": "^1.2.8",
"@codemirror/autocomplete": "^6.11.1",
"@codemirror/commands": "^6.3.2",
Expand All @@ -28,6 +33,8 @@
"@jsonforms/core": "^3.5.1",
"@lottiefiles/dotlottie-react": "^0.13.0",
"@manaflair/redux-batch": "^1.0.0",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.7",
"@rjsf/antd": "^5.24.9",
"@rjsf/core": "^5.24.9",
"@rjsf/utils": "^5.24.9",
Expand All @@ -37,11 +44,13 @@
"@types/react-signature-canvas": "^1.0.2",
"@types/react-test-renderer": "^18.0.0",
"@types/react-virtualized": "^9.21.21",
"ai": "^4.3.16",
"alasql": "^4.6.6",
"animate.css": "^4.1.1",
"antd": "^5.25.2",
"axios": "^1.7.7",
"buffer": "^6.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.0.0",
"cnchar": "^3.2.4",
"coolshapes-react": "lowcoder-org/coolshapes-react",
Expand All @@ -61,6 +70,7 @@
"loglevel": "^1.8.0",
"lowcoder-core": "workspace:^",
"lowcoder-design": "workspace:^",
"lucide-react": "^0.525.0",
"mime": "^3.0.0",
"moment": "^2.29.4",
"numbro": "^2.3.6",
Expand Down Expand Up @@ -98,7 +108,7 @@
"regenerator-runtime": "^0.13.9",
"rehype-raw": "^6.1.1",
"rehype-sanitize": "^5.0.1",
"remark-gfm": "^4.0.0",
"remark-gfm": "^4.0.1",
"resize-observer-polyfill": "^1.5.1",
"simplebar-react": "^3.2.4",
"sql-formatter": "^8.2.0",
Expand Down
19 changes: 19 additions & 0 deletions client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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";

// Build the component
const ChatTmpComp = new UICompBuilder(
chatChildrenMap,
(props) => <ChatView {...props} chatQuery={props.chatQuery.value} />
)
.setPropertyViewFn((children) => <ChatPropertyView children={children} />)
.build();

// Export the component
export const ChatComp = withExposingConfigs(ChatTmpComp, [
new NameConfig("text", "Chat component text"),
]);
32 changes: 32 additions & 0 deletions client/packages/lowcoder/src/comps/comps/chatComp/chatCompTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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";

// 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"),
chatQuery: QuerySelectControl,
modelType: dropdownControl(ModelTypeOptions, "direct-llm"),
streaming: BoolControl.DEFAULT_TRUE,
systemPrompt: withDefault(StringControl, "You are a helpful assistant."),
agent: BoolControl,
maxInteractions: withDefault(NumberControl, 10),
};

export type ChatCompProps = {
text: string;
chatQuery: string;
modelType: string;
streaming: boolean;
systemPrompt: string;
agent: boolean;
maxInteractions: number;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx
import React from "react";
import { Section, sectionNames } from "lowcoder-design";

export const ChatPropertyView = React.memo((props: any) => {
const { children } = props;

return (
<Section name={sectionNames.basic}>
{children.text.propertyView({ label: "Text" })}
{children.chatQuery.propertyView({ label: "Chat Query" })}
{children.modelType.propertyView({ label: "Model Type" })}
{children.streaming.propertyView({ label: "Enable Streaming" })}
{children.systemPrompt.propertyView({
label: "System Prompt",
placeholder: "Enter system prompt...",
enableSpellCheck: false,
})}
{children.agent.propertyView({ label: "Enable Agent Mode" })}
{children.maxInteractions.propertyView({
label: "Max Interactions",
placeholder: "10",
})}
</Section>
);
});

ChatPropertyView.displayName = 'ChatPropertyView';
13 changes: 13 additions & 0 deletions client/packages/lowcoder/src/comps/comps/chatComp/chatView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// client/packages/lowcoder/src/comps/comps/chatComp/chatView.tsx
import React from "react";
import { ChatCompProps } from "./chatCompTypes";
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 <ChatApp />;
});

ChatView.displayName = 'ChatView';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ChatProvider } from "./context/ChatContext";
import { ChatMain } from "./ChatMain";

export function ChatApp() {
return (
<ChatProvider>
<ChatMain />
</ChatProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import React, { useState } from "react";
import {
useExternalStoreRuntime,
ThreadMessageLike,
AppendMessage,
AssistantRuntimeProvider,
ExternalStoreThreadListAdapter,
} from "@assistant-ui/react";
import { Thread } from "./assistant-ui/thread";
import { ThreadList } from "./assistant-ui/thread-list";
import {
useChatContext,
MyMessage,
ThreadData,
RegularThreadData,
ArchivedThreadData
} from "./context/ChatContext";
import styled from "styled-components";

const ChatContainer = styled.div`
display: flex;
height: 500px;

.aui-thread-list-root {
width: 250px;
background-color: #fff;
padding: 10px;
}

.aui-thread-root {
flex: 1;
background-color: #f9fafb;
}

.aui-thread-list-item {
cursor: pointer;
transition: background-color 0.2s ease;

&[data-active="true"] {
background-color: #dbeafe;
border: 1px solid #bfdbfe;
}
}
`;

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
};
};

export function ChatMain() {
const { state, actions } = useChatContext();
const [isRunning, setIsRunning] = useState(false);

console.log("STATE", state);

// Get messages for current thread
const currentMessages = actions.getCurrentMessages();

// Convert custom format to ThreadMessageLike
const convertMessage = (message: MyMessage): ThreadMessageLike => ({
role: message.role,
content: [{ type: "text", text: message.text }],
id: message.id,
createdAt: new Date(message.timestamp),
});

const onNew = async (message: AppendMessage) => {
// Extract text from AppendMessage content array
if (message.content.length !== 1 || message.content[0]?.type !== "text") {
throw new Error("Only text content is supported");
}

// Add user message in custom format
const userMessage: MyMessage = {
id: generateId(),
role: "user",
text: message.content[0].text,
timestamp: Date.now(),
};

// 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);

const assistantMessage: MyMessage = {
id: generateId(),
role: "assistant",
text: response.content,
timestamp: Date.now(),
};

// Update current thread with assistant response
await actions.addMessage(state.currentThreadId, assistantMessage);
} catch (error) {
// Handle errors gracefully
const errorMessage: MyMessage = {
id: generateId(),
role: "assistant",
text: `Sorry, I encountered an error: ${error instanceof Error ? error.message : 'Unknown error'}`,
timestamp: Date.now(),
};

await actions.addMessage(state.currentThreadId, errorMessage);
} finally {
setIsRunning(false);
}
};

// Add onEdit functionality
const onEdit = async (message: AppendMessage) => {
// Extract text from AppendMessage content array
if (message.content.length !== 1 || message.content[0]?.type !== "text") {
throw new Error("Only text content is supported");
}

// Find the index where to insert the edited message
const index = currentMessages.findIndex((m) => m.id === message.parentId) + 1;

// Keep messages up to the parent
const newMessages = [...currentMessages.slice(0, index)];

// Add the edited message in custom format
const editedMessage: MyMessage = {
id: generateId(),
role: "user",
text: message.content[0].text,
timestamp: Date.now(),
};
newMessages.push(editedMessage);

// 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 assistantMessage: MyMessage = {
id: generateId(),
role: "assistant",
text: response.content,
timestamp: Date.now(),
};

newMessages.push(assistantMessage);
await actions.updateMessages(state.currentThreadId, newMessages);
} catch (error) {
// Handle errors gracefully
const errorMessage: MyMessage = {
id: generateId(),
role: "assistant",
text: `Sorry, I encountered an error: ${error instanceof Error ? error.message : 'Unknown error'}`,
timestamp: Date.now(),
};

newMessages.push(errorMessage);
await actions.updateMessages(state.currentThreadId, newMessages);
} finally {
setIsRunning(false);
}
};

// Thread list adapter for managing multiple threads
const threadListAdapter: ExternalStoreThreadListAdapter = {
threadId: state.currentThreadId,
threads: state.threadList.filter((t): t is RegularThreadData => t.status === "regular"),
archivedThreads: state.threadList.filter((t): t is ArchivedThreadData => t.status === "archived"),

onSwitchToNewThread: async () => {
const threadId = await actions.createThread("New Chat");
actions.setCurrentThread(threadId);
},

onSwitchToThread: (threadId) => {
actions.setCurrentThread(threadId);
},

onRename: async (threadId, newTitle) => {
await actions.updateThread(threadId, { title: newTitle });
},

onArchive: async (threadId) => {
await actions.updateThread(threadId, { status: "archived" });
},

onDelete: async (threadId) => {
await actions.deleteThread(threadId);
},
};

const runtime = useExternalStoreRuntime({
messages: currentMessages,
setMessages: (messages) => {
actions.updateMessages(state.currentThreadId, messages);
},
convertMessage,
isRunning,
onNew,
onEdit,
adapters: {
threadList: threadListAdapter,
},
});

if (!state.isInitialized) {
return <div>Loading...</div>;
}

return (
<AssistantRuntimeProvider runtime={runtime}>
<ChatContainer>
<ThreadList />
<Thread />
</ChatContainer>
</AssistantRuntimeProvider>
);
}

Loading
Loading