Skip to content

Commit 52cdcf2

Browse files
committed
add threads logic
1 parent 22934c6 commit 52cdcf2

File tree

4 files changed

+303
-11
lines changed

4 files changed

+303
-11
lines changed
Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,13 @@
11
// client/packages/lowcoder/src/comps/comps/chatComp/chatView.tsx
22
import React from "react";
33
import { ChatCompProps } from "./chatCompTypes";
4-
import { Thread } from "./components/assistant-ui/thread";
4+
import { ChatApp } from "./components/ChatWithThreads";
55

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

9-
10-
import { MyRuntimeProvider } from "./components/context/MyRuntimeProvider";
11-
12-
13-
149
export const ChatView = React.memo((props: ChatCompProps) => {
15-
return (
16-
<MyRuntimeProvider>
17-
<Thread />
18-
</MyRuntimeProvider>
19-
);
10+
return <ChatApp />;
2011
});
2112

2213
ChatView.displayName = 'ChatView';
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import React, { useState } from "react";
2+
import {
3+
useExternalStoreRuntime,
4+
ThreadMessageLike,
5+
AppendMessage,
6+
AssistantRuntimeProvider,
7+
ExternalStoreThreadListAdapter,
8+
} from "@assistant-ui/react";
9+
import { useThreadContext, MyMessage, ThreadProvider } from "./context/ThreadContext";
10+
import { Thread } from "./assistant-ui/thread";
11+
import { ThreadList } from "./assistant-ui/thread-list";
12+
13+
// Define thread data interfaces to match ExternalStoreThreadData requirements
14+
interface RegularThreadData {
15+
threadId: string;
16+
status: "regular";
17+
title: string;
18+
}
19+
20+
interface ArchivedThreadData {
21+
threadId: string;
22+
status: "archived";
23+
title: string;
24+
}
25+
26+
type ThreadData = RegularThreadData | ArchivedThreadData;
27+
28+
const generateId = () => Math.random().toString(36).substr(2, 9);
29+
30+
const callYourAPI = async (text: string) => {
31+
// Simulate API delay
32+
await new Promise(resolve => setTimeout(resolve, 1500));
33+
34+
// Simple responses
35+
return {
36+
content: "This is a mock response from your backend. You typed: " + text
37+
};
38+
};
39+
40+
function ChatWithThreads() {
41+
const { currentThreadId, setCurrentThreadId, threads, setThreads } =
42+
useThreadContext();
43+
const [isRunning, setIsRunning] = useState(false);
44+
const [threadList, setThreadList] = useState<ThreadData[]>([
45+
{ threadId: "default", status: "regular", title: "New Chat" } as RegularThreadData,
46+
]);
47+
48+
// Get messages for current thread
49+
const currentMessages = threads.get(currentThreadId) || [];
50+
51+
// Convert custom format to ThreadMessageLike
52+
const convertMessage = (message: MyMessage): ThreadMessageLike => ({
53+
role: message.role,
54+
content: [{ type: "text", text: message.text }],
55+
id: message.id,
56+
createdAt: new Date(message.timestamp),
57+
});
58+
59+
const onNew = async (message: AppendMessage) => {
60+
// Extract text from AppendMessage content array
61+
if (message.content.length !== 1 || message.content[0]?.type !== "text") {
62+
throw new Error("Only text content is supported");
63+
}
64+
65+
// Add user message in custom format
66+
const userMessage: MyMessage = {
67+
id: generateId(),
68+
role: "user",
69+
text: message.content[0].text,
70+
timestamp: Date.now(),
71+
};
72+
73+
// Update current thread with new user message
74+
const updatedMessages = [...currentMessages, userMessage];
75+
setThreads(prev => new Map(prev).set(currentThreadId, updatedMessages));
76+
setIsRunning(true);
77+
78+
try {
79+
// Call mock API
80+
const response = await callYourAPI(userMessage.text);
81+
82+
const assistantMessage: MyMessage = {
83+
id: generateId(),
84+
role: "assistant",
85+
text: response.content,
86+
timestamp: Date.now(),
87+
};
88+
89+
// Update current thread with assistant response
90+
const finalMessages = [...updatedMessages, assistantMessage];
91+
setThreads(prev => new Map(prev).set(currentThreadId, finalMessages));
92+
} catch (error) {
93+
// Handle errors gracefully
94+
const errorMessage: MyMessage = {
95+
id: generateId(),
96+
role: "assistant",
97+
text: `Sorry, I encountered an error: ${error instanceof Error ? error.message : 'Unknown error'}`,
98+
timestamp: Date.now(),
99+
};
100+
101+
const finalMessages = [...updatedMessages, errorMessage];
102+
setThreads(prev => new Map(prev).set(currentThreadId, finalMessages));
103+
} finally {
104+
setIsRunning(false);
105+
}
106+
};
107+
108+
// Add onEdit functionality
109+
const onEdit = async (message: AppendMessage) => {
110+
// Extract text from AppendMessage content array
111+
if (message.content.length !== 1 || message.content[0]?.type !== "text") {
112+
throw new Error("Only text content is supported");
113+
}
114+
115+
// Find the index where to insert the edited message
116+
const index = currentMessages.findIndex((m) => m.id === message.parentId) + 1;
117+
118+
// Keep messages up to the parent
119+
const newMessages = [...currentMessages.slice(0, index)];
120+
121+
// Add the edited message in custom format
122+
const editedMessage: MyMessage = {
123+
id: generateId(),
124+
role: "user",
125+
text: message.content[0].text,
126+
timestamp: Date.now(),
127+
};
128+
newMessages.push(editedMessage);
129+
130+
setThreads(prev => new Map(prev).set(currentThreadId, newMessages));
131+
setIsRunning(true);
132+
133+
try {
134+
// Generate new response
135+
const response = await callYourAPI(editedMessage.text);
136+
137+
const assistantMessage: MyMessage = {
138+
id: generateId(),
139+
role: "assistant",
140+
text: response.content,
141+
timestamp: Date.now(),
142+
};
143+
144+
newMessages.push(assistantMessage);
145+
setThreads(prev => new Map(prev).set(currentThreadId, newMessages));
146+
} catch (error) {
147+
// Handle errors gracefully
148+
const errorMessage: MyMessage = {
149+
id: generateId(),
150+
role: "assistant",
151+
text: `Sorry, I encountered an error: ${error instanceof Error ? error.message : 'Unknown error'}`,
152+
timestamp: Date.now(),
153+
};
154+
155+
newMessages.push(errorMessage);
156+
setThreads(prev => new Map(prev).set(currentThreadId, newMessages));
157+
} finally {
158+
setIsRunning(false);
159+
}
160+
};
161+
162+
// Thread list adapter for managing multiple threads
163+
const threadListAdapter: ExternalStoreThreadListAdapter = {
164+
threadId: currentThreadId,
165+
threads: threadList.filter((t): t is RegularThreadData => t.status === "regular"),
166+
archivedThreads: threadList.filter((t): t is ArchivedThreadData => t.status === "archived"),
167+
168+
onSwitchToNewThread: () => {
169+
const newId = `thread-${Date.now()}`;
170+
setThreadList((prev) => [
171+
...prev,
172+
{
173+
threadId: newId,
174+
status: "regular",
175+
title: "New Chat",
176+
} as RegularThreadData,
177+
]);
178+
setThreads((prev) => new Map(prev).set(newId, []));
179+
setCurrentThreadId(newId);
180+
},
181+
182+
onSwitchToThread: (threadId) => {
183+
setCurrentThreadId(threadId);
184+
},
185+
186+
onRename: (threadId, newTitle) => {
187+
setThreadList((prev) =>
188+
prev.map((t) =>
189+
t.threadId === threadId ? { ...t, title: newTitle } : t,
190+
),
191+
);
192+
},
193+
194+
onArchive: (threadId) => {
195+
setThreadList((prev) =>
196+
prev.map((t) =>
197+
t.threadId === threadId ? { ...t, status: "archived" } : t,
198+
),
199+
);
200+
},
201+
202+
onDelete: (threadId) => {
203+
setThreadList((prev) => prev.filter((t) => t.threadId !== threadId));
204+
setThreads((prev) => {
205+
const next = new Map(prev);
206+
next.delete(threadId);
207+
return next;
208+
});
209+
if (currentThreadId === threadId) {
210+
setCurrentThreadId("default");
211+
}
212+
},
213+
};
214+
215+
const runtime = useExternalStoreRuntime({
216+
messages: currentMessages,
217+
setMessages: (messages) => {
218+
setThreads((prev) => new Map(prev).set(currentThreadId, messages));
219+
},
220+
convertMessage,
221+
isRunning,
222+
onNew,
223+
onEdit,
224+
adapters: {
225+
threadList: threadListAdapter,
226+
},
227+
});
228+
229+
return (
230+
<AssistantRuntimeProvider runtime={runtime}>
231+
<ThreadList />
232+
<Thread />
233+
</AssistantRuntimeProvider>
234+
);
235+
}
236+
237+
// Main App component with proper context wrapping
238+
export function ChatApp() {
239+
return (
240+
<ThreadProvider>
241+
<ChatWithThreads />
242+
</ThreadProvider>
243+
);
244+
}
245+
246+
export { ChatWithThreads };

