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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
61
components/form/AdvancedGenerationOptions.tsx
Normal file
61
components/form/AdvancedGenerationOptions.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 && (
|
||||
|
||||
63
components/form/ToggleWithConfirmation.tsx
Normal file
63
components/form/ToggleWithConfirmation.tsx
Normal 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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
75
electron/autoUpdater.ts
Normal 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);
|
||||
}
|
||||
@@ -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();
|
||||
if (mainWindow) {
|
||||
initAutoUpdater(mainWindow);
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user