auto-git:

[add] README.md
 [add] backend/libraries/punk/library.json
 [add] backend/libraries/punk/stage/19f1e5d2ceaab5fd1f1dc58ff07422388f156610d16dfdea2bdb35a5b9e70813--GeorgeJordac-TheVoiceOfHumanJustice.pdf
 [add] backend/libraries/punk/stage/85fce554ff7685f7bccb136aff5768e54b9ba8361672fe45dbce599598c4be4b--4_Strings_-_Take_Me_Away_Into_The_Night_Vocal_Radio_Mix_.mp3
 [add] backend/libraries/punk/stage/e816ca61aebd84159747d248fedd6d5ff318c471c36bcc31b1ac6bf9aebcd3c1--The_Evolution_of_Cooperation_Robert_Axelrod_liber3.pdf
 [add] backend/local_rag.py
 [add] backend/rag/__init__.py
 [add] backend/rag/corpus_builder.py
 [add] backend/rag/corpus_enricher.py
 [add] backend/rag/index_builder.py
 [add] backend/rag/unified_rag.py
 [add] dist/assets/index-Cc0DLWqA.css
 [add] dist/assets/index-DKAz6gtp.js
 [add] dist/index.html
 [add] src/LibraryManager.jsx
 [add] wheelcheck2117/pydantic-2.11.7-py3-none-any.whl
 [add] wheelcheck274/pydantic-2.7.4-py3-none-any.whl
 [change] backend/main.py
 [change] backend/requirements.txt
 [change] backend/schemas.py
 [change] electron/main.cjs
 [change] electron/preload.cjs
 [change] package.json
 [change] run.sh
 [change] src/App.jsx
 [change] src/InterfaceSettings.jsx
 [change] src/colorSchemes.js
 [change] src/main.jsx
 [change] src/styles.css
This commit is contained in:
2026-03-19 21:07:22 +01:00
parent 9c6143a55a
commit 795f4d272a
29 changed files with 6023 additions and 359 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,58 @@
import React, { useState, useEffect } from 'react';
import { colorSchemes, applyColorScheme } from './colorSchemes';
import React, { useEffect, useState } from 'react'
import { colorSchemes, applyColorScheme } from './colorSchemes'
const COLOR_SCHEME_KEY = 'colorScheme';
const COLOR_SCHEME_KEY = 'colorScheme'
const UI_SCALE_KEY = 'uiScale'
const DEFAULT_UI_SCALE = 1
const MIN_UI_SCALE = 0.7
const MAX_UI_SCALE = 1.3
const UI_SCALE_STEP = 0.05
function normalizeUiScale(value) {
const numericValue = Number(value)
if (!Number.isFinite(numericValue)) {
return DEFAULT_UI_SCALE
}
return Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Math.round(numericValue * 100) / 100))
}
export default function InterfaceSettings() {
const [selectedColorScheme, setSelectedColorScheme] = useState('Default');
const [selectedColorScheme, setSelectedColorScheme] = useState('Default')
const [uiScale, setUiScale] = useState(DEFAULT_UI_SCALE)
useEffect(() => {
window.electronAPI.getSettings().then(settings => {
setSelectedColorScheme(settings.colorScheme);
applyColorScheme(settings.colorScheme);
});
}, []);
const schemeName = settings.colorScheme || 'Default'
setSelectedColorScheme(schemeName)
setUiScale(normalizeUiScale(settings.uiScale))
applyColorScheme(schemeName)
})
}, [])
useEffect(() => {
applyColorScheme(selectedColorScheme);
}, [selectedColorScheme]);
applyColorScheme(selectedColorScheme)
}, [selectedColorScheme])
const handleColorSchemeChange = (e) => {
const newScheme = e.target.value;
setSelectedColorScheme(newScheme);
window.electronAPI.setSetting(COLOR_SCHEME_KEY, newScheme);
};
const handleColorSchemeChange = (event) => {
const newScheme = event.target.value
setSelectedColorScheme(newScheme)
window.electronAPI.setSetting(COLOR_SCHEME_KEY, newScheme)
}
const persistUiScale = (value) => {
const nextScale = normalizeUiScale(value)
setUiScale(nextScale)
window.electronAPI.setSetting(UI_SCALE_KEY, nextScale)
}
const handleUiScaleChange = (event) => {
persistUiScale(event.target.value)
}
const handleUiScaleReset = () => {
persistUiScale(DEFAULT_UI_SCALE)
}
return (
<div className="settings-content-panel">
@@ -39,6 +70,32 @@ export default function InterfaceSettings() {
))}
</select>
</div>
<div className="setting-section">
<h3>UI Scale</h3>
<div className="setting-control-row">
<input
type="range"
className="range-input"
min={MIN_UI_SCALE}
max={MAX_UI_SCALE}
step={UI_SCALE_STEP}
value={uiScale}
onChange={handleUiScaleChange}
/>
<span className="setting-value">{Math.round(uiScale * 100)}%</span>
<button
type="button"
className="button"
onClick={handleUiScaleReset}
disabled={uiScale === DEFAULT_UI_SCALE}
>
Reset
</button>
</div>
<p className="setting-description">
Scales the whole interface, including fonts, spacing, and controls. 100% is the default size.
</p>
</div>
</div>
);
)
}

