Add advanced generation options with Explicit and Smart modes

- Implemented `AdvancedGenerationOptions` component for toggling Explicit and Smart modes with confirmation dialogs.
- Integrated generation options into `GhostWriter`, `DraftCompanion`, and `ShortStoryGenerator`.
- Introduced `ToggleWithConfirmation` component for user interaction with alerts.
- Updated `InputField` to support centered alignment for better layout flexibility.
- Localized Explicit and Smart mode strings in English and French.
- Enhanced content preview logic to filter placeholder text before display.
- Added `autoUpdater` initialization checks and refactored updater setup for improved reliability.
This commit is contained in:
natreex
2026-01-17 23:26:22 -05:00
parent 0020b3abbd
commit c62a7eb0f7
11 changed files with 335 additions and 34 deletions

View File

@@ -27,6 +27,9 @@ export default function QSTextGeneratedPreview(
const [isVisible, setIsVisible] = useState(false);
const t = useTranslations();
const filteredValue: string = value.replace(/^starting\.{0,3}\s*/i, '').trim();
const hasRealContent: boolean = filteredValue.length > 0;
useEffect((): () => void => {
setMounted(true);
const timer = setTimeout(() => setIsVisible(true), 10);
@@ -83,21 +86,46 @@ export default function QSTextGeneratedPreview(
<div className="flex-1 p-5 overflow-auto custom-scrollbar">
<div
className="w-full bg-darkest-background text-text-primary p-5 rounded-xl border border-secondary/50 shadow-inner">
{isGenerating && !value ? (
<div className="space-y-4 animate-pulse">
<div className="h-4 bg-secondary/30 rounded w-full"></div>
<div className="h-4 bg-secondary/30 rounded w-11/12"></div>
<div className="h-4 bg-secondary/30 rounded w-full"></div>
<div className="h-4 bg-secondary/30 rounded w-10/12"></div>
<div className="h-4 bg-secondary/30 rounded w-full"></div>
<div className="h-4 bg-secondary/30 rounded w-9/12"></div>
<div className="h-4 bg-secondary/30 rounded w-full"></div>
<div className="h-4 bg-secondary/30 rounded w-11/12"></div>
{isGenerating && !hasRealContent ? (
<div className="space-y-3 animate-pulse">
<div className="flex flex-wrap gap-2">
<span className="h-5 bg-primary/20 rounded px-4"></span>
<span className="h-5 bg-primary/15 rounded px-6"></span>
<span className="h-5 bg-primary/20 rounded px-3"></span>
<span className="h-5 bg-primary/10 rounded px-8"></span>
<span className="h-5 bg-primary/20 rounded px-5"></span>
<span className="h-5 bg-primary/15 rounded px-4"></span>
<span className="h-5 bg-primary/20 rounded px-7"></span>
</div>
<div className="flex flex-wrap gap-2">
<span className="h-5 bg-primary/15 rounded px-5"></span>
<span className="h-5 bg-primary/20 rounded px-3"></span>
<span className="h-5 bg-primary/10 rounded px-6"></span>
<span className="h-5 bg-primary/20 rounded px-4"></span>
<span className="h-5 bg-primary/15 rounded px-8"></span>
<span className="h-5 bg-primary/20 rounded px-3"></span>
</div>
<div className="flex flex-wrap gap-2">
<span className="h-5 bg-primary/20 rounded px-6"></span>
<span className="h-5 bg-primary/10 rounded px-4"></span>
<span className="h-5 bg-primary/20 rounded px-5"></span>
<span className="h-5 bg-primary/15 rounded px-7"></span>
<span className="h-5 bg-primary/20 rounded px-3"></span>
<span className="h-5 bg-primary/10 rounded px-5"></span>
<span className="h-5 bg-primary/20 rounded px-4"></span>
</div>
<div className="flex flex-wrap gap-2">
<span className="h-5 bg-primary/15 rounded px-4"></span>
<span className="h-5 bg-primary/20 rounded px-6"></span>
<span className="h-5 bg-primary/10 rounded px-3"></span>
<span className="h-5 bg-primary/20 rounded px-5"></span>
<span className="h-5 bg-primary/15 rounded px-7"></span>
</div>
</div>
) : (
<div className="space-y-4">
<div className="text-justify leading-relaxed whitespace-pre-wrap fade-in-text">
{value}
{filteredValue}
</div>
</div>
)}

View File

@@ -53,6 +53,7 @@ import QuillSense from "@/lib/models/QuillSense";
import {useTranslations} from "next-intl";
import {LangContext, LangContextProps} from "@/context/LangContext";
import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext";
import AdvancedGenerationOptions from "@/components/form/AdvancedGenerationOptions";
interface ShortStoryGeneratorProps {
onClose: () => void;
@@ -94,6 +95,9 @@ export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps)
const [hasGenerated, setHasGenerated] = useState<boolean>(false);
const [abortController, setAbortController] = useState<ReadableStreamDefaultReader<Uint8Array> | null>(null);
const [useExplicit, setUseExplicit] = useState<boolean>(false);
const [useSmart, setUseSmart] = useState<boolean>(false);
const isAnthropicEnabled: boolean = QuillSense.isAnthropicEnabled(session);
const isSubTierTwo: boolean = QuillSense.getSubLevel(session) >= 2;
const hasAccess: boolean = isAnthropicEnabled || isSubTierTwo;
@@ -169,7 +173,9 @@ export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps)
language: language,
dialogueType: dialogueType,
directives: directives,
wordsCount: wordsCount
wordsCount: wordsCount,
useExplicit: useExplicit,
useSmart: useSmart,
}),
});
@@ -561,6 +567,13 @@ export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps)
/>
}
/>
<AdvancedGenerationOptions
useExplicit={useExplicit}
setUseExplicit={setUseExplicit}
useSmart={useSmart}
setUseSmart={setUseSmart}
/>
</div>
)}

