Skip to content

Commit f51dff1

Browse files
authored
Merge pull request #7 from VariableThe/fix/openrouter-defaults-and-settings
fix: updated openrouter defaults and settings window
2 parents e90bccf + f4be610 commit f51dff1

10 files changed

Lines changed: 251 additions & 35 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,11 @@ Built with Electron, React, TypeScript, and Vite.
8787

8888
## AI setup
8989

90-
PaperCache uses your own OpenAI API key — no subscription, no middleman. Configure your key, model, and optionally a custom endpoint (works with local LLMs like Ollama) in Settings (`Cmd+K` → Settings).
90+
PaperCache brings AI right into your note — no subscription, no middleman. You can configure your API key, model, and endpoint in Settings (`Cmd+Shift+S`).
91+
92+
- **OpenAI:** Works out-of-the-box with your OpenAI API key and models like `gpt-4o`.
93+
- **Free Models (OpenRouter):** Don't have an AI subscription? Get a free key at [OpenRouter](https://openrouter.ai/keys), set your Base URL to `https://openrouter.ai/api/v1`, and try a powerful free model like `nvidia/nemotron-3-super-120b-a12b:free`.
94+
- **Local LLMs:** Works seamlessly with local models like Ollama by pointing the Base URL to your local instance.
9195

9296
---
9397

