feat: ai roadmaps and chat (#8821)
* wip * wip * wip * wip * wip * wip * wip * wip * wip: ai roadmap * wip * wip * Add question answers * Update content generator * Update question answer chat * Refresh button not working * UI for chat * Update question answers functionality * wip * wip * fix: ai roadmap regenerate * Improve preferences * feat: roadmap actions * wip * fix: hydration error * Fix responsiveness issue * fix: show preferences option only when questions exist * fix: infinite loop issue * feat: node click message populate * Improve UI * wip * wip * fix: roadmap regenerate * Update UI * Update placeholder * Update answers * Update navigations --------- Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
This commit is contained in:
1 parent
08e8b83262
commit
e6d10ef592
53 files changed
+2933
-949
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
import { AlertCircleIcon, ExternalLink } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { generateGuide } from '../../helper/generate-ai-guide';
|
||||
@@ -16,6 +16,9 @@ import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
|
||||
import { AIGuideChat } from './AIGuideChat';
|
||||
import { AIGuideContent } from './AIGuideContent';
|
||||
import { GenerateAIGuide } from './GenerateAIGuide';
|
||||
import { getAiCourseLimitOptions } from '../../queries/ai-course';
|
||||
import { billingDetailsOptions } from '../../queries/billing';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
|
||||
type AIGuideProps = {
|
||||
guideSlug?: string;
|
||||
@@ -32,10 +35,23 @@ export function AIGuide(props: AIGuideProps) {
|
||||
|
||||
// only fetch the guide if the guideSlug is provided
|
||||
// otherwise we are still generating the guide
|
||||
const { data: aiGuide, isLoading: isLoadingBySlug } = useQuery(
|
||||
getAiGuideOptions(guideSlug),
|
||||
queryClient,
|
||||
);
|
||||
const {
|
||||
data: aiGuide,
|
||||
isLoading: isLoadingBySlug,
|
||||
error: aiGuideError,
|
||||
} = useQuery(getAiGuideOptions(guideSlug), queryClient);
|
||||
|
||||
const {
|
||||
data: tokenUsage,
|
||||
isLoading: isTokenUsageLoading,
|
||||
refetch: refetchTokenUsage,
|
||||
} = useQuery(getAiCourseLimitOptions(), queryClient);
|
||||
|
||||
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
|
||||
useQuery(billingDetailsOptions(), queryClient);
|
||||
|
||||
const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
|
||||
const isPaidUser = userBillingDetails?.status === 'active';
|
||||
|
||||
const { data: aiGuideSuggestions, isLoading: isAiGuideSuggestionsLoading } =
|
||||
useQuery(
|
||||
@@ -57,6 +73,16 @@ export function AIGuide(props: AIGuideProps) {
|
||||
}, [aiGuideSuggestions]);
|
||||
|
||||
const handleRegenerate = async (prompt?: string) => {
|
||||
if (!isLoggedIn()) {
|
||||
showLoginPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPaidUser && isLimitExceeded) {
|
||||
setShowUpgradeModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
flushSync(() => {
|
||||
setIsRegenerating(true);
|
||||
setRegeneratedHtml(null);
|
||||
@@ -78,7 +104,6 @@ export function AIGuide(props: AIGuideProps) {
|
||||
await generateGuide({
|
||||
slug: aiGuide?.slug || '',
|
||||
term: aiGuide?.keyword || '',
|
||||
depth: aiGuide?.depth || '',
|
||||
prompt,
|
||||
onStreamingChange: setIsRegenerating,
|
||||
onHtmlChange: setRegeneratedHtml,
|
||||
@@ -93,24 +118,44 @@ export function AIGuide(props: AIGuideProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
isLoadingBySlug ||
|
||||
isRegenerating ||
|
||||
isTokenUsageLoading ||
|
||||
isBillingDetailsLoading;
|
||||
|
||||
return (
|
||||
<AITutorLayout
|
||||
wrapperClassName="flex-row p-0 lg:p-0 overflow-hidden bg-white"
|
||||
wrapperClassName="flex-row p-0 lg:p-0 relative overflow-hidden bg-white"
|
||||
containerClassName="h-[calc(100vh-49px)] overflow-hidden relative"
|
||||
>
|
||||
{showUpgradeModal && (
|
||||
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
|
||||
)}
|
||||
|
||||
{!isLoading && aiGuideError && (
|
||||
<div className="absolute inset-0 z-10 flex h-full flex-col items-center justify-center bg-white">
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<AlertCircleIcon className="size-10 text-gray-500" />
|
||||
<p className="text-center">
|
||||
{aiGuideError?.message || 'Something went wrong'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grow overflow-y-auto p-4 pt-0">
|
||||
{guideSlug && (
|
||||
{guideSlug && !aiGuideError && (
|
||||
<AIGuideContent
|
||||
html={regeneratedHtml || aiGuide?.html || ''}
|
||||
onRegenerate={handleRegenerate}
|
||||
isLoading={isLoadingBySlug || isRegenerating}
|
||||
isLoading={isLoading}
|
||||
guideSlug={guideSlug}
|
||||
/>
|
||||
)}
|
||||
{!guideSlug && <GenerateAIGuide onGuideSlugChange={setGuideSlug} />}
|
||||
{!guideSlug && !aiGuideError && (
|
||||
<GenerateAIGuide onGuideSlugChange={setGuideSlug} />
|
||||
)}
|
||||
|
||||
{aiGuide && !isRegenerating && (
|
||||
<div className="mx-auto mt-12 mb-12 max-w-4xl">
|
||||
|
||||
@@ -60,11 +60,8 @@ export function AIGuideChat(props: AIGuideChatProps) {
|
||||
refetch: refetchTokenUsage,
|
||||
} = useQuery(getAiCourseLimitOptions(), queryClient);
|
||||
|
||||
const {
|
||||
data: userBillingDetails,
|
||||
isLoading: isBillingDetailsLoading,
|
||||
refetch: refetchBillingDetails,
|
||||
} = useQuery(billingDetailsOptions(), queryClient);
|
||||
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
|
||||
useQuery(billingDetailsOptions(), queryClient);
|
||||
|
||||
const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
|
||||
const isPaidUser = userBillingDetails?.status === 'active';
|
||||
@@ -127,6 +124,10 @@ export function AIGuideChat(props: AIGuideChatProps) {
|
||||
});
|
||||
sendMessages(newMessages);
|
||||
setInputValue('');
|
||||
|
||||
setTimeout(() => {
|
||||
scrollToBottom('smooth');
|
||||
}, 0);
|
||||
},
|
||||
[inputValue, isStreamingMessage, messages, sendMessages, setMessages],
|
||||
);
|
||||
@@ -332,6 +333,24 @@ export function AIGuideChat(props: AIGuideChatProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoggedIn() && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center gap-2 bg-black text-white">
|
||||
<LockIcon
|
||||
className="size-4 cursor-not-allowed"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
<p className="cursor-not-allowed">Please login to continue</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
showLoginPopup();
|
||||
}}
|
||||
className="rounded-md bg-white px-2 py-1 text-xs font-medium text-black hover:bg-gray-300"
|
||||
>
|
||||
Login / Register
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
|
||||
@@ -2,15 +2,17 @@ import './AIGuideContent.css';
|
||||
import { AIGuideRegenerate } from './AIGuideRegenerate';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { LoadingChip } from '../LoadingChip';
|
||||
import type { QuestionAnswerChatMessage } from '../ContentGenerator/QuestionAnswerChat';
|
||||
|
||||
type AIGuideContentProps = {
|
||||
html: string;
|
||||
onRegenerate?: (prompt?: string) => void;
|
||||
isLoading?: boolean;
|
||||
guideSlug?: string;
|
||||
};
|
||||
|
||||
export function AIGuideContent(props: AIGuideContentProps) {
|
||||
const { html, onRegenerate, isLoading } = props;
|
||||
const { html, onRegenerate, isLoading, guideSlug } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -30,9 +32,12 @@ export function AIGuideContent(props: AIGuideContentProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{onRegenerate && !isLoading && (
|
||||
{onRegenerate && !isLoading && guideSlug && (
|
||||
<div className="absolute top-4 right-4">
|
||||
<AIGuideRegenerate onRegenerate={onRegenerate} />
|
||||
<AIGuideRegenerate
|
||||
onRegenerate={onRegenerate}
|
||||
guideSlug={guideSlug}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,74 @@
|
||||
import { PenSquare, RefreshCcw } from 'lucide-react';
|
||||
import { PenSquare, RefreshCcw, SettingsIcon } from 'lucide-react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
|
||||
import { ModifyCoursePrompt } from '../GenerateCourse/ModifyCoursePrompt';
|
||||
import { UpdatePreferences } from './UpdatePreferences';
|
||||
import type { QuestionAnswerChatMessage } from '../ContentGenerator/QuestionAnswerChat';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { getAiGuideOptions } from '../../queries/ai-guide';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
import { httpPost } from '../../lib/query-http';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
|
||||
type AIGuideRegenerateProps = {
|
||||
onRegenerate: (prompt?: string) => void;
|
||||
guideSlug: string;
|
||||
};
|
||||
|
||||
export function AIGuideRegenerate(props: AIGuideRegenerateProps) {
|
||||
const { onRegenerate } = props;
|
||||
const { onRegenerate, guideSlug } = props;
|
||||
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
||||
const [showPromptModal, setShowPromptModal] = useState(false);
|
||||
const [showUpdatePreferencesModal, setShowUpdatePreferencesModal] =
|
||||
useState(false);
|
||||
const currentUser = useAuth();
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOutsideClick(ref, () => setIsDropdownVisible(false));
|
||||
|
||||
const { data: aiGuide } = useQuery(getAiGuideOptions(guideSlug), queryClient);
|
||||
const { mutate: updatePreferences, isPending: isUpdating } = useMutation(
|
||||
{
|
||||
mutationFn: (questionAndAnswers: QuestionAnswerChatMessage[]) => {
|
||||
return httpPost(`/v1-update-guide-preferences/${guideSlug}`, {
|
||||
questionAndAnswers,
|
||||
});
|
||||
},
|
||||
onSuccess: (_, vars) => {
|
||||
queryClient.setQueryData(
|
||||
getAiGuideOptions(guideSlug).queryKey,
|
||||
(old) => {
|
||||
if (!old) {
|
||||
return old;
|
||||
}
|
||||
|
||||
return {
|
||||
...old,
|
||||
questionAndAnswers: vars,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
setShowUpdatePreferencesModal(false);
|
||||
setIsDropdownVisible(false);
|
||||
onRegenerate();
|
||||
},
|
||||
},
|
||||
queryClient,
|
||||
);
|
||||
|
||||
const showUpdatePreferences =
|
||||
aiGuide?.questionAndAnswers &&
|
||||
aiGuide.questionAndAnswers.length > 0 &&
|
||||
currentUser?.id === aiGuide.userId;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showUpgradeModal && (
|
||||
@@ -41,6 +90,19 @@ export function AIGuideRegenerate(props: AIGuideRegenerateProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{showUpdatePreferencesModal && (
|
||||
<UpdatePreferences
|
||||
onClose={() => setShowUpdatePreferencesModal(false)}
|
||||
questionAndAnswers={aiGuide?.questionAndAnswers}
|
||||
term={aiGuide?.keyword || ''}
|
||||
format="guide"
|
||||
onUpdatePreferences={(questionAndAnswers) => {
|
||||
updatePreferences(questionAndAnswers);
|
||||
}}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div ref={ref} className="relative flex items-stretch">
|
||||
<button
|
||||
className={cn('rounded-md px-2.5 text-gray-400 hover:text-black', {
|
||||
@@ -51,9 +113,31 @@ export function AIGuideRegenerate(props: AIGuideRegenerateProps) {
|
||||
<PenSquare className="text-current" size={16} strokeWidth={2.5} />
|
||||
</button>
|
||||
{isDropdownVisible && (
|
||||
<div className="absolute top-full right-0 min-w-[170px] translate-y-1 overflow-hidden rounded-md border border-gray-200 bg-white shadow-md">
|
||||
<div className="absolute top-full right-0 min-w-[190px] translate-y-1 overflow-hidden rounded-md border border-gray-200 bg-white shadow-md">
|
||||
{showUpdatePreferences && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDropdownVisible(false);
|
||||
setShowUpdatePreferencesModal(true);
|
||||
}}
|
||||
className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-100"
|
||||
>
|
||||
<SettingsIcon
|
||||
size={16}
|
||||
className="text-gray-400"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
Update Preferences
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!isLoggedIn()) {
|
||||
showLoginPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDropdownVisible(false);
|
||||
onRegenerate();
|
||||
}}
|
||||
@@ -68,6 +152,11 @@ export function AIGuideRegenerate(props: AIGuideRegenerateProps) {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!isLoggedIn()) {
|
||||
showLoginPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDropdownVisible(false);
|
||||
setShowPromptModal(true);
|
||||
}}
|
||||
|
||||
@@ -7,6 +7,8 @@ import { AIGuideContent } from './AIGuideContent';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
import { getAiGuideOptions } from '../../queries/ai-guide';
|
||||
import { LoadingChip } from '../LoadingChip';
|
||||
import type { QuestionAnswerChatMessage } from '../ContentGenerator/QuestionAnswerChat';
|
||||
import { getQuestionAnswerChatMessages } from '../../lib/ai-questions';
|
||||
|
||||
type GenerateAIGuideProps = {
|
||||
onGuideSlugChange?: (guideSlug: string) => void;
|
||||
@@ -26,48 +28,32 @@ export function GenerateAIGuide(props: GenerateAIGuideProps) {
|
||||
useEffect(() => {
|
||||
const params = getUrlParams();
|
||||
const paramsTerm = params?.term;
|
||||
const paramsDepth = params?.depth;
|
||||
const paramsSrc = params?.src || 'search';
|
||||
if (!paramsTerm || !paramsDepth) {
|
||||
if (!paramsTerm) {
|
||||
return;
|
||||
}
|
||||
|
||||
let paramsGoal = '';
|
||||
let paramsAbout = '';
|
||||
let paramsCustomInstructions = '';
|
||||
|
||||
let questionAndAnswers: QuestionAnswerChatMessage[] = [];
|
||||
const sessionId = params?.id;
|
||||
if (sessionId) {
|
||||
const fineTuneData = getCourseFineTuneData(sessionId);
|
||||
if (fineTuneData) {
|
||||
paramsGoal = fineTuneData.goal;
|
||||
paramsAbout = fineTuneData.about;
|
||||
paramsCustomInstructions = fineTuneData.customInstructions;
|
||||
}
|
||||
questionAndAnswers = getQuestionAnswerChatMessages(sessionId);
|
||||
}
|
||||
|
||||
handleGenerateDocument({
|
||||
term: paramsTerm,
|
||||
depth: paramsDepth,
|
||||
instructions: paramsCustomInstructions,
|
||||
goal: paramsGoal,
|
||||
about: paramsAbout,
|
||||
src: paramsSrc,
|
||||
questionAndAnswers,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleGenerateDocument = async (options: {
|
||||
term: string;
|
||||
depth: string;
|
||||
instructions?: string;
|
||||
goal?: string;
|
||||
about?: string;
|
||||
isForce?: boolean;
|
||||
prompt?: string;
|
||||
src?: string;
|
||||
questionAndAnswers?: QuestionAnswerChatMessage[];
|
||||
}) => {
|
||||
const { term, depth, isForce, prompt, instructions, goal, about, src } =
|
||||
options;
|
||||
const { term, isForce, prompt, src, questionAndAnswers } = options;
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
window.location.href = '/ai';
|
||||
@@ -76,7 +62,6 @@ export function GenerateAIGuide(props: GenerateAIGuideProps) {
|
||||
|
||||
await generateGuide({
|
||||
term,
|
||||
depth,
|
||||
onDetailsChange: (details) => {
|
||||
const { guideId, guideSlug, creatorId, title } = details;
|
||||
|
||||
@@ -86,7 +71,6 @@ export function GenerateAIGuide(props: GenerateAIGuideProps) {
|
||||
title,
|
||||
html: htmlRef.current,
|
||||
keyword: term,
|
||||
depth,
|
||||
content,
|
||||
tokens: {
|
||||
prompt: 0,
|
||||
@@ -96,6 +80,7 @@ export function GenerateAIGuide(props: GenerateAIGuideProps) {
|
||||
relatedTopics: [],
|
||||
deepDiveTopics: [],
|
||||
questions: [],
|
||||
questionAndAnswers,
|
||||
viewCount: 0,
|
||||
lastVisitedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
@@ -112,12 +97,10 @@ export function GenerateAIGuide(props: GenerateAIGuideProps) {
|
||||
},
|
||||
onLoadingChange: setIsLoading,
|
||||
onError: setError,
|
||||
instructions,
|
||||
goal,
|
||||
about,
|
||||
isForce,
|
||||
prompt,
|
||||
src,
|
||||
questionAndAnswers,
|
||||
onHtmlChange: (html) => {
|
||||
htmlRef.current = html;
|
||||
setHtml(html);
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { AllowedFormat } from '../ContentGenerator/ContentGenerator';
|
||||
import {
|
||||
QuestionAnswerChat,
|
||||
type QuestionAnswerChatMessage,
|
||||
} from '../ContentGenerator/QuestionAnswerChat';
|
||||
import { Modal } from '../Modal';
|
||||
|
||||
type UpdatePreferencesProps = {
|
||||
onClose: () => void;
|
||||
term: string;
|
||||
format: AllowedFormat;
|
||||
questionAndAnswers?: QuestionAnswerChatMessage[];
|
||||
onUpdatePreferences: (
|
||||
questionAndAnswers: QuestionAnswerChatMessage[],
|
||||
) => void;
|
||||
isUpdating: boolean;
|
||||
};
|
||||
|
||||
export function UpdatePreferences(props: UpdatePreferencesProps) {
|
||||
const {
|
||||
onClose,
|
||||
questionAndAnswers: defaultQuestionAndAnswers,
|
||||
term,
|
||||
format,
|
||||
onUpdatePreferences,
|
||||
isUpdating,
|
||||
} = props;
|
||||
|
||||
const [questionAnswerChatMessages, setQuestionAnswerChatMessages] = useState<
|
||||
QuestionAnswerChatMessage[]
|
||||
>(defaultQuestionAndAnswers || []);
|
||||
|
||||
const defaultQuestions = defaultQuestionAndAnswers
|
||||
?.filter((message) => message.role === 'assistant')
|
||||
.map((message) => ({
|
||||
question: message.question,
|
||||
possibleAnswers: message.possibleAnswers,
|
||||
}));
|
||||
|
||||
const hasChangedQuestionAndAnswers = useMemo(() => {
|
||||
return (
|
||||
JSON.stringify(questionAnswerChatMessages) !==
|
||||
JSON.stringify(defaultQuestionAndAnswers)
|
||||
);
|
||||
}, [questionAnswerChatMessages, defaultQuestionAndAnswers]);
|
||||
|
||||
console.log(questionAnswerChatMessages);
|
||||
console.log(defaultQuestionAndAnswers);
|
||||
|
||||
const userAnswers = questionAnswerChatMessages.filter(
|
||||
(message) => message.role === 'user',
|
||||
);
|
||||
|
||||
const hasAnsweredAllQuestions =
|
||||
userAnswers.length === defaultQuestions?.length;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={onClose}
|
||||
bodyClassName="p-4 flex flex-col gap-4"
|
||||
wrapperClassName="max-w-xl h-auto"
|
||||
overlayClassName="items-start md:items-center"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="text-lg font-medium">Update Preferences</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
Update your preferences for better content
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<QuestionAnswerChat
|
||||
term={term}
|
||||
format={format}
|
||||
questionAnswerChatMessages={questionAnswerChatMessages}
|
||||
setQuestionAnswerChatMessages={setQuestionAnswerChatMessages}
|
||||
defaultQuestions={defaultQuestions}
|
||||
onGenerateNow={() => {
|
||||
onUpdatePreferences(questionAnswerChatMessages);
|
||||
}}
|
||||
className="-mx-2 h-[400px] border-none p-0"
|
||||
type="update"
|
||||
/>
|
||||
|
||||
{hasChangedQuestionAndAnswers && hasAnsweredAllQuestions && (
|
||||
<button
|
||||
className="rounded-lg bg-black px-4 py-2 text-white hover:opacity-80 disabled:opacity-50"
|
||||
disabled={isUpdating || !hasChangedQuestionAndAnswers}
|
||||
onClick={() => {
|
||||
onUpdatePreferences(questionAnswerChatMessages);
|
||||
}}
|
||||
>
|
||||
{isUpdating ? 'Updating...' : 'Apply preferences'}
|
||||
</button>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user