View File

@@ -33,6 +33,7 @@ import {BookTags} from "@/lib/models/Book";
import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext";
import {configs} from "@/lib/configs";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import AdvancedGenerationOptions from "@/components/form/AdvancedGenerationOptions";
interface CompanionContent {
version: number;
@@ -95,6 +96,9 @@ export default function DraftCompanion() {
const [showObjectSuggestions, setShowObjectSuggestions] = useState<boolean>(false);
const [showWorldElementSuggestions, setShowWorldElementSuggestions] = useState<boolean>(false);
const [useExplicit, setUseExplicit] = useState<boolean>(false);
const [useSmart, setUseSmart] = useState<boolean>(false);
const isGPTEnabled: boolean = QuillSense.isOpenAIEnabled(session);
const isSubTierTree: boolean = QuillSense.getSubLevel(session) === 3;
const hasAccess: boolean = (isGPTEnabled || isSubTierTree) && !isCurrentlyOffline() && !book?.localBook;
@@ -222,7 +226,9 @@ export default function DraftCompanion() {
locations: taguedLocations,
objects: taguedObjects,
worldElements: taguedWorldElements,
}
},
useExplicit: useExplicit,
useSmart: useSmart,
}),
});
@@ -432,7 +438,7 @@ export default function DraftCompanion() {
return element ? element.label : value;
}
if (showEnhancer && hasAccess && book?.quillsenseEnabled !== false) {
if (showEnhancer && book?.quillsenseEnabled !== false) {
return (
<div className="flex flex-col h-full min-h-0 overflow-hidden">
<div
@@ -539,6 +545,13 @@ export default function DraftCompanion() {
}
/>
</div>
<AdvancedGenerationOptions
useExplicit={useExplicit}
setUseExplicit={setUseExplicit}
useSmart={useSmart}
setUseSmart={setUseSmart}
/>
</div>
<div

View File

@@ -0,0 +1,61 @@
'use client'
import React from "react";
import {faBrain, faTriangleExclamation} from "@fortawesome/free-solid-svg-icons";
import InputField from "@/components/form/InputField";
import ToggleWithConfirmation from "@/components/form/ToggleWithConfirmation";
import {useTranslations} from "next-intl";
interface AdvancedGenerationOptionsProps {
useExplicit: boolean;
setUseExplicit: (value: boolean) => void;
useSmart: boolean;
setUseSmart: (value: boolean) => void;
}
export default function AdvancedGenerationOptions({
useExplicit,
setUseExplicit,
useSmart,
setUseSmart
}: AdvancedGenerationOptionsProps) {
const t = useTranslations();
return (
<div className="bg-secondary/20 rounded-xl p-5 shadow-inner border border-secondary/30">
<div className="flex justify-evenly items-center">
<InputField
icon={faTriangleExclamation}
fieldName={t("generationOptions.explicit.label")}
centered
input={
<ToggleWithConfirmation
checked={useExplicit}
onChange={setUseExplicit}
alertTitle={t("generationOptions.explicit.alertTitle")}
alertMessage={t("generationOptions.explicit.alertMessage")}
alertType="alert"
confirmText={t("generationOptions.activate")}
cancelText={t("generationOptions.cancel")}
/>
}
/>
<InputField
icon={faBrain}
fieldName={t("generationOptions.smart.label")}
centered
input={
<ToggleWithConfirmation
checked={useSmart}
onChange={setUseSmart}
alertTitle={t("generationOptions.smart.alertTitle")}
alertMessage={t("generationOptions.smart.alertMessage")}
alertType="informatif"
confirmText={t("generationOptions.activate")}
cancelText={t("generationOptions.cancel")}
/>
}
/>
</div>
</div>
);
}

View File

@@ -14,6 +14,7 @@ interface InputFieldProps {
actionLabel?: string
actionIcon?: IconDefinition
hint?: string,
centered?: boolean,
}
export default function InputField(
@@ -27,11 +28,12 @@ export default function InputField(
action,
actionLabel,
actionIcon,
hint
hint,
centered = false
}: InputFieldProps) {
return (
<div className={'flex flex-col'}>
<div className={'flex justify-between items-center mb-2 lg:mb-3 flex-wrap gap-2'}>
<div className={`flex flex-col ${centered ? 'items-center' : ''}`}>
<div className={`flex items-center mb-2 lg:mb-3 flex-wrap gap-2 ${centered ? 'justify-center' : 'justify-between'}`}>
{
fieldName && (
<h3 className="text-text-primary text-xl font-[ADLaM Display] font-medium mb-2 flex items-center gap-2">
@@ -64,7 +66,7 @@ export default function InputField(
</span>
)}
</div>
<div className="flex justify-between items-center gap-2">
<div className={`flex items-center gap-2 ${centered ? 'justify-center' : 'justify-between'}`}>
{input}
{
addButtonCallBack && (

View File

@@ -0,0 +1,63 @@
'use client'
import React, {useState} from "react";
import ToggleSwitch from "@/components/form/ToggleSwitch";
import AlertBox, {AlertType} from "@/components/AlertBox";
interface ToggleWithConfirmationProps {
checked: boolean;
onChange: (checked: boolean) => void;
alertTitle: string;
alertMessage: string;
alertType: AlertType;
confirmText?: string;
cancelText?: string;
disabled?: boolean;
}
export default function ToggleWithConfirmation({
checked,
onChange,
alertTitle,
alertMessage,
alertType,
confirmText = "Activer",
cancelText = "Annuler",
disabled = false
}: ToggleWithConfirmationProps) {
const [showAlert, setShowAlert] = useState<boolean>(false);
function handleToggle(newChecked: boolean): void {
if (newChecked) {
setShowAlert(true);
} else {
onChange(false);
}
}
async function handleConfirm(): Promise<void> {
onChange(true);
setShowAlert(false);
}
function handleCancel(): void {
setShowAlert(false);
}
return (
<>
<ToggleSwitch checked={checked} onChange={handleToggle} disabled={disabled}/>
{showAlert && (
<AlertBox
title={alertTitle}
message={alertMessage}
type={alertType}
confirmText={confirmText}
cancelText={cancelText}
onConfirm={handleConfirm}
onCancel={handleCancel}
/>
)}
</>
);
}

View File

@@ -34,6 +34,7 @@ import QuillSense, {AIGeneratedText} from "@/lib/models/QuillSense";
import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext";
import {LangContext} from "@/context/LangContext";
import {configs} from "@/lib/configs";
import AdvancedGenerationOptions from "@/components/form/AdvancedGenerationOptions";
export default function GhostWriter() {
const t = useTranslations();
@@ -62,6 +63,9 @@ export default function GhostWriter() {
const [taguedWorldElements, setTaguedWorldElements] = useState<string[]>([]);
const [abortController, setAbortController] = useState<ReadableStreamDefaultReader<Uint8Array> | null>(null);
const [useExplicit, setUseExplicit] = useState<boolean>(false);
const [useSmart, setUseSmart] = useState<boolean>(false);
const isGPTEnabled: boolean = QuillSense.isOpenAIEnabled(session);
const isSubTierTree: boolean = QuillSense.getSubLevel(session) === 3;
const hasAccess: boolean = isGPTEnabled || isSubTierTree;
@@ -147,6 +151,8 @@ export default function GhostWriter() {
objects: taguedObjects,
worldElements: taguedWorldElements,
},
useExplicit: useExplicit,
useSmart: useSmart,
}),
});
@@ -371,6 +377,13 @@ export default function GhostWriter() {
}
/>
</div>
<AdvancedGenerationOptions
useExplicit={useExplicit}
setUseExplicit={setUseExplicit}
useSmart={useSmart}
setUseSmart={setUseSmart}
/>
</div>
) : advanceSettings && (
<GhostWriterSettings advancedPrompt={advancedPrompt} setAdvancedPrompt={setAdvancedPrompt}/>

75
electron/autoUpdater.ts Normal file
View File

@@ -0,0 +1,75 @@
import pkg from 'electron-updater';
import type { UpdateInfo } from 'electron-updater';
const { autoUpdater } = pkg;
import { app, BrowserWindow } from 'electron';
const updateCheckInterval = 4 * 60 * 60 * 1000; // 4 heures
let initialized = false;
let currentWindow: BrowserWindow | null = null;
export function initAutoUpdater(window: BrowserWindow): void {
currentWindow = window;
if (!app.isPackaged) {
console.log('[AutoUpdater] Skipped in development mode');
return;
}
// Si déjà initialisé, juste mettre à jour la fenêtre cible
if (initialized) {
console.log('[AutoUpdater] Window target updated');
return;
}
initialized = true;
// Config: télécharge auto, installe au quit
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.on('checking-for-update', () => {
console.log('[AutoUpdater] Checking for updates...');
});
autoUpdater.on('update-available', (info: UpdateInfo) => {
console.log('[AutoUpdater] Update available:', info.version);
currentWindow?.webContents.send('update:available', info.version);
});
autoUpdater.on('update-not-available', () => {
console.log('[AutoUpdater] App is up to date');
});
autoUpdater.on('download-progress', (progress) => {
const percent = Math.round(progress.percent);
console.log(`[AutoUpdater] Downloading: ${percent}%`);
currentWindow?.webContents.send('update:progress', percent);
});
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
console.log('[AutoUpdater] Update ready:', info.version);
currentWindow?.webContents.send('update:ready', info.version);
});
autoUpdater.on('error', (error: Error) => {
console.error('[AutoUpdater] Error:', error.message);
});
// Check initial
autoUpdater.checkForUpdates().catch((err) => {
console.error('[AutoUpdater] Check failed:', err.message);
});
// Re-check périodique
setInterval(() => {
autoUpdater.checkForUpdates().catch((err) => {
console.error('[AutoUpdater] Periodic check failed:', err.message);
});
}, updateCheckInterval);
}
// Pour forcer l'installation immédiate (optionnel, appelable depuis le renderer)
export function installUpdateNow(): void {
autoUpdater.quitAndInstall(false, true);
}

View File

@@ -87,6 +87,9 @@ function createLoginWindow(): void {
loginWindow.once('ready-to-show', () => {
loginWindow?.show();
if (loginWindow) {
initAutoUpdater(loginWindow);
}
});
loginWindow.on('closed', () => {
@@ -145,7 +148,9 @@ function createMainWindow(): void {
mainWindow.once('ready-to-show', () => {
mainWindow?.show();
initAutoUpdater(mainWindow);
if (mainWindow) {
initAutoUpdater(mainWindow);
}
});
mainWindow.on('closed', () => {

View File

@@ -1,4 +1,18 @@
{
"generationOptions": {
"explicit": {
"label": "Explicit",
"alertTitle": "Explicit Mode",
"alertMessage": "This mode enables mature content generation. Some restrictions remain in effect. For users 18 years and older only."
},
"smart": {
"label": "Smart",
"alertTitle": "Smart Mode",
"alertMessage": "This mode uses the most powerful models (Claude Opus, Grok 4.1) for superior generation quality. Additional fees apply."
},
"activate": "Activate",
"cancel": "Cancel"
},
"loginPage": {
"title": "Login",
"welcome": "Welcome to ERitors",

View File

@@ -1,4 +1,18 @@
{
"generationOptions": {
"explicit": {
"label": "Explicite",
"alertTitle": "Mode Explicite",
"alertMessage": "Ce mode permet de générer du contenu mature. Certaines restrictions demeurent en vigueur. Réservé aux utilisateurs de 18 ans et plus."
},
"smart": {
"label": "Intelligent",
"alertTitle": "Mode Intelligent",
"alertMessage": "Ce mode utilise les modèles les plus performants (Claude Opus, Grok 4.1) pour une qualité de génération supérieure. Des frais supplémentaires s'appliquent."
},
"activate": "Activer",
"cancel": "Annuler"
},
"loginPage": {
"title": "Connexion",
"welcome": "Bienvenue sur ERitors",