feat: histories in global chat and roadmap chat (#8775)
* feat: ai chat history * wip * wip * wip * wip * wip * wip * wip * wip * wip: skeleton loading * wip * wip * wip * wip * wip * wip * wip * wip * fix: chat history * wip * wip * fix: responsiveness * wip * wip * Chat history UI * Update chat history * wip * Update chat history * Update chat history * Fix ai chat not working * Update * wip * feat: show chat history always * feat: upgrade to pro * wip * Update history design * UI design improvement for empty sidebar * feat: chat history title * Fix, delete chat throwing error * Plus icon when chat is closed * fix: action z-index * Improve skeleton and logged out user workflow * Chat history improvements * Add plus for chat icons --------- Co-authored-by: Arik Chakma <arikchangma@gmail.com>
This commit is contained in:
1 parent
7d91696c85
commit
0f99964bd6
32 files changed
+1995
-325
No files matched your search
@@ -16,6 +16,47 @@ import { RoadmapTopicList } from '../components/RoadmapAIChat/RoadmapTopicList';
|
||||
import { ShareResourceLink } from '../components/RoadmapAIChat/ShareResourceLink';
|
||||
import { RoadmapRecommendations } from '../components/RoadmapAIChat/RoadmapRecommendations';
|
||||
import type { AllowedAIChatRole } from '../components/GenerateCourse/AICourseLessonChat';
|
||||
import { readChatStream } from '../lib/chat';
|
||||
|
||||
type RoadmapAIChatRendererOptions = {
|
||||
totalTopicCount: number;
|
||||
roadmapId: string;
|
||||
onSelectTopic: (topicId: string, topicTitle: string) => void;
|
||||
};
|
||||
|
||||
export function roadmapAIChatRenderer(
|
||||
options: RoadmapAIChatRendererOptions,
|
||||
): Record<string, MessagePartRenderer> {
|
||||
const { totalTopicCount, roadmapId, onSelectTopic } = options;
|
||||
|
||||
return {
|
||||
'user-progress': () => (
|
||||
<UserProgressList
|
||||
totalTopicCount={totalTopicCount}
|
||||
roadmapId={roadmapId}
|
||||
/>
|
||||
),
|
||||
'update-progress': (opts) => (
|
||||
<UserProgressActionList roadmapId={roadmapId} {...opts} />
|
||||
),
|
||||
'roadmap-topics': (opts) => (
|
||||
<RoadmapTopicList
|
||||
roadmapId={roadmapId}
|
||||
onTopicClick={(topicId, text) => {
|
||||
const title = text.split(' > ').pop();
|
||||
if (!title) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelectTopic(topicId, title);
|
||||
}}
|
||||
{...opts}
|
||||
/>
|
||||
),
|
||||
'resource-progress-link': () => <ShareResourceLink roadmapId={roadmapId} />,
|
||||
'roadmap-recommendations': (opts) => <RoadmapRecommendations {...opts} />,
|
||||
};
|
||||
}
|
||||
|
||||
export type RoadmapAIChatHistoryType = {
|
||||
role: AllowedAIChatRole;
|
||||
@@ -27,14 +68,23 @@ export type RoadmapAIChatHistoryType = {
|
||||
};
|
||||
|
||||
type Options = {
|
||||
activeChatHistoryId?: string;
|
||||
roadmapId: string;
|
||||
totalTopicCount: number;
|
||||
scrollareaRef: React.RefObject<HTMLDivElement | null>;
|
||||
onSelectTopic: (topicId: string, topicTitle: string) => void;
|
||||
onChatHistoryIdChange?: (chatHistoryId: string) => void;
|
||||
};
|
||||
|
||||
export function useRoadmapAIChat(options: Options) {
|
||||
const { roadmapId, totalTopicCount, scrollareaRef, onSelectTopic } = options;
|
||||
const {
|
||||
activeChatHistoryId,
|
||||
roadmapId,
|
||||
totalTopicCount,
|
||||
scrollareaRef,
|
||||
onSelectTopic,
|
||||
onChatHistoryIdChange,
|
||||
} = options;
|
||||
const toast = useToast();
|
||||
|
||||
const [aiChatHistory, setAiChatHistory] = useState<
|
||||
@@ -99,33 +149,7 @@ export function useRoadmapAIChat(options: Options) {
|
||||
}, [isStreamingMessage, streamedMessage, scrollToBottom]);
|
||||
|
||||
const renderer: Record<string, MessagePartRenderer> = useMemo(
|
||||
() => ({
|
||||
'user-progress': () => (
|
||||
<UserProgressList
|
||||
totalTopicCount={totalTopicCount}
|
||||
roadmapId={roadmapId}
|
||||
/>
|
||||
),
|
||||
'update-progress': (opts) => (
|
||||
<UserProgressActionList roadmapId={roadmapId} {...opts} />
|
||||
),
|
||||
'roadmap-topics': (opts) => (
|
||||
<RoadmapTopicList
|
||||
roadmapId={roadmapId}
|
||||
onTopicClick={(topicId, text) => {
|
||||
const title = text.split(' > ').pop();
|
||||
if (title) {
|
||||
onSelectTopic(topicId, title);
|
||||
}
|
||||
}}
|
||||
{...opts}
|
||||
/>
|
||||
),
|
||||
'resource-progress-link': () => (
|
||||
<ShareResourceLink roadmapId={roadmapId} />
|
||||
),
|
||||
'roadmap-recommendations': (opts) => <RoadmapRecommendations {...opts} />,
|
||||
}),
|
||||
() => roadmapAIChatRenderer({ roadmapId, totalTopicCount, onSelectTopic }),
|
||||
[roadmapId, onSelectTopic, totalTopicCount],
|
||||
);
|
||||
|
||||
@@ -141,7 +165,13 @@ export function useRoadmapAIChat(options: Options) {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
signal: abortController?.signal,
|
||||
body: JSON.stringify({ roadmapId, messages: messages.slice(-10) }),
|
||||
body: JSON.stringify({
|
||||
roadmapId,
|
||||
messages,
|
||||
...(activeChatHistoryId
|
||||
? { chatHistoryId: activeChatHistoryId }
|
||||
: {}),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -158,23 +188,31 @@ export function useRoadmapAIChat(options: Options) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
const stream = response.body;
|
||||
if (!stream) {
|
||||
setIsStreamingMessage(false);
|
||||
toast.error('Something went wrong');
|
||||
return;
|
||||
}
|
||||
|
||||
await readStream(reader, {
|
||||
onStream: async (content) => {
|
||||
if (abortController?.signal.aborted) return;
|
||||
await readChatStream(stream, {
|
||||
onMessage: async (content) => {
|
||||
if (abortController?.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jsx = await renderMessage(content, renderer, {
|
||||
isLoading: true,
|
||||
});
|
||||
flushSync(() => setStreamedMessage(jsx));
|
||||
flushSync(() => {
|
||||
setStreamedMessage(jsx);
|
||||
});
|
||||
},
|
||||
onStreamEnd: async (content) => {
|
||||
if (abortController?.signal.aborted) return;
|
||||
onMessageEnd: async (content) => {
|
||||
if (abortController?.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jsx = await renderMessage(content, renderer, {
|
||||
isLoading: false,
|
||||
});
|
||||
@@ -188,6 +226,24 @@ export function useRoadmapAIChat(options: Options) {
|
||||
setAiChatHistory(newMessages);
|
||||
});
|
||||
queryClient.invalidateQueries(getAiCourseLimitOptions());
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (query) => {
|
||||
return (
|
||||
query.queryKey[0] === 'list-chat-history' &&
|
||||
(query.queryKey[1] as { roadmapId: string })?.roadmapId ===
|
||||
roadmapId
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
onDetails: (details) => {
|
||||
const detailsJson = JSON.parse(details);
|
||||
const chatHistoryId = detailsJson?.chatHistoryId;
|
||||
if (!chatHistoryId) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChatHistoryIdChange?.(chatHistoryId);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -256,10 +312,11 @@ export function useRoadmapAIChat(options: Options) {
|
||||
handleAbort,
|
||||
clearChat,
|
||||
scrollToBottom,
|
||||
setAiChatHistory,
|
||||
};
|
||||
}
|
||||
|
||||
function htmlFromTiptapJSON(json: JSONContent): string {
|
||||
export function htmlFromTiptapJSON(json: JSONContent): string {
|
||||
const content = json.content;
|
||||
let text = '';
|
||||
for (const child of content || []) {
|
||||
|
||||
Reference in new issue
Block a user