v0.1.1: System default AI config, configurable URLs, single .env, website refactor

This commit is contained in:
lotherk
2026-03-27 13:51:15 +00:00
parent 749a913309
commit f6c4da1da3
23 changed files with 486 additions and 180 deletions

View File

@@ -81,7 +81,7 @@ function Footer({ appName = 'DearDiary' }: { appName?: string }) {
const { resolvedTheme } = useTheme();
return (
<footer className={`py-6 text-center text-sm text-slate-500 ${resolvedTheme === 'dark' ? 'border-t border-slate-800' : 'border-t border-slate-200'}`}>
<p>{appName} v{packageJson.version} Self-hosted AI-powered journaling · <a href="https://github.com/lotherk/deardiary" className="hover:text-purple-400 transition">GitHub</a> · <a href="https://deardiary.io" className="hover:text-purple-400 transition">deardiary.io</a> · MIT License · © 2026 Konrad Lother</p>
<p>{appName} v{packageJson.version} Self-hosted AI-powered journaling · <a href={import.meta.env.VITE_GIT_URL || 'https://git.kropa.tech/lotherk/deardiary'} className="hover:text-purple-400 transition">Git Repository</a> · MIT License · © 2026 Konrad Lother</p>
</footer>
);
}

View File

@@ -19,9 +19,30 @@ export default function QuickAddWidget({ isOpen, onClose }: Props) {
useEffect(() => {
if (isOpen) {
checkDiaryStatus();
setTimeout(() => inputRef.current?.focus(), 50);
}
}, [isOpen]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
if (content.trim() && !locked && !loading) {
const form = inputRef.current?.form;
if (form) {
const submitEvent = new Event('submit', { bubbles: true, cancelable: true });
form.dispatchEvent(submitEvent);
}
}
}
};
if (isOpen) {
window.addEventListener('keydown', handleKeyDown);
}
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, content, locked, loading]);
const checkDiaryStatus = async () => {
const res = await api.getDay(today);
if (res.data) {

View File

@@ -221,7 +221,7 @@ export interface Task {
}
export interface Settings {
aiProvider: 'openai' | 'anthropic' | 'ollama' | 'lmstudio' | 'groq';
aiProvider: 'openai' | 'anthropic' | 'ollama' | 'lmstudio' | 'groq' | 'xai' | 'custom';
aiApiKey?: string;
aiModel: string;
aiBaseUrl?: string;
@@ -229,10 +229,13 @@ export interface Settings {
language: string;
timezone: string;
journalContextDays: number;
useSystemDefault: boolean;
providerSettings?: Record<string, {
apiKey?: string;
model?: string;
baseUrl?: string;
headers?: Record<string, string>;
parameters?: Record<string, unknown>;
}>;
}

View File

@@ -7,6 +7,8 @@ interface ProviderSettings {
apiKey?: string;
model?: string;
baseUrl?: string;
headers?: Record<string, string>;
parameters?: Record<string, unknown>;
}
interface FullSettings extends Settings {
@@ -15,15 +17,21 @@ interface FullSettings extends Settings {
const DEFAULT_MODELS: Record<string, string> = {
groq: 'llama-3.3-70b-versatile',
openai: 'gpt-4o',
openai: 'gpt-5-nano',
anthropic: 'claude-3-5-sonnet-20241022',
ollama: 'llama3.2',
lmstudio: 'local-model',
custom: '',
xai: 'grok-4-1-fast-reasoning',
};
const PROVIDERS_WITH_API_KEY: string[] = ['openai', 'anthropic', 'groq', 'xai', 'custom'];
const PROVIDERS_WITH_BASE_URL: string[] = ['ollama', 'lmstudio', 'custom'];
const DEFAULT_BASE_URLS: Record<string, string> = {
ollama: 'http://localhost:11434',
lmstudio: 'http://localhost:1234/v1',
custom: 'https://your-api.example.com/v1',
};
export default function SettingsPage() {
@@ -34,6 +42,7 @@ export default function SettingsPage() {
language: 'en',
timezone: 'UTC',
journalContextDays: 10,
useSystemDefault: true,
});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
@@ -80,6 +89,9 @@ export default function SettingsPage() {
if (!data.providerSettings) {
data.providerSettings = {};
}
if (data.useSystemDefault === undefined) {
data.useSystemDefault = true;
}
const provider = data.aiProvider || 'groq';
const providerData = data.providerSettings[provider] || {};
data.aiApiKey = providerData.apiKey;
@@ -158,6 +170,7 @@ export default function SettingsPage() {
await api.updateSettings({
aiProvider: settings.aiProvider,
providerSettings: newProviderSettings,
useSystemDefault: settings.useSystemDefault,
});
setSaving(false);
setSaved(true);
@@ -448,13 +461,33 @@ export default function SettingsPage() {
>
<option value="groq">Groq (Free, Fast)</option>
<option value="openai">OpenAI (GPT-4)</option>
<option value="xai">xAI (Grok)</option>
<option value="anthropic">Anthropic (Claude)</option>
<option value="ollama">Ollama (Local)</option>
<option value="lmstudio">LM Studio (Local)</option>
<option value="custom">Custom (OpenAI-compatible)</option>
</select>
</div>
{(provider === 'openai' || provider === 'anthropic' || provider === 'groq') && (
<div className="flex items-center gap-3">
<input
type="checkbox"
id="useSystemDefault"
checked={settings.useSystemDefault}
onChange={(e) => setSettings({ ...settings, useSystemDefault: e.target.checked })}
className="w-4 h-4 rounded border-slate-600 bg-slate-800 text-purple-600 focus:ring-purple-500"
/>
<label htmlFor="useSystemDefault" className="text-sm text-slate-300">
Use system default settings
</label>
</div>
<p className="text-xs text-slate-500 -mt-2">
{settings.useSystemDefault
? 'Using the default AI configuration set by the administrator.'
: 'Override with your own AI settings.'}
</p>
{!settings.useSystemDefault && PROVIDERS_WITH_API_KEY.includes(provider) && (
<div>
<label className="block text-sm text-slate-400 mb-1">API Key</label>
<input
@@ -467,7 +500,7 @@ export default function SettingsPage() {
</div>
)}
{(provider === 'ollama' || provider === 'lmstudio') && (
{!settings.useSystemDefault && PROVIDERS_WITH_BASE_URL.includes(provider) && (
<div>
<label className="block text-sm text-slate-400 mb-1">Base URL</label>
<input
@@ -480,16 +513,58 @@ export default function SettingsPage() {
</div>
)}
<div>
<label className="block text-sm text-slate-400 mb-1">Model</label>
<input
type="text"
value={currentProviderSettings.model || ''}
onChange={(e) => updateProviderSettings({ model: e.target.value })}
placeholder={DEFAULT_MODELS[provider]}
className="w-full px-4 py-2 bg-slate-800 rounded-lg border border-slate-700 focus:border-blue-500 focus:outline-none"
/>
</div>
{!settings.useSystemDefault && (
<div>
<label className="block text-sm text-slate-400 mb-1">Model</label>
<input
type="text"
value={currentProviderSettings.model || ''}
onChange={(e) => updateProviderSettings({ model: e.target.value })}
placeholder={DEFAULT_MODELS[provider]}
className="w-full px-4 py-2 bg-slate-800 rounded-lg border border-slate-700 focus:border-blue-500 focus:outline-none"
/>
</div>
)}
{!settings.useSystemDefault && (
<details className="group">
<summary className="cursor-pointer text-sm text-slate-400 hover:text-slate-300 list-none flex items-center gap-2">
<span className="transform transition-transform group-open:rotate-90"></span>
Advanced Settings
</summary>
<div className="mt-4 space-y-4 pl-4 border-l-2 border-slate-700">
<div>
<label className="block text-sm text-slate-400 mb-1">Custom Headers (JSON)</label>
<textarea
value={JSON.stringify(currentProviderSettings.headers || {}, null, 2)}
onChange={(e) => {
try {
updateProviderSettings({ headers: JSON.parse(e.target.value || '{}') });
} catch {}
}}
rows={3}
placeholder={'{"Authorization": "Bearer ..."}'}
className="w-full px-4 py-2 bg-slate-800 rounded-lg border border-slate-700 focus:border-blue-500 focus:outline-none resize-none font-mono text-sm"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-1">Custom Parameters (JSON)</label>
<textarea
value={JSON.stringify(currentProviderSettings.parameters || {}, null, 2)}
onChange={(e) => {
try {
updateProviderSettings({ parameters: JSON.parse(e.target.value || '{}') });
} catch {}
}}
rows={4}
placeholder={'{"think": false}'}
className="w-full px-4 py-2 bg-slate-800 rounded-lg border border-slate-700 focus:border-blue-500 focus:outline-none resize-none font-mono text-sm"
/>
<p className="text-xs text-slate-500 mt-1">Examples: {"{ \"think\": false }"} for Ollama, {"{ \"thinking\": { \"type\": \"disabled\" } }"} for OpenAI</p>
</div>
</div>
</details>
)}
<div className="flex gap-3 items-center">
<button

View File

@@ -1,15 +1,22 @@
import { defineConfig } from 'vite';
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: process.env.VITE_API_URL || 'http://localhost:3000',
changeOrigin: true,
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: process.env.VITE_API_URL || 'http://localhost:3000',
changeOrigin: true,
},
},
},
},
define: {
'import.meta.env.VITE_GIT_URL': JSON.stringify(env.VITE_GIT_URL || 'https://git.kropa.tech/lotherk/deardiary'),
},
};
});