electron/main.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -571,9 +571,16 @@ ipcMain.on('open-settings', () => {
571571
return
572572
}
573573

574+
let bounds: any = { width: 900, height: 700 }
575+
if (win && !win.isDestroyed()) {
576+
bounds = win.getBounds()
577+
}
578+
574579
settingsWin = new BrowserWindow({
575-
width: 900,
576-
height: 700,
580+
width: bounds.width,
581+
height: bounds.height,
582+
x: bounds.x,
583+
y: bounds.y,
577584
titleBarStyle: 'hiddenInset',
578585
icon: path.join(__dirname, '../public/icon.png'),
579586
webPreferences: {
@@ -592,6 +599,9 @@ ipcMain.on('open-settings', () => {
592599

593600
settingsWin.on('closed', () => {
594601
settingsWin = null
602+
if (win && !win.isDestroyed()) {
603+
win.show()
604+
}
595605
})
596606
})
597607

fix_ts.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import os
2+
3+
def fix_file(filepath, replacements):
4+
with open(filepath, "r") as f:
5+
code = f.read()
6+
for old, new in replacements:
7+
code = code.replace(old, new)
8+
with open(filepath, "w") as f:
9+
f.write(code)
10+
11+
fix_file("src/hooks/useNoteStorage.ts", [
12+
("import { Note } from '../store/useAppStore'", "import type { Note } from '../store/useAppStore'"),
13+
("note.content", "note?.content || ''")
14+
])
15+
16+
fix_file("src/hooks/useReminders.ts", [
17+
("body: label,", "body: label || '',")
18+
])
19+
20+
fix_file("src/hooks/useVariables.ts", [
21+
("const name = varMatch[1]", "const name = varMatch[1]!"),
22+
("globals[name] = mathjs.evaluate(varMatch[2]", "globals[name] = mathjs.evaluate(varMatch[2]!"),
23+
("globals[name] = varMatch[2].trim()", "globals[name] = varMatch[2]!.trim()")
24+
])
25+
26+
fix_file("src/lib/editor/plugins.ts", [
27+
("const name = match[1]", "const name = match[1]!"),
28+
("mathjs.evaluate(match[2]", "mathjs.evaluate(match[2]!"),
29+
("scope[name] = match[2].trim()", "scope[name] = match[2]!.trim()"),
30+
("const targetStr = match[4]", "const targetStr = match[4] || ''"),
31+
("const targetMs = new Date(targetStr).getTime()", "const targetMs = targetStr ? new Date(targetStr).getTime() : 0"),
32+
("const label = match[3]", "const label = match[3] || ''"),
33+
("const exprPart = calcMatch[1]", "const exprPart = calcMatch[1]!"),
34+
("const oldResult = calcMatch[2]", "const oldResult = calcMatch[2]!"),
35+
("console.log(e)", ""),
36+
("const url = match[1]", "const url = match[1]!"),
37+
("const path = match[1]", "const path = match[1]!"),
38+
("const isDone = match[1] ===", "const isDone = match[1]! ==="),
39+
("text.slice(match.index + 2, match.index + match[0].length - 2)", "text.slice(match.index + 2, match.index + match[0]!.length - 2)")
40+
])
41+
42+
fix_file("src/utils.ts", [
43+
("const lastPart = parts.pop()", "const lastPart = parts.pop() || ''"),
44+
("parts.length > 0", "parts && parts.length > 0")
45+
])
46+
47+
fix_file("src/utils.test.ts", [
48+
("const lastPart = parts.pop()", "const lastPart = parts.pop() || ''")
49+
])
50+
51+
fix_file("src/App.tsx", [
52+
("const name = match[1]", "const name = match[1]!"),
53+
("mathjs.evaluate(match[2]", "mathjs.evaluate(match[2]!"),
54+
("scope[name] = match[2].trim()", "scope[name] = match[2]!.trim()"),
55+
("const exprPart = calcMatch[1]", "const exprPart = calcMatch[1]!"),
56+
("const oldResult = calcMatch[2]", "const oldResult = calcMatch[2]!"),
57+
("const filename = note.id.replace", "const filename = note?.id.replace"),
58+
("note.content.split", "note?.content.split"),
59+
("selNote.id", "selNote?.id"),
60+
("selNote.content", "selNote?.content")
61+
])
62+
63+
fix_file("src/components/RemindersPage.tsx", [
64+
("const label = match[3]", "const label = match[3] || ''")
65+
])
66+
67+
fix_file("src/GraphView.tsx", [
68+
("targetId: match[1]", "targetId: match[1]!")
69+
])
70+

src/App.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,3 +704,21 @@ body {
704704
.cm-overdue-line-text {
705705
color: #ff3b30 !important;
706706
}
707+
708+
.cm-ctx-pill {
709+
display: inline-block;
710+
padding: 0 6px;
711+
border-radius: 4px;
712+
background-color: rgba(139, 92, 246, 0.15);
713+
color: #8b5cf6;
714+
font-size: 0.9em;
715+
font-weight: bold;
716+
border: 1px solid rgba(139, 92, 246, 0.3);
717+
margin-right: 6px;
718+
vertical-align: middle;
719+
}
720+
721+
.cm-ctx-highlight {
722+
color: #8b5cf6;
723+
background-color: rgba(139, 92, 246, 0.1);
724+
}

src/App.tsx

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,10 @@ function App() {
130130
})
131131
// Refresh AI Store (API Key handled securely, we don't listen to localStorage for it directly)
132132
useAIStore.setState({
133-
apiBaseUrl: localStorage.getItem('papercache-api-base-url') || 'https://api.openai.com/v1',
134-
apiModel: localStorage.getItem('papercache-api-model') || 'gpt-4o',
133+
apiBaseUrl:
134+
localStorage.getItem('papercache-api-base-url') || 'https://openrouter.ai/api/v1',
135+
apiModel:
136+
localStorage.getItem('papercache-api-model') || 'nvidia/nemotron-3-super-120b-a12b:free',
135137
aiSystemPrompt:
136138
localStorage.getItem('papercache-ai-system-prompt') ||
137139
'You are a helpful assistant directly inside a markdown note. You can format your responses with markdown.',
@@ -366,8 +368,18 @@ function App() {
366368
const line = view.state.doc.lineAt(pos)
367369
const lineText = line.text.trim()
368370
const lowerLine = lineText.toLowerCase()
369-
if (lowerLine.startsWith('/ai')) {
370-
const prompt = lineText.substring(3).trim()
371+
if (
372+
lowerLine.startsWith('/ai') ||
373+
lowerLine.startsWith('/ctx') ||
374+
lowerLine.startsWith('/context')
375+
) {
376+
const isCtx = lowerLine.startsWith('/ctx') || lowerLine.startsWith('/context')
377+
const prefixLength = lowerLine.startsWith('/context')
378+
? 8
379+
: lowerLine.startsWith('/ctx')
380+
? 4
381+
: 3
382+
const prompt = lineText.substring(prefixLength).trim()
371383
if (!apiKey) {
372384
const errorText = '\n\u200BError - Set your OpenAI API key in settings\u200C\n'
373385
view.dispatch({ changes: { from: line.to, insert: errorText } })
@@ -390,22 +402,48 @@ function App() {
390402
apiKey: apiKey.trim() || 'dummy',
391403
baseURL: finalBaseUrl || undefined,
392404
dangerouslyAllowBrowser: true,
405+
defaultHeaders: {
406+
'HTTP-Referer': 'https://github.com/papercache/papercache',
407+
'X-Title': 'PaperCache',
408+
},
393409
})
394410

395411
const systemContent = aiSystemPrompt.trim()
396412
const messages: any[] = []
397413
if (systemContent) {
398414
messages.push({ role: 'system', content: systemContent })
399415
}
400-
messages.push({ role: 'user', content: prompt })
416+
417+
let finalPrompt = prompt
418+
if (isCtx) {
419+
const fullNoteText = view.state.doc.toString()
420+
const MAX_CONTEXT_LENGTH = 50000
421+
let contextText = fullNoteText
422+
if (contextText.length > MAX_CONTEXT_LENGTH) {
423+
contextText =
424+
contextText.substring(0, MAX_CONTEXT_LENGTH) +
425+
'\n...[Context truncated due to length]'
426+
}
427+
finalPrompt = `Context:\n${contextText}\n\nPrompt:\n${prompt}`
428+
}
429+
430+
messages.push({ role: 'user', content: finalPrompt })
401431

402432
openai.chat.completions
403433
.create({
404-
model: apiModel.trim() || 'gpt-4o',
434+
model: apiModel.trim() || 'nvidia/nemotron-3-super-120b-a12b:free',
405435
messages: messages,
406436
})
407-
.then((completion) => {
408-
const response = completion.choices[0].message.content
437+
.then((completion: any) => {
438+
let response: string
439+
if (completion.choices && completion.choices.length > 0) {
440+
response = completion.choices[0].message?.content || ''
441+
} else if (completion.error) {
442+
throw new Error(completion.error.message || 'Unknown API Error')
443+
} else {
444+
throw new Error('Unexpected response format: ' + JSON.stringify(completion))
445+
}
446+
409447
const docStr = view.state.doc.toString()
410448
const finalVal = docStr.replace(
411449
'\n\u200B...\u200C\n',

src/Settings.css

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,16 +111,14 @@ section h3 {
111111
}
112112

113113
.save-btn {
114-
width: 100%;
114+
flex: 1;
115115
padding: 12px;
116116
background: #fcfcfc;
117117
color: #000;
118118
border: none;
119119
border-radius: 6px;
120120
font-weight: bold;
121121
cursor: pointer;
122-
margin-top: 10px;
123-
margin-bottom: 24px;
124122
}
125123

126124
.save-btn:hover {
@@ -130,10 +128,12 @@ section h3 {
130128
.settings-footer {
131129
margin-top: auto;
132130
-webkit-app-region: no-drag;
131+
display: flex;
132+
gap: 10px;
133133
}
134134

135135
.quit-btn {
136-
width: 100%;
136+
flex: 1;
137137
padding: 12px;
138138
background: #2a2a2a;
139139
color: #ff4444;
@@ -146,3 +146,18 @@ section h3 {
146146
.quit-btn:hover {
147147
background: rgba(255, 68, 68, 0.1);
148148
}
149+
150+
.close-btn {
151+
flex: 1;
152+
padding: 12px;
153+
background: #2a2a2a;
154+
color: #fff;
155+
border: 1px solid #666;
156+
border-radius: 6px;
157+
font-weight: bold;
158+
cursor: pointer;
159+
}
160+
161+
.close-btn:hover {
162+
background: #333;
163+
}

src/Settings.tsx

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
1-
import { useState } from 'react'
1+
import { useState, useEffect } from 'react'
2+
import { getSecure, setSecure } from './lib/safeStorage'
23
import './Settings.css'
34

45
export default function Settings() {
5-
const [apiKey, setApiKey] = useState(localStorage.getItem('papercache-apikey') || '')
6+
const [apiKey, setApiKey] = useState('')
7+
8+
useEffect(() => {
9+
getSecure('papercache-apikey').then((key) => {
10+
if (key) setApiKey(key)
11+
})
12+
13+
const handleKeyDown = (e: KeyboardEvent) => {
14+
if (e.key === 'Escape') {
15+
window.electronAPI.closeWindow()
16+
}
17+
}
18+
window.addEventListener('keydown', handleKeyDown)
19+
return () => window.removeEventListener('keydown', handleKeyDown)
20+
}, [])
621
const [apiBaseUrl, setApiBaseUrl] = useState(
7-
localStorage.getItem('papercache-baseurl') || 'https://api.openai.com/v1'
22+
localStorage.getItem('papercache-api-base-url') || 'https://openrouter.ai/api/v1'
23+
)
24+
const [apiModel, setApiModel] = useState(
25+
localStorage.getItem('papercache-api-model') || 'nvidia/nemotron-3-super-120b-a12b:free'
826
)
9-
const [apiModel, setApiModel] = useState(localStorage.getItem('papercache-model') || 'gpt-4o')
1027
const [aiSystemPrompt, setAiSystemPrompt] = useState(
11-
localStorage.getItem('papercache-system-prompt') || 'Please provide a short and concise answer.'
28+
localStorage.getItem('papercache-ai-system-prompt') ||
29+
'Please provide a short and concise answer.'
1230
)
1331

1432
// Shortcuts
@@ -52,11 +70,12 @@ export default function Settings() {
5270
localStorage.getItem('papercache-color-math') || '#f59e0b'
5371
)
5472

55-
const saveSettings = () => {
56-
localStorage.setItem('papercache-apikey', apiKey)
57-
localStorage.setItem('papercache-baseurl', apiBaseUrl)
58-
localStorage.setItem('papercache-model', apiModel)
59-
localStorage.setItem('papercache-system-prompt', aiSystemPrompt)
73+
const saveSettings = async () => {
74+
await setSecure('papercache-apikey', apiKey)
75+
localStorage.removeItem('papercache-apikey')
76+
localStorage.setItem('papercache-api-base-url', apiBaseUrl)
77+
localStorage.setItem('papercache-api-model', apiModel)
78+
localStorage.setItem('papercache-ai-system-prompt', aiSystemPrompt)
6079

6180
localStorage.setItem('papercache-font', fontFamily)
6281
localStorage.setItem('papercache-show-rulings', showRulings.toString())
@@ -98,6 +117,10 @@ export default function Settings() {
98117
window.electronAPI.closeWindow() // actually closes settings window
99118
}
100119

120+
const closeSettings = () => {
121+
window.electronAPI.closeWindow()
122+
}
123+
101124
const quitApp = () => {
102125
window.electronAPI.quitApp()
103126
}
@@ -127,7 +150,7 @@ export default function Settings() {
127150
type="text"
128151
value={apiBaseUrl}
129152
onChange={(e) => setApiBaseUrl(e.target.value)}
130-
placeholder="https://api.openai.com/v1"
153+
placeholder="https://openrouter.ai/api/v1"
131154
/>
132155
</div>
133156

@@ -137,7 +160,7 @@ export default function Settings() {
137160
type="text"
138161
value={apiModel}
139162
onChange={(e) => setApiModel(e.target.value)}
140-
placeholder="gpt-4o"
163+
placeholder="nvidia/nemotron-3-super-120b-a12b:free"
141164
/>
142165
</div>
143166

@@ -291,15 +314,17 @@ export default function Settings() {
291314
<input type="color" value={aiColor} onChange={(e) => setAiColor(e.target.value)} />
292315
</div>
293316
</section>
294-
295-
<button className="save-btn" onClick={saveSettings}>
296-
Save Settings
297-
</button>
298317
</div>
299318

300319
<div className="settings-footer">
301320
<button className="quit-btn" onClick={quitApp}>
302-
Quit PaperCache
321+
Quit
322+
</button>
323+
<button className="close-btn" onClick={closeSettings}>
324+
Close Settings
325+
</button>
326+
<button className="save-btn" onClick={saveSettings}>
327+
Save Settings
303328
</button>
304329
</div>
305330
</div>

0 commit comments

Comments
 (0)