274
src/LibraryManager.jsx Normal file
View File

@@ -0,0 +1,274 @@
import React, { useEffect, useState } from 'react'
function statusLabel(job) {
if (!job) return null
const progress = typeof job.progress === 'number' ? `${job.progress.toFixed(0)}%` : null
const detail = job.detail ? ` ${job.detail}` : ''
return `${job.type} · ${job.status}${progress ? ` · ${progress}` : ''}${detail}`
}
export default function LibraryManager({
apiBase,
library,
jobs,
chatLibrarySlug,
onRefresh,
onToggleChatLibrary,
onDeleted
}) {
const [busy, setBusy] = useState(false)
const [isRenaming, setIsRenaming] = useState(false)
const [renameValue, setRenameValue] = useState('')
const [confirmDelete, setConfirmDelete] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
useEffect(() => {
setIsRenaming(false)
setRenameValue(library?.name || '')
setConfirmDelete(false)
setErrorMessage('')
}, [library?.slug, library?.name])
async function expectOk(response) {
if (response.ok) return response
const detail = await response.text()
throw new Error(detail || `HTTP ${response.status}`)
}
async function runAction(fn) {
setBusy(true)
try {
setErrorMessage('')
await fn()
setConfirmDelete(false)
} finally {
setBusy(false)
await onRefresh()
}
}
async function addPaths() {
if (!library) return
const paths = await window.electronAPI?.pickPaths?.()
if (!Array.isArray(paths) || paths.length === 0) return
try {
await runAction(async () => {
const response = await fetch(`${apiBase}/libraries/${library.slug}/files/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paths })
})
await expectOk(response)
})
} catch (error) {
setErrorMessage(String(error?.message || error))
}
}
async function removeFile(rel) {
if (!library) return
try {
await runAction(async () => {
const response = await fetch(`${apiBase}/libraries/${library.slug}/files`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rel })
})
await expectOk(response)
})
} catch (error) {
setErrorMessage(String(error?.message || error))
}
}
async function renameLibrary() {
if (!library) return
const name = renameValue.trim()
if (!name || name === library.name) {
setIsRenaming(false)
setRenameValue(library.name || '')
return
}
await runAction(async () => {
const response = await fetch(`${apiBase}/libraries/${library.slug}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
})
await expectOk(response)
})
setIsRenaming(false)
}
async function deleteLibrary() {
if (!library) return
await runAction(async () => {
const response = await fetch(`${apiBase}/libraries/${library.slug}`, { method: 'DELETE' })
await expectOk(response)
})
onDeleted?.(library.slug)
}
async function startJob(kind) {
if (!library) return
try {
await runAction(async () => {
const endpoint = `${apiBase}/libraries/${library.slug}/jobs/${kind}`
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}
if (kind === 'embed') {
options.body = JSON.stringify({})
}
const response = await fetch(endpoint, options)
await expectOk(response)
})
} catch (error) {
setErrorMessage(String(error?.message || error))
}
}
if (!library) {
return (
<div className="placeholder-view">
<p>Create a database, add files or folders, then build and index it for local RAG.</p>
</div>
)
}
const activeJobs = (jobs || []).filter(job => job.slug === library.slug && (job.status === 'queued' || job.status === 'running'))
const usingInChat = chatLibrarySlug === library.slug
const canStartRename = () => {
setRenameValue(library.name || '')
setErrorMessage('')
setIsRenaming(true)
setConfirmDelete(false)
}
return (
<div className="library-panel">
{isRenaming && (
<div className="library-inline-form">
<input
type="text"
className="rename-input"
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
renameLibrary().catch((error) => setErrorMessage(String(error?.message || error)))
} else if (e.key === 'Escape') {
setIsRenaming(false)
setRenameValue(library.name || '')
}
}}
autoFocus
/>
<div className="new-db-actions">
<button
className="button"
disabled={busy}
onClick={() => renameLibrary().catch((error) => setErrorMessage(String(error?.message || error)))}
>
Save
</button>
<button
className="button ghost"
onClick={() => {
setIsRenaming(false)
setRenameValue(library.name || '')
}}
>
Cancel
</button>
</div>
</div>
)}
{confirmDelete && (
<div className="library-inline-form danger-zone">
<div className="muted-copy">Delete "{library.name}"? This removes the local index and metadata for this database.</div>
<div className="new-db-actions">
<button
className="button danger"
disabled={busy}
onClick={() => deleteLibrary().catch((error) => setErrorMessage(String(error?.message || error)))}
>
Confirm Delete
</button>
<button className="button ghost" onClick={() => setConfirmDelete(false)}>Cancel</button>
</div>
</div>
)}
{errorMessage && <div className="form-error">{errorMessage}</div>}
<div className="library-toolbar">
<button className="button" disabled={busy} onClick={addPaths}>Add Files</button>
<button className="button" disabled={busy || !library.files?.length} onClick={() => startJob('build')}>Build Corpus</button>
<button className="button" disabled={busy || !library.states?.has_corpus} onClick={() => startJob('enrich')}>Enrich</button>
<button className="button" disabled={busy || !library.states?.has_corpus} onClick={() => startJob('embed')}>Index</button>
<button className="button" onClick={canStartRename}>Rename</button>
<button className="button" onClick={() => onToggleChatLibrary(usingInChat ? null : library.slug)}>
{usingInChat ? 'Stop Using In Chat' : 'Use In Chat'}
</button>
<button
className="button danger"
onClick={() => {
setConfirmDelete(true)
setIsRenaming(false)
setErrorMessage('')
}}
>
Delete
</button>
</div>
<div className="library-states">
<div className={`state-pill ${library.states?.has_files ? 'ready' : ''}`}>Files: {library.files?.length || 0}</div>
<div className={`state-pill ${library.states?.has_corpus ? 'ready' : ''}`}>Corpus: {library.artifacts?.corpus_records || 0}</div>
<div className={`state-pill ${library.states?.is_enriched ? 'ready' : ''}`}>Enriched: {library.artifacts?.enhanced_records || 0}</div>
<div className={`state-pill ${library.states?.is_indexed ? 'ready' : ''}`}>Indexed</div>
</div>
{usingInChat && (
<div className="library-chat-note">
This database will be queried before each chat request and its context will be appended to the prompt.
</div>
)}
{activeJobs.length > 0 && (
<div className="library-jobs">
{activeJobs.map(job => (
<div key={job.id} className={`job-card ${job.status}`}>
{statusLabel(job)}
</div>
))}
</div>
)}
<div className="library-files">
<h2>Files</h2>
{library.files?.length ? (
<div className="library-file-list">
{library.files.map(file => (
<div key={file.sha256 || file.rel} className="library-file-row">
<div className="library-file-meta">
<div className="library-file-name">{file.name || file.path}</div>
<div className="library-file-path">{file.path}</div>
</div>
<div className="library-file-actions">
<button className="button ghost" onClick={() => window.electronAPI?.openPath?.(file.path)}>Open</button>
<button className="button ghost" onClick={() => removeFile(file.rel)}>Remove</button>
</div>
</div>
))}
</div>
) : (
<p className="muted-copy">No files registered yet.</p>
)}
</div>
</div>
)
}

