Files
ERitors-Scribe-Desktop/components/ScribeControllerBar.tsx
natreex ac95e00127 Integrate offline support and improve error handling across app
- Add `OfflineContext` to manage offline state and interactions within components.
- Refactor session logic in `ScribeControllerBar` and `page.tsx` to handle offline scenarios (e.g., check connectivity before enabling GPT features).
- Enhance offline PIN setup and verification with better flow and error messaging.
- Optimize database IPC handlers to initialize and sync data in offline mode.
- Refactor code to clean up redundant logs and ensure stricter typings.
- Improve consistency and structure in handling online and offline operations for smoother user experience.
2025-11-26 15:25:53 -05:00

186 lines
9.0 KiB
TypeScript

import {useContext, useState} from "react";
import {ChapterProps, chapterVersions} from "@/lib/models/Chapter";
import {ChapterContext} from "@/context/ChapterContext";
import {BookContext} from "@/context/BookContext";
import System from "@/lib/models/System";
import UserMenu from "@/components/UserMenu";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faGear, faGlobe, faHome} from "@fortawesome/free-solid-svg-icons";
import {SelectBoxProps} from "@/shared/interface";
import {AlertContext} from "@/context/AlertContext";
import {SessionContext} from "@/context/SessionContext";
import Book, {BookListProps} from "@/lib/models/Book";
import Modal from "@/components/Modal";
import BookSetting from "@/components/book/settings/BookSetting";
import SelectBox from "@/components/form/SelectBox";
import {useTranslations} from "next-intl";
import {LangContext, LangContextProps} from "@/context/LangContext";
import CreditCounter from "@/components/CreditMeters";
import QuillSense from "@/lib/models/QuillSense";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
export default function ScribeControllerBar() {
const {chapter, setChapter} = useContext(ChapterContext);
const {book, setBook} = useContext(BookContext);
const {errorMessage} = useContext(AlertContext)
const {session} = useContext(SessionContext);
const t = useTranslations();
const {lang, setLang} = useContext<LangContextProps>(LangContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext)
const isGPTEnabled: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session);
const isGemini: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session);
const isAnthropic: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session);
const isSubTierTwo: boolean = !isCurrentlyOffline() && QuillSense.getSubLevel(session) >= 2;
const hasAccess: boolean = (isGPTEnabled || isAnthropic || isGemini) || isSubTierTwo;
const [showSettingPanel, setShowSettingPanel] = useState<boolean>(false);
async function handleChapterVersionChanged(version: number) {
try {
const response: ChapterProps = await System.authGetQueryToServer<ChapterProps>(`chapter/whole`, session.accessToken, lang, {
bookid: book?.bookId,
id: chapter?.chapterId,
version: version,
});
if (!response) {
errorMessage(t("controllerBar.chapterNotFound"));
return;
}
setChapter(response);
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t("controllerBar.unknownChapterError"));
}
}
}
async function getBook(bookId: string): Promise<void> {
try {
const response: BookListProps = await System.authGetQueryToServer<BookListProps>(`book/basic-information`, session.accessToken, lang, {
id: bookId,
});
if (!response) {
errorMessage(t("controllerBar.bookNotFound"));
return;
}
setBook!!({
bookId: response.id,
type: response.type,
title: response.title,
subTitle: response.subTitle,
summary: response.summary,
publicationDate: response.desiredReleaseDate,
desiredWordCount: response.desiredWordCount,
totalWordCount: response.desiredWordCount,
});
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t("controllerBar.unknownBookError"));
}
}
}
function handleLanguageChange(language: "fr" | "en"): void {
System.setCookie('lang', language, 365);
const newLang: "en" | "fr" | null = System.getCookie('lang') as "en" | "fr" | null;
if (newLang) {
setLang(language);
}
}
return (
<div
className="flex items-center justify-between px-6 py-3 bg-tertiary/90 backdrop-blur-sm border-b border-secondary/50 shadow-md">
<div className="flex items-center space-x-4">
<div className="flex items-center gap-2">
{book && (
<button onClick={(): void => setShowSettingPanel(true)}
className="group p-2 rounded-lg text-muted hover:text-text-primary hover:bg-secondary/50 transition-all hover:scale-110">
<FontAwesomeIcon icon={faGear}
className={'w-5 h-5 transition-transform group-hover:rotate-90'}/>
</button>
)}
{
book && (
<button onClick={(): void => {
setBook && setBook(null)
setChapter && setChapter(undefined)
}}
className="group p-2 rounded-lg text-muted hover:text-primary hover:bg-secondary/50 transition-all hover:scale-110">
<FontAwesomeIcon icon={faHome}
className={'w-5 h-5 transition-transform group-hover:scale-110'}/>
</button>
)
}
</div>
<div className="min-w-[200px]">
<SelectBox onChangeCallBack={(e) => getBook(e.target.value)}
data={Book.booksToSelectBox(session.user?.books ?? [])} defaultValue={book?.bookId}
placeholder={t("controllerBar.selectBook")}/>
</div>
{chapter && (
<div className="min-w-[180px]">
<SelectBox onChangeCallBack={(e) => handleChapterVersionChanged(parseInt(e.target.value))}
data={chapterVersions.filter((version: SelectBoxProps): boolean => {
return !(version.value === '1' && !hasAccess);
}).map((version: SelectBoxProps) => {
return {
value: version.value.toString(),
label: t(version.label)
}
})} defaultValue={chapter?.chapterContent.version.toString()}/>
</div>
)}
</div>
<div className="flex items-center space-x-4">
{
hasAccess &&
<CreditCounter isCredit={isSubTierTwo}/>
}
<div
className="flex items-center bg-secondary/50 rounded-xl overflow-hidden border border-secondary shadow-sm">
<div className="flex items-center px-3 py-2 bg-dark-background/50 border-r border-secondary">
<FontAwesomeIcon icon={faGlobe} className="w-4 h-4 text-primary"/>
</div>
<button
onClick={() => handleLanguageChange('fr')}
className={`px-4 py-2 text-sm font-semibold transition-all ${
lang === 'fr'
? 'bg-primary text-text-primary shadow-md'
: 'bg-transparent text-text-secondary hover:bg-secondary/50 hover:text-text-primary'
}`}
>
FR
</button>
<button
onClick={() => handleLanguageChange('en')}
className={`px-4 py-2 text-sm font-semibold transition-all ${
lang === 'en'
? 'bg-primary text-text-primary shadow-md'
: 'bg-transparent text-text-secondary hover:bg-secondary/50 hover:text-text-primary'
}`}
>
EN
</button>
</div>
<UserMenu/>
</div>
{
showSettingPanel &&
<Modal title={t("controllerBar.bookSettings")}
size={'large'}
onClose={() => setShowSettingPanel(false)}
onConfirm={() => {
}}
children={<BookSetting/>}
enableFooter={false}
/>
}
</div>
)
}