client/packages/lowcoder/src/comps/comps/chatComp/components/context/MyRuntimeProvider.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
ThreadMessageLike,
55
AppendMessage,
66
AssistantRuntimeProvider,
7+
ExternalStoreThreadData,
8+
ExternalStoreThreadListAdapter,
79
} from "@assistant-ui/react";
810

911
// Define your custom message type
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React, { createContext, useContext, useState, ReactNode } from "react";
2+
3+
// Define thread-specific message type
4+
interface MyMessage {
5+
id: string;
6+
role: "user" | "assistant";
7+
text: string;
8+
timestamp: number;
9+
}
10+
11+
// Thread context type
12+
interface ThreadContextType {
13+
currentThreadId: string;
14+
setCurrentThreadId: (id: string) => void;
15+
threads: Map<string, MyMessage[]>;
16+
setThreads: React.Dispatch<React.SetStateAction<Map<string, MyMessage[]>>>;
17+
}
18+
19+
// Create the context
20+
const ThreadContext = createContext<ThreadContextType>({
21+
currentThreadId: "default",
22+
setCurrentThreadId: () => {},
23+
threads: new Map(),
24+
setThreads: () => {},
25+
});
26+
27+
// Thread provider component
28+
export function ThreadProvider({ children }: { children: ReactNode }) {
29+
const [threads, setThreads] = useState<Map<string, MyMessage[]>>(
30+
new Map([["default", []]]),
31+
);
32+
const [currentThreadId, setCurrentThreadId] = useState("default");
33+
34+
return (
35+
<ThreadContext.Provider
36+
value={{ currentThreadId, setCurrentThreadId, threads, setThreads }}
37+
>
38+
{children}
39+
</ThreadContext.Provider>
40+
);
41+
}
42+
43+
// Hook for accessing thread context
44+
export function useThreadContext() {
45+
const context = useContext(ThreadContext);
46+
if (!context) {
47+
throw new Error("useThreadContext must be used within ThreadProvider");
48+
}
49+
return context;
50+
}
51+
52+
// Export the MyMessage type for use in other files
53+
export type { MyMessage };

0 commit comments

Comments
 (0)