♿ AI is totally optional.
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
|||||||
import type { Market, Submission, StoryMeta, TelemetrySession } from './fileSystem'
|
import type { Market, Submission, StoryMeta, TelemetrySession } from './fileSystem'
|
||||||
import { streamMessage, streamPrompt, resetClient } from './aiService'
|
import { streamMessage, streamPrompt, resetClient } from './aiService'
|
||||||
import type { AIPayload } from './aiService'
|
import type { AIPayload } from './aiService'
|
||||||
import { readGlobalConfig, writeGlobalConfig } from './globalConfig'
|
import { readGlobalConfig, writeGlobalConfig, getApiKey } from './globalConfig'
|
||||||
import type { GlobalConfig } from './globalConfig'
|
import type { GlobalConfig } from './globalConfig'
|
||||||
|
|
||||||
export function registerIpcHandlers(): void {
|
export function registerIpcHandlers(): void {
|
||||||
@@ -54,6 +54,7 @@ export function registerIpcHandlers(): void {
|
|||||||
ipcMain.handle('telemetry:read', async () => readTelemetry())
|
ipcMain.handle('telemetry:read', async () => readTelemetry())
|
||||||
|
|
||||||
// ── Config ────────────────────────────────────────────────────────────────────
|
// ── Config ────────────────────────────────────────────────────────────────────
|
||||||
|
ipcMain.handle('config:isAIEnabled', async () => !!getApiKey())
|
||||||
ipcMain.handle('config:read', async () => readGlobalConfig())
|
ipcMain.handle('config:read', async () => readGlobalConfig())
|
||||||
ipcMain.handle('config:write', async (_e, updates: Partial<GlobalConfig>) => {
|
ipcMain.handle('config:write', async (_e, updates: Partial<GlobalConfig>) => {
|
||||||
writeGlobalConfig(updates)
|
writeGlobalConfig(updates)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
loadRevision: (path: string, id: string): Promise<string> => ipcRenderer.invoke('revisions:load', path, id),
|
loadRevision: (path: string, id: string): Promise<string> => ipcRenderer.invoke('revisions:load', path, id),
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
|
isAIEnabled: (): Promise<boolean> => ipcRenderer.invoke('config:isAIEnabled'),
|
||||||
readConfig: (): Promise<GlobalConfig> => ipcRenderer.invoke('config:read'),
|
readConfig: (): Promise<GlobalConfig> => ipcRenderer.invoke('config:read'),
|
||||||
writeConfig: (updates: Partial<GlobalConfig>): Promise<void> => ipcRenderer.invoke('config:write', updates),
|
writeConfig: (updates: Partial<GlobalConfig>): Promise<void> => ipcRenderer.invoke('config:write', updates),
|
||||||
pickFolder: (): Promise<string | null> => ipcRenderer.invoke('config:pickFolder'),
|
pickFolder: (): Promise<string | null> => ipcRenderer.invoke('config:pickFolder'),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function App(): JSX.Element {
|
|||||||
} = useBorgesStore()
|
} = useBorgesStore()
|
||||||
|
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||||
const [, setIsFirstRun] = useState(false)
|
const [aiEnabled, setAiEnabled] = useState(false)
|
||||||
|
|
||||||
// Initialise app
|
// Initialise app
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -40,11 +40,7 @@ export default function App(): JSX.Element {
|
|||||||
setMarkets(marketsList)
|
setMarkets(marketsList)
|
||||||
setSubmissions(subsList)
|
setSubmissions(subsList)
|
||||||
await loadSession()
|
await loadSession()
|
||||||
const cfg = await window.api.readConfig()
|
setAiEnabled(await window.api.isAIEnabled())
|
||||||
if (!cfg.apiKey) {
|
|
||||||
setIsFirstRun(true)
|
|
||||||
setSettingsOpen(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
init()
|
init()
|
||||||
}, [])
|
}, [])
|
||||||
@@ -146,12 +142,16 @@ export default function App(): JSX.Element {
|
|||||||
onClick={() => toggleSeg('sub')}
|
onClick={() => toggleSeg('sub')}
|
||||||
title={submissionPanelOpen ? 'Hide submission panel' : 'Show submission panel'}
|
title={submissionPanelOpen ? 'Hide submission panel' : 'Show submission panel'}
|
||||||
/>
|
/>
|
||||||
|
{aiEnabled && (
|
||||||
|
<>
|
||||||
<div className="app-layout-toggle-seg app-layout-toggle-seg--mid" style={{ width: '4px' }} />
|
<div className="app-layout-toggle-seg app-layout-toggle-seg--mid" style={{ width: '4px' }} />
|
||||||
<button
|
<button
|
||||||
className={`app-layout-toggle-seg${chatOpen ? ' active' : ''}`}
|
className={`app-layout-toggle-seg${chatOpen ? ' active' : ''}`}
|
||||||
onClick={() => toggleSeg('chat')}
|
onClick={() => toggleSeg('chat')}
|
||||||
title={chatOpen ? 'Hide AI chat' : 'Show AI chat'}
|
title={chatOpen ? 'Hide AI chat' : 'Show AI chat'}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className={`app-titlebar-btn${revisionPanelOpen ? ' active' : ''}`}
|
className={`app-titlebar-btn${revisionPanelOpen ? ' active' : ''}`}
|
||||||
@@ -188,11 +188,11 @@ export default function App(): JSX.Element {
|
|||||||
<main className="editor-area">
|
<main className="editor-area">
|
||||||
{activeStoryId ? (
|
{activeStoryId ? (
|
||||||
<>
|
<>
|
||||||
<AnalysisToolbar />
|
{aiEnabled && <AnalysisToolbar />}
|
||||||
<MarkdownEditor />
|
<MarkdownEditor />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Dashboard />
|
<Dashboard aiEnabled={aiEnabled} />
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -202,9 +202,11 @@ export default function App(): JSX.Element {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Chat panel */}
|
{/* Chat panel */}
|
||||||
|
{aiEnabled && (
|
||||||
<aside className="chat-area">
|
<aside className="chat-area">
|
||||||
<ChatPanel />
|
<ChatPanel />
|
||||||
</aside>
|
</aside>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Settings */}
|
{/* Settings */}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ type StoryRow =
|
|||||||
| { kind: 'ready'; storyId: string; storyTitle: string; storyPath: string; wordCount: number }
|
| { kind: 'ready'; storyId: string; storyTitle: string; storyPath: string; wordCount: number }
|
||||||
| { kind: 'accepted' | 'rejected'; storyId: string; storyTitle: string; storyPath: string; marketName: string }
|
| { kind: 'accepted' | 'rejected'; storyId: string; storyTitle: string; storyPath: string; marketName: string }
|
||||||
|
|
||||||
export function Dashboard(): JSX.Element {
|
export function Dashboard({ aiEnabled }: { aiEnabled: boolean }): JSX.Element {
|
||||||
const { stories, submissions, markets, setActiveStory, markSaved, isDirty, activeStoryPath, activeStoryContent } = useBorgesStore()
|
const { stories, submissions, markets, setActiveStory, markSaved, isDirty, activeStoryPath, activeStoryContent } = useBorgesStore()
|
||||||
|
|
||||||
const openStory = async (path: string, id: string): Promise<void> => {
|
const openStory = async (path: string, id: string): Promise<void> => {
|
||||||
@@ -68,7 +68,7 @@ export function Dashboard(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard">
|
<div className="dashboard">
|
||||||
<PromptHero />
|
{aiEnabled && <PromptHero />}
|
||||||
<div className="dashboard-greeting">
|
<div className="dashboard-greeting">
|
||||||
{stories.length === 0
|
{stories.length === 0
|
||||||
? 'Welcome to Borges. Create your first story to get started.'
|
? 'Welcome to Borges. Create your first story to get started.'
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ type Tab = 'general' | 'editor' | 'collection'
|
|||||||
export function SettingsDialog({ onClose }: Props): JSX.Element {
|
export function SettingsDialog({ onClose }: Props): JSX.Element {
|
||||||
const { theme, fontSize, setFontSize } = useBorgesStore()
|
const { theme, fontSize, setFontSize } = useBorgesStore()
|
||||||
const [tab, setTab] = useState<Tab>('general')
|
const [tab, setTab] = useState<Tab>('general')
|
||||||
const [apiKey, setApiKey] = useState('')
|
|
||||||
const [collectionPath, setCollectionPath] = useState('')
|
const [collectionPath, setCollectionPath] = useState('')
|
||||||
const [defaultTarget, setDefaultTarget] = useState('')
|
const [defaultTarget, setDefaultTarget] = useState('')
|
||||||
const [collectionContext, setCollectionContext] = useState('')
|
const [collectionContext, setCollectionContext] = useState('')
|
||||||
@@ -18,7 +17,6 @@ export function SettingsDialog({ onClose }: Props): JSX.Element {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.api.readConfig().then((cfg) => {
|
window.api.readConfig().then((cfg) => {
|
||||||
setApiKey(cfg.apiKey ?? '')
|
|
||||||
setCollectionPath(cfg.collectionPath ?? '')
|
setCollectionPath(cfg.collectionPath ?? '')
|
||||||
setDefaultTarget(String(cfg.defaultWordCountTarget ?? ''))
|
setDefaultTarget(String(cfg.defaultWordCountTarget ?? ''))
|
||||||
})
|
})
|
||||||
@@ -30,7 +28,6 @@ export function SettingsDialog({ onClose }: Props): JSX.Element {
|
|||||||
const save = async (): Promise<void> => {
|
const save = async (): Promise<void> => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
await window.api.writeConfig({
|
await window.api.writeConfig({
|
||||||
apiKey: apiKey.trim() || undefined,
|
|
||||||
collectionPath: collectionPath || undefined,
|
collectionPath: collectionPath || undefined,
|
||||||
defaultWordCountTarget: defaultTarget ? parseInt(defaultTarget) : undefined,
|
defaultWordCountTarget: defaultTarget ? parseInt(defaultTarget) : undefined,
|
||||||
theme
|
theme
|
||||||
@@ -66,16 +63,6 @@ export function SettingsDialog({ onClose }: Props): JSX.Element {
|
|||||||
{tab === 'general' && (
|
{tab === 'general' && (
|
||||||
<div>
|
<div>
|
||||||
<div className="settings-section-title">General</div>
|
<div className="settings-section-title">General</div>
|
||||||
<div className="settings-field">
|
|
||||||
<label className="settings-label">Anthropic API key</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={apiKey}
|
|
||||||
onChange={(e) => setApiKey(e.target.value)}
|
|
||||||
placeholder="sk-ant-…"
|
|
||||||
/>
|
|
||||||
<div className="settings-hint">Used for all AI features (Compression, Ending, Tone, Market fit).</div>
|
|
||||||
</div>
|
|
||||||
<div className="settings-field">
|
<div className="settings-field">
|
||||||
<label className="settings-label">Collection folder</label>
|
<label className="settings-label">Collection folder</label>
|
||||||
<div className="settings-field-row">
|
<div className="settings-field-row">
|
||||||
@@ -121,10 +108,10 @@ export function SettingsDialog({ onClose }: Props): JSX.Element {
|
|||||||
className="context-textarea"
|
className="context-textarea"
|
||||||
value={collectionContext}
|
value={collectionContext}
|
||||||
onChange={(e) => setCollectionContext(e.target.value)}
|
onChange={(e) => setCollectionContext(e.target.value)}
|
||||||
placeholder="Describe the themes, aesthetic, and goals of your collection. This is injected into AI prompts when 'Collection context' is enabled."
|
placeholder="Describe the themes, aesthetic, and goals of your collection."
|
||||||
rows={8}
|
rows={8}
|
||||||
/>
|
/>
|
||||||
<div className="settings-hint">Enable via the 'Collection' toggle in the analysis toolbar.</div>
|
<div className="settings-hint">Used when the 'Collection' context toggle is enabled.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
1
src/renderer/types/global.d.ts
vendored
1
src/renderer/types/global.d.ts
vendored
@@ -55,6 +55,7 @@ declare global {
|
|||||||
loadRevision(path: string, id: string): Promise<string>
|
loadRevision(path: string, id: string): Promise<string>
|
||||||
appendTelemetrySession(session: TelemetrySession): Promise<void>
|
appendTelemetrySession(session: TelemetrySession): Promise<void>
|
||||||
readTelemetry(): Promise<TelemetrySession[]>
|
readTelemetry(): Promise<TelemetrySession[]>
|
||||||
|
isAIEnabled(): Promise<boolean>
|
||||||
readConfig(): Promise<GlobalConfig>
|
readConfig(): Promise<GlobalConfig>
|
||||||
writeConfig(updates: Partial<GlobalConfig>): Promise<void>
|
writeConfig(updates: Partial<GlobalConfig>): Promise<void>
|
||||||
pickFolder(): Promise<string | null>
|
pickFolder(): Promise<string | null>
|
||||||
|
|||||||
Reference in New Issue
Block a user