Add QuillSense support with settings and integration

- Introduced new "QuillSense" feature for AI-assisted book creation.
- Added QuillSense settings panel with advanced options and disable/enable functionality.
- Updated GhostWriter and TextEditor components to respect QuillSense settings.
- Extended models and repositories to track and manage QuillSense state per book.
- Localized new strings for English and French.
This commit is contained in:
natreex
2026-01-13 19:52:31 -05:00
parent 8bad6159cf
commit 306262caba
12 changed files with 106 additions and 28 deletions

View File

@@ -77,6 +77,7 @@ export default function ScribeControllerBar() {
publicationDate: response.desiredReleaseDate,
desiredWordCount: response.desiredWordCount,
totalWordCount: response.desiredWordCount,
quillsenseEnabled: response.quillsenseEnabled,
});
} catch (e: unknown) {
if (e instanceof Error) {
@@ -141,7 +142,7 @@ export default function ScribeControllerBar() {
</div>
<div className="flex items-center space-x-4">
{
hasAccess &&
hasAccess && book?.quillsenseEnabled !== false &&
<CreditCounter isCredit={isSubTierTwo}/>
}
<div

View File

@@ -266,6 +266,7 @@ export default function BookList() {
totalWordCount: 0,
localBook: localBookOnly,
coverImage: bookResponse?.coverImage ? 'data:image/jpeg;base64,' + bookResponse.coverImage : '',
quillsenseEnabled: bookResponse?.quillsenseEnabled,
});
}
} catch (e: unknown) {

View File

@@ -7,6 +7,7 @@ import {RefObject, useRef} from "react";
import PanelHeader from "@/components/PanelHeader";
import LocationComponent from "@/components/book/settings/locations/LocationComponent";
import CharacterComponent from "@/components/book/settings/characters/CharacterComponent";
import QuillSenseSetting from "@/components/book/settings/quillsense/QuillSenseSetting";
import {useTranslations} from "next-intl"; // Ajouté pour la traduction
export default function BookSettingOption(
@@ -35,6 +36,9 @@ export default function BookSettingOption(
const characterRef: RefObject<{ handleSave: () => Promise<void> } | null> = useRef<{
handleSave: () => Promise<void>
}>(null);
const quillSenseRef: RefObject<{ handleSave: () => Promise<void> } | null> = useRef<{
handleSave: () => Promise<void>
}>(null);
function renderTitle(): string {
switch (setting) {
@@ -54,6 +58,8 @@ export default function BookSettingOption(
return t("bookSettingOption.objectsList");
case 'goals':
return t("bookSettingOption.bookGoals");
case 'quillsense':
return t("bookSettingOption.quillsense");
default:
return "";
}
@@ -79,6 +85,9 @@ export default function BookSettingOption(
case 'characters':
characterRef.current?.handleSave();
break;
case 'quillsense':
quillSenseRef.current?.handleSave();
break;
default:
break;
}
@@ -109,6 +118,8 @@ export default function BookSettingOption(
<LocationComponent ref={locationRef}/>
) : setting === 'characters' ? (
<CharacterComponent ref={characterRef}/>
) : setting === 'quillsense' ? (
<QuillSenseSetting ref={quillSenseRef}/>
) : <div
className="text-text-secondary py-4 text-center">{t("bookSettingOption.notAvailable")}</div>
}

View File

@@ -1,7 +1,7 @@
'use client'
// Removed Next.js Link import for Electron
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faBook, faGlobe, faListAlt, faMapMarkedAlt, faPencilAlt, faUser} from "@fortawesome/free-solid-svg-icons";
import {faBook, faFeather, faGlobe, faListAlt, faMapMarkedAlt, faPencilAlt, faUser} from "@fortawesome/free-solid-svg-icons";
import {Dispatch, SetStateAction} from "react";
import {IconDefinition} from "@fortawesome/fontawesome-svg-core";
import {useTranslations} from "next-intl";
@@ -53,6 +53,11 @@ export default function BookSettingSidebar(
name: 'bookSetting.characters',
icon: faUser
},
{
id: 'quillsense',
name: 'bookSetting.quillsense',
icon: faFeather
},
// {
// id: 'objects',
// name: t('bookSetting.objects'),

View File

@@ -483,7 +483,7 @@ export default function TextEditor() {
onClick={handleShowUserSettings}
icon={faCog}
/>
{chapter?.chapterContent.version === 2 && !isCurrentlyOffline() && !book?.localBook && (
{chapter?.chapterContent.version === 2 && !isCurrentlyOffline() && !book?.localBook && book?.quillsenseEnabled !== false && (
<CollapsableButton
showCollapsable={showGhostWriter}
text={t("textEditor.ghostWriter")}

View File

@@ -260,19 +260,25 @@ export default function GhostWriter() {
}
}
if (!hasAccess) {
if (!hasAccess || !book?.quillsenseEnabled) {
return (
<div className="flex items-center justify-center h-full">
<div
className="bg-tertiary/90 backdrop-blur-sm p-10 rounded-2xl shadow-2xl text-center border border-secondary/50 max-w-md">
<h2 className="text-2xl font-['ADLaM_Display'] text-text-primary mb-4">{t("ghostWriter.title")}</h2>
<p className="text-muted mb-6 text-lg leading-relaxed">{t("ghostWriter.subscriptionRequired")}</p>
<p className="text-muted mb-6 text-lg leading-relaxed">
{!book?.quillsenseEnabled
? t("ghostWriter.quillsenseDisabled")
: t("ghostWriter.subscriptionRequired")}
</p>
{hasAccess && !book?.quillsenseEnabled ? null : (
<button
onClick={(): string => window.location.href = '/pricing'}
className="px-6 py-3 bg-primary text-text-primary rounded-xl hover:bg-primary-dark transition-all duration-200 hover:scale-105 shadow-md hover:shadow-lg font-semibold"
>
{t("ghostWriter.subscribe")}
</button>
)}
</div>
</div>
);

View File

@@ -171,7 +171,16 @@ export default function ComposerRightBar() {
<div className="bg-tertiary border-l border-secondary/50 p-3 flex flex-col space-y-3 shadow-xl">
{book ? editorComponents
.filter((component: PanelComponent):boolean => {
return !((isCurrentlyOffline() || book?.localBook) && component.id === 1);
// Filter QuillSense if offline, local book, or quillsenseEnabled is false
if (component.id === 1) {
if (isCurrentlyOffline() || book?.localBook) {
return false;
}
if (book?.quillsenseEnabled === false) {
return false;
}
}
return true;
})
.map((component: PanelComponent) => (
<button

View File

@@ -1,4 +1,4 @@
import { Database, QueryResult, RunResult, SQLiteValue } from "node-sqlite3-wasm";
import {Database, QueryResult, RunResult, SQLiteValue} from "node-sqlite3-wasm";
import System from "../System.js";
export interface ChapterContentQueryResult extends Record<string, SQLiteValue> {
@@ -244,8 +244,7 @@ export default class ChapterContentRepository {
const db: Database = System.getDb();
const query: string = 'SELECT content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update FROM book_chapter_content WHERE author_id=? AND chapter_id=?';
const params: SQLiteValue[] = [userId, chapterId];
const bookChapterContents: BookChapterContentTable[] = db.all(query, params) as BookChapterContentTable[];
return bookChapterContents;
return db.all(query, params) as BookChapterContentTable[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
@@ -267,8 +266,7 @@ export default class ChapterContentRepository {
const db: Database = System.getDb();
const query: string = 'SELECT content_id, chapter_id, last_update FROM book_chapter_content WHERE author_id = ?';
const params: SQLiteValue[] = [userId];
const syncedChapterContents: SyncedChapterContentResult[] = db.all(query, params) as SyncedChapterContentResult[];
return syncedChapterContents;
return db.all(query, params) as SyncedChapterContentResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
@@ -296,10 +294,7 @@ export default class ChapterContentRepository {
static insertSyncChapterContent(contentId: string, chapterId: string, authorId: string, version: number, content: string | null, wordsCount: number, timeOnIt: number, lastUpdate: number, lang: 'fr' | 'en'): boolean {
try {
const db: Database = System.getDb();
const query: string = `
INSERT INTO book_chapter_content (content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`;
const query: string = `INSERT INTO book_chapter_content (content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`;
const params: SQLiteValue[] = [contentId, chapterId, authorId, version, content, wordsCount, timeOnIt, lastUpdate];
const insertResult: RunResult = db.run(query, params);
return insertResult.changes > 0;
@@ -324,8 +319,7 @@ export default class ChapterContentRepository {
const db: Database = System.getDb();
const query: string = 'SELECT content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update FROM book_chapter_content WHERE content_id = ?';
const params: SQLiteValue[] = [contentId];
const completeChapterContent: BookChapterContentTable[] = db.all(query, params) as BookChapterContentTable[];
return completeChapterContent;
return db.all(query, params) as BookChapterContentTable[];
} catch (error: unknown) {
if (error instanceof Error) {
throw new Error(lang === 'fr' ? `Impossible de récupérer le contenu de chapitre complet.` : `Unable to retrieve complete chapter content.`);

View File

@@ -450,6 +450,7 @@
"characters": "Characters",
"objectsList": "Objects list",
"bookGoals": "Book goals",
"quillsense": "QuillSense Settings",
"save": "Save",
"notAvailable": "Option not available"
},
@@ -489,6 +490,7 @@
"title": "Ghost Writer",
"description": "Turn your ideas into captivating prose",
"subscriptionRequired": "You must be subscribed to Quill Sense to use Ghost Writer.",
"quillsenseDisabled": "QuillSense is disabled for this book. You can enable it in the book settings.",
"subscribe": "Subscribe",
"length": "Text length",
"minimum": "Minimum",
@@ -768,7 +770,8 @@
"locations": "Locations",
"characters": "Characters",
"objects": "Objects",
"goals": "Goals"
"goals": "Goals",
"quillsense": "QuillSense"
},
"basicInformationSetting": {
"error": {
@@ -829,7 +832,8 @@
"common": {
"cancel": "Cancel",
"confirm": "Confirm",
"unknownError": "An unknown error occurred"
"unknownError": "An unknown error occurred",
"loading": "Loading..."
},
"editor": {
"error": {
@@ -993,5 +997,22 @@
"deleteLocalToo": "Also delete local version",
"deleteLocalWarning": "Warning: This action will delete the book from the server AND your device. This action is irreversible.",
"errorUnknown": "An unknown error occurred while deleting the book."
},
"quillSenseSetting": {
"title": "QuillSense Settings",
"description": "Manage AI features for this book.",
"enableSection": "Enable/Disable QuillSense",
"enableLabel": "QuillSense enabled",
"enableDescription": "When enabled, AI features like Ghost Writer and QuillSense will be available for this book.",
"advancedSection": "Advanced Settings",
"advancedPromptLabel": "Advanced Prompt",
"advancedPromptPlaceholder": "Enter custom instructions for the AI...",
"advancedPromptHint": "These instructions will be included in every text generation for this book.",
"disabledWarning": "QuillSense is disabled. AI features will not be available for this book.",
"errorFetch": "Error fetching QuillSense settings.",
"errorSave": "Error saving settings.",
"errorUnknown": "An unknown error occurred.",
"successSave": "QuillSense settings saved successfully.",
"noBookSelected": "No book selected."
}
}

View File

@@ -450,6 +450,7 @@
"characters": "Les personnages",
"objectsList": "Liste des objets",
"bookGoals": "Objectifs du livre",
"quillsense": "Parametres QuillSense",
"save": "Sauvegarder",
"notAvailable": "Option non disponible"
},
@@ -489,6 +490,7 @@
"title": "Écrivain Fantôme",
"description": "Transformez vos idées en prose captivante",
"subscriptionRequired": "Vous devez être abonné à Quill Sense pour utiliser Ghost Writer.",
"quillsenseDisabled": "QuillSense est désactivé pour ce livre. Vous pouvez l'activer dans les paramètres du livre.",
"subscribe": "S'abonner",
"length": "Longueur du texte",
"minimum": "Minimum",
@@ -769,7 +771,8 @@
"locations": "Emplacements",
"characters": "Personnages",
"objects": "Objets",
"goals": "Buts"
"goals": "Buts",
"quillsense": "QuillSense"
},
"basicInformationSetting": {
"error": {
@@ -830,7 +833,8 @@
"common": {
"cancel": "Annuler",
"confirm": "Confirmer",
"unknownError": "Une erreur inconnue est survenue"
"unknownError": "Une erreur inconnue est survenue",
"loading": "Chargement..."
},
"editor": {
"error": {
@@ -994,5 +998,22 @@
"deleteLocalToo": "Supprimer également la version locale",
"deleteLocalWarning": "Attention : Cette action supprimera le livre du serveur ET de votre appareil. Cette action est irréversible.",
"errorUnknown": "Une erreur inconnue est survenue lors de la suppression du livre."
},
"quillSenseSetting": {
"title": "Paramètres QuillSense",
"description": "Gérez les fonctionnalités d'intelligence artificielle pour ce livre.",
"enableSection": "Activer/Désactiver QuillSense",
"enableLabel": "QuillSense activé",
"enableDescription": "Lorsque activé, les fonctionnalités d'IA comme Ghost Writer et QuillSense seront disponibles pour ce livre.",
"advancedSection": "Paramètres avancés",
"advancedPromptLabel": "Invite avancée",
"advancedPromptPlaceholder": "Entrez des instructions personnalisées pour l'IA...",
"advancedPromptHint": "Ces instructions seront incluses dans chaque génération de texte pour ce livre.",
"disabledWarning": "QuillSense est désactivé. Les fonctionnalités d'IA ne seront pas disponibles pour ce livre.",
"errorFetch": "Erreur lors de la récupération des paramètres QuillSense.",
"errorSave": "Erreur lors de la sauvegarde des paramètres.",
"errorUnknown": "Une erreur inconnue est survenue.",
"successSave": "Paramètres QuillSense sauvegardés avec succès.",
"noBookSelected": "Aucun livre sélectionné."
}
}

View File

@@ -71,6 +71,7 @@ export interface BookProps {
coverImage?: string;
localBook?: boolean;
chapters?: ChapterProps[];
quillsenseEnabled?: boolean;
}
export interface BookListProps {
@@ -87,6 +88,7 @@ export interface BookListProps {
coverImage?: string;
bookMeta?: string;
itIsLocal?: boolean;
quillsenseEnabled?: boolean;
}
export interface GuideLine {

View File

@@ -195,3 +195,10 @@ export interface LocationSubElementTable {
sub_elem_description: string | null;
last_update: number;
}
export interface GhostWriterSettingsTable {
book_id: string;
user_id: string;
advanced_prompt: string | null;
quillsense_enabled: number;
}