View File

@@ -1,5 +1,18 @@
const colorSchemes = {
'Nightsky': {
Default: {
'--bg': '#0b1020',
'--panel': '#141b34',
'--text': '#e6e8ef',
'--muted': '#9aa3b2',
'--accent': '#6ea8fe',
'--border': '#24304f',
'--input-bg': '#121933',
'--user-msg-bg': '#18213d',
'--assistant-msg-bg': '#10172d',
'--active-bg': 'rgba(110, 168, 254, 0.16)',
'--hover-bg': 'rgba(255, 255, 255, 0.06)',
},
Nightsky: {
'--bg': '#0a0e1a',
'--panel': '#18203a',
'--text': '#ffffff',
@@ -12,7 +25,7 @@ const colorSchemes = {
'--active-bg': 'rgba(74, 144, 226, 0.15)',
'--hover-bg': 'rgba(255, 255, 255, 0.05)',
},
'Grayscale': {
Grayscale: {
'--bg': '#1a1a1a',
'--panel': '#2a2a2a',
'--text': '#f0f0f0',
@@ -25,33 +38,33 @@ const colorSchemes = {
'--active-bg': 'rgba(136, 136, 136, 0.15)',
'--hover-bg': 'rgba(255, 255, 255, 0.05)',
},
'Japan': {
Japan: {
'--bg': '#ffffff',
'--panel': '#f5f5f5',
'--text': '#000000',
'--muted': '#444444',
'--accent': '#e74c3c', /* Vibrant Red */
'--accent': '#e74c3c',
'--border': '#999999',
'--input-bg': '#ffffff',
'--user-msg-bg': '#f0f0f0',
'--assistant-msg-bg': '#f0f0f0',
'--active-bg': 'rgba(231, 76, 60, 0.15)', /* Light red for active */
'--hover-bg': 'rgba(231, 76, 60, 0.08)', /* Lighter red for hover */
'--active-bg': 'rgba(231, 76, 60, 0.15)',
'--hover-bg': 'rgba(231, 76, 60, 0.08)',
},
'Lime': {
Lime: {
'--bg': '#f0fff0',
'--panel': '#e0ffe0',
'--text': '#1a1a1a',
'--muted': '#72a272ff',
'--accent': '#deef88',
'--accent': '#8e9f38ff',
'--border': '#a0c0a0',
'--input-bg': '#ffffff',
'--user-msg-bg': '#f8f7ad',
'--user-msg-bg': '#f8f7adff',
'--assistant-msg-bg': '#f5fff5',
'--active-bg': 'rgba(104, 159, 56, 0.2)',
'--hover-bg': 'rgba(104, 159, 56, 0.1)',
},
'Vampire': {
Vampire: {
'--bg': '#1a050a',
'--panel': '#2a1015',
'--text': '#ffefff',
@@ -64,15 +77,80 @@ const colorSchemes = {
'--active-bg': 'rgba(216, 27, 96, 0.15)',
'--hover-bg': 'rgba(255, 255, 255, 0.05)',
},
};
'Sunset Drive': {
'--bg': '#1f1024',
'--panel': '#2e1632',
'--text': '#fff2ea',
'--muted': '#caa8b7',
'--accent': '#ff8a5b',
'--border': '#593050',
'--input-bg': '#26132a',
'--user-msg-bg': '#442038',
'--assistant-msg-bg': '#32172c',
'--active-bg': 'rgba(255, 138, 91, 0.18)',
'--hover-bg': 'rgba(255, 138, 91, 0.08)',
},
'Aurora Pulse': {
'--bg': '#07171d',
'--panel': '#102730',
'--text': '#eafcff',
'--muted': '#9bc8cf',
'--accent': '#54f2c2',
'--border': '#214853',
'--input-bg': '#0b2028',
'--user-msg-bg': '#12313d',
'--assistant-msg-bg': '#0f2530',
'--active-bg': 'rgba(84, 242, 194, 0.18)',
'--hover-bg': 'rgba(84, 242, 194, 0.08)',
},
'Sakura Neon': {
'--bg': '#160b1d',
'--panel': '#251331',
'--text': '#fff5fd',
'--muted': '#d4abc7',
'--accent': '#ff4fb6',
'--border': '#52315f',
'--input-bg': '#1d1027',
'--user-msg-bg': '#341844',
'--assistant-msg-bg': '#281534',
'--active-bg': 'rgba(255, 79, 182, 0.18)',
'--hover-bg': 'rgba(255, 79, 182, 0.09)',
},
'Cobalt Punch': {
'--bg': '#081527',
'--panel': '#102643',
'--text': '#eef6ff',
'--muted': '#9fb7d0',
'--accent': '#ffb703',
'--border': '#234164',
'--input-bg': '#0d1f37',
'--user-msg-bg': '#162f54',
'--assistant-msg-bg': '#102640',
'--active-bg': 'rgba(255, 183, 3, 0.18)',
'--hover-bg': 'rgba(255, 183, 3, 0.08)',
},
'Mango Mojito': {
'--bg': '#fff7ea',
'--panel': '#ffe9c8',
'--text': '#2a1c13',
'--muted': '#7c6150',
'--accent': '#ff6b35',
'--border': '#e6bf91',
'--input-bg': '#fffdf9',
'--user-msg-bg': '#fff0d7',
'--assistant-msg-bg': '#fff8ed',
'--active-bg': 'rgba(255, 107, 53, 0.14)',
'--hover-bg': 'rgba(255, 107, 53, 0.08)',
},
}
function applyColorScheme(schemeName) {
const scheme = colorSchemes[schemeName];
if (scheme) {
for (const [key, value] of Object.entries(scheme)) {
document.documentElement.style.setProperty(key, value);
}
const scheme = colorSchemes[schemeName] || colorSchemes.Default
if (!scheme) return
for (const [key, value] of Object.entries(scheme)) {
document.documentElement.style.setProperty(key, value)
}
}
export { colorSchemes, applyColorScheme };
export { colorSchemes, applyColorScheme }

View File

@@ -9,9 +9,7 @@ import { applyColorScheme } from './colorSchemes'
function Main() {
useEffect(() => {
window.electronAPI.getSettings().then(settings => {
if (settings.colorScheme) {
applyColorScheme(settings.colorScheme)
}
applyColorScheme(settings.colorScheme || 'Default')
})
}, [])

View File

@@ -245,6 +245,22 @@ body { background: var(--bg); color: var(--text); font-family: ui-sans-serif, sy
background: var(--panel);
}
.new-db-form,
.library-inline-form {
display: grid;
gap: 8px;
}
.new-db-actions {
display: flex;
gap: 8px;
}
.form-error {
color: #ff9aa8;
font-size: 12px;
}
.new-chat-button {
width: 100%;
padding: 10px;
@@ -291,6 +307,13 @@ body { background: var(--bg); color: var(--text); font-family: ui-sans-serif, sy
.select { min-width: 220px; }
.button { cursor: pointer; }
.button:hover { border-color: var(--accent); }
.button.ghost { background: transparent; }
.button.danger { border-color: #8f3d49; color: #ffb8c2; }
.button.danger:hover { border-color: #d86a79; }
.header-subtle {
color: var(--muted);
font-size: 13px;
}
.chat {
display: grid;
@@ -510,6 +533,30 @@ textarea.input {
min-width: unset;
}
.setting-control-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.range-input {
width: min(360px, 100%);
accent-color: var(--accent);
}
.setting-value {
min-width: 48px;
color: var(--text);
font-variant-numeric: tabular-nums;
}
.setting-description {
margin: 10px 0 0;
color: var(--muted);
line-height: 1.5;
}
/* Markdown Styles */
.msg h1, .msg h2, .msg h3, .msg h4 {
margin: 10px 0;
@@ -972,3 +1019,113 @@ input:checked + .slider:before {
white-space: nowrap;
margin-top: 0.5rem;
}
.db-active-badge {
margin-left: 8px;
padding: 2px 8px;
border-radius: 999px;
background: color-mix(in srgb, var(--accent) 20%, transparent);
color: var(--accent);
font-size: 11px;
}
.placeholder-view,
.library-panel {
overflow: auto;
padding: 20px;
}
.placeholder-view h1 {
margin-top: 0;
}
.library-toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 18px;
}
.library-states {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 14px;
}
.state-pill {
padding: 6px 10px;
border-radius: 999px;
border: 1px solid var(--border);
color: var(--muted);
font-size: 13px;
}
.state-pill.ready {
color: var(--text);
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}
.library-chat-note,
.job-card {
margin-bottom: 12px;
padding: 12px 14px;
border-radius: 12px;
background: color-mix(in srgb, var(--panel) 82%, black);
border: 1px solid var(--border);
}
.library-inline-form {
margin-bottom: 14px;
padding: 12px 14px;
border-radius: 12px;
border: 1px solid var(--border);
background: color-mix(in srgb, var(--panel) 88%, black);
}
.danger-zone {
border-color: #8f3d49;
}
.library-files h2 {
margin: 18px 0 12px;
font-size: 16px;
}
.library-file-list {
display: grid;
gap: 10px;
}
.library-file-row {
display: flex;
justify-content: space-between;
gap: 14px;
align-items: flex-start;
padding: 12px 14px;
border-radius: 12px;
border: 1px solid var(--border);
background: color-mix(in srgb, var(--panel) 88%, black);
}
.library-file-meta {
min-width: 0;
}
.library-file-name {
font-weight: 600;
margin-bottom: 4px;
}
.library-file-path,
.muted-copy {
color: var(--muted);
font-size: 13px;
word-break: break-word;
}
.library-file-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}