Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Null Library — Environment Variables
# Copy this file to .env and fill in your values.
# .env is gitignored and never committed.

# ── Free Gemini API Key (fallback) ──────────────────────────────────────────
# From https://aistudio.google.com/apikey — free tier available
# VITE_GOOGLE_API_KEY=AIza...

# ── NullProxy Engine: OAuth Provider Credentials ────────────────────────────
# These are needed to use the OAuth proxy feature (requires desktop app).
# Register at https://console.cloud.google.com → APIs & Services → Credentials
# Redirect URI to add: http://localhost:8085/callback

# GEMINI_CLI_CLIENT_ID=your-client-id.apps.googleusercontent.com
# GEMINI_CLI_CLIENT_SECRET=your-client-secret

# GEMINI_ANTIGRAVITY_CLIENT_ID=your-second-client-id.apps.googleusercontent.com
# GEMINI_ANTIGRAVITY_CLIENT_SECRET=your-second-client-secret
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ dist-ssr
*.njsproj
*.sln
*.sw?

# Environment files (contain secrets)
.env
.env.local
.env.*.local
Comment on lines +26 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 | Confidence: High

The .gitignore update simplifies the pattern but removes the explicit negation !.env.example. This is fine because .env.example is not matched by the new patterns (.env, .env.local, .env.*.local). However, the comment "Environment files (contain secrets)" is less emphatic than "NEVER commit real keys." The change is functionally correct but could be slightly improved for security awareness.

Suggested change
# Environment files (contain secrets)
.env
.env.local
.env.*.local
# SECURITY: Environment files contain secrets like API keys and OAuth credentials.
# NEVER commit these files to version control.
.env
.env.local
.env.*.local

Evidence: path:.gitignore

77 changes: 58 additions & 19 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import React, { useState, useCallback, useEffect, useRef } from 'react';
import { AppStep, BookOutline, Chapter, MarketReport, AuthorProfile, GenreSuggestion, TopicSuggestion, KdpMarketingInfo, AppMode, BatchProject, KdpAutomationPayload } from './types';
import { AppStep, BookOutline, Chapter, MarketReport, AuthorProfile, GenreSuggestion, TopicSuggestion, KdpMarketingInfo, AppMode, BatchProject, KdpAutomationPayload, ProxySettings } from './types';
import * as geminiService from './services/geminiService';
import * as realMarketService from './services/realMarketService';
import * as storageService from './services/storageService';
Expand All @@ -12,17 +12,20 @@ import OutlineStep from './components/steps/OutlineStep';
import ContentGenerationStep from './components/steps/ContentGenerationStep';
import IllustrationStep from './components/steps/IllustrationStep';
import ReviewStep from './components/steps/ReviewStep';
import SetupWizardStep from './components/steps/SetupWizardStep';
import ProxySettingsModal from './components/ProxySettingsModal';
import LoadingSpinner from './components/shared/LoadingSpinner';
import { SparklesIcon, UserCircleIcon, TrashIcon, RocketLaunchIcon, CheckBadgeIcon } from './components/icons';
import NullLibraryLogo from './components/NullLibraryLogo';
import AuthorProfileModal from './components/AuthorProfileModal';
import BatchMode from './components/BatchMode';
import KdpAutomationBot from './components/KdpAutomationBot';
import TitleBar from './components/TitleBar';
import desktopBridge from './services/desktopBridge';


import { useAutoSave } from './hooks/useAutoSave';
import AutoSaveIndicator from './components/shared/AutoSaveIndicator';
import { isFirstRun } from './services/oauthSetupService';

const pageRangeToChapterCount = (pageRange: string): number => {
const firstNum = parseInt(pageRange.split('-')[0], 10);
Expand All @@ -35,14 +38,18 @@ const pageRangeToChapterCount = (pageRange: string): number => {
return 25;
};

// Versioned storage key
const STORAGE_KEY = 'kdp-ai-booksmith-v5-db';
// Versioned storage key (renamed to null-library from kdp-ai-booksmith)
const STORAGE_KEY = 'null-library-v1-db';

function App() {
const [mode, setMode] = useState<AppMode>(AppMode.Single);
const [currentStep, setCurrentStep] = useState<AppStep>(AppStep.MarketResearch);
const [isLoading, setIsLoading] = useState(true); // Start true to allow DB load
const [error, setError] = useState<string | null>(null);

// ── First-run wizard state ────────────────────────────────────────────────
const [showWizard, setShowWizard] = useState(false);
const [isProxySettingsOpen, setIsProxySettingsOpen] = useState(false);

// SINGLE BOOK MODE STATE
const [genreSuggestions, setGenreSuggestions] = useState<GenreSuggestion[] | null>(null);
Expand Down Expand Up @@ -76,6 +83,7 @@ function App() {
const [batchProjects, setBatchProjects] = useState<BatchProject[]>([]);
const [isBatchRunning, setIsBatchRunning] = useState(false);


// --- AUTO SAVE ---
const { lastSaved, isSaving: isAutoSaving } = useAutoSave({
mode,
Expand Down Expand Up @@ -133,20 +141,28 @@ function App() {
}
} catch (e) {
console.error("Failed to load state from DB", e);
// Fallback: Check localStorage for migration
const legacyState = localStorage.getItem('kdp-ai-booksmith-v4');
if (legacyState) {
console.log("Migrating from legacy localStorage...");
try {
const parsed = JSON.parse(legacyState);
setAuthorProfile(parsed.authorProfile);
// We don't load everything to avoid bugs, just profile is useful
} catch(err) {}
// Fallback: Check localStorage for migration from legacy storage keys
const legacyKeys = ['kdp-ai-booksmith-v4', 'kdp-ai-booksmith-v5-db'];
for (const key of legacyKeys) {
const legacyState = localStorage.getItem(key);
if (legacyState) {
console.log("Migrating from legacy localStorage key:", key);
try {
const parsed = JSON.parse(legacyState);
setAuthorProfile(parsed.authorProfile);
} catch(err) {}
break;
}
}
} finally {
setIsLoading(false);
}

// Show first-run wizard if not completed
if (isFirstRun()) {
setShowWizard(true);
}

// Check storage status
const isPersisted = await navigator.storage && navigator.storage.persisted ? await navigator.storage.persisted() : false;
setIsPersistentStorage(isPersisted);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Contextual Comment]
This comment refers to code near real line 337. Anchored to nearest_changed(168) line 168.


P1 | Confidence: High

The PR introduces a new aiService.ts as a unified AI client, explicitly stating it replaces direct geminiService calls. However, App.tsx continues to call geminiService.generateBookOutline. This creates a critical architectural inconsistency. The geminiService module may have been updated to proxy through aiService, but without verification, this creates a risk of broken functionality, duplicate logic, or the NullProxy Engine being bypassed entirely. The impact is a fractured AI routing system where some calls use the new unified client and others do not, undermining the core feature of the PR.

Code Suggestion:

Review the `geminiService.ts` implementation to ensure it exports a wrapper that calls `aiService`. If not, migrate the call in `App.tsx` to use `aiService.generateText` with the appropriate `TaskType` (e.g., 'creative-writing').

Evidence: symbol:geminiService.generateBookOutline, path:services/geminiService.ts


[Contextual Comment]
This comment refers to code near real line 379. Anchored to nearest_changed(168) line 168.


P1 | Confidence: High

Consistent with the previous finding, the chapter generation still uses the legacy geminiService instead of the new aiService. This perpetuates the architectural split. Furthermore, the related context shows the removal of streaming generation, RAG embedding (embedChapter), and Book Bible extraction (extractBibleEntries). This is a significant functional regression. The handleGenerateChapter function no longer updates the UI with streaming chunks and loses the background RAG and continuity tracking features, degrading the user experience and potentially breaking dependent features.

Code Suggestion:

N/A (Requires a broader decision on whether to restore streaming/RAG features). At minimum, ensure this call routes through `aiService.generateText` with task type 'creative-writing'.

Evidence: symbol:geminiService.generateChapterContent, path:services/geminiService.ts

Expand Down Expand Up @@ -796,7 +812,7 @@ function App() {
chapterLoadingStates: {} // Don't save loading states

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Contextual Comment]
This comment refers to code near real line 649. Anchored to nearest_changed(812) line 812.


P2 | Confidence: Medium

Speculative: The humanization logic was refactored from a sequential for loop to a parallel Promise.all. While this improves performance, it may trigger rate limits or quotas on the AI provider (whether using the new NullProxy OAuth accounts or a manual API key) because all chapters are sent simultaneously. The previous sequential approach was a natural rate limiter. This change, combined with the high-concurrency mode, could lead to increased failure rates.

Code Suggestion:

Introduce a concurrency limit (e.g., using `p-limit` library) or make the parallelism configurable based on the `isHighPerformanceMode` setting. For example, use `Promise.all` only when `isHighPerformanceMode` is true, otherwise process sequentially.

Evidence: method:handleHumanizeBook

};

const fileName = bookOutline?.title ? `${bookOutline.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.json` : 'kdp-project.json';
const fileName = bookOutline?.title ? `${bookOutline.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.json` : 'null-library-project.json';

try {
const result = await desktopBridge.saveFile(JSON.stringify(projectState, null, 2), fileName);
Expand Down Expand Up @@ -940,21 +956,36 @@ function App() {

return (
<div className="pt-8 min-h-screen flex flex-col bg-slate-900 text-slate-200 font-sans selection:bg-indigo-500 selection:text-white">
<TitleBar onSave={handleSaveProject} onLoad={handleLoadProject} />
<TitleBar onSave={handleSaveProject} onLoad={handleLoadProject} onOpenSettings={() => setIsProxySettingsOpen(true)} />

{/* Setup Wizard overlay — shows on first launch */}
{showWizard && (
<div className="fixed inset-0 z-[200]">
<SetupWizardStep
onComplete={(_settings: ProxySettings) => setShowWizard(false)}
onSkip={() => setShowWizard(false)}
/>
</div>
)}

{/* Proxy Settings Modal */}
{isProxySettingsOpen && (
<ProxySettingsModal onClose={() => setIsProxySettingsOpen(false)} />
)}

{/* Header */}
<header className="border-b border-slate-800 bg-slate-900/50 backdrop-blur-sm sticky top-8 z-40">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="bg-gradient-to-br from-violet-600 to-indigo-600 p-2 rounded-lg shadow-lg shadow-violet-900/20">
<SparklesIcon className="w-6 h-6 text-white" />
<NullLibraryLogo size={24} />
</div>
<div>
<h1 className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-white to-slate-400">
KDP E-Book Generator
Null Library
</h1>
<p className="text-xs text-slate-500 font-medium tracking-wide">AI-POWERED PUBLISHING ENGINE</p>
<p className="text-xs text-slate-500 font-medium tracking-wide">THE ART OF INFINITE PRODUCTION</p>
</div>
</div>

Expand Down Expand Up @@ -1062,7 +1093,15 @@ function App() {
{isHighPerformanceMode ? "High Concurrency" : "Sequential"}
</button>
</div>
<span>App v1.5.0</span>
<span>App v3.0.0</span>
<button
onClick={() => setIsProxySettingsOpen(true)}
className="flex items-center gap-1 text-violet-400 hover:text-violet-300 transition-colors hover:underline"
title="Open NullProxy Engine settings"
>
<span>⚡</span>
<span>NullProxy</span>
</button>
</div>
</div>
</div>
Expand Down
Loading
Loading