feat: add theme system, branding, and task logging

- Add light/dark/system theme toggle in settings
- Add DearDiary.io branding in navbar
- Add task logging for journal generation with request/response
- Rename project from TotalRecall to DearDiary
- Update Docker configuration
This commit is contained in:
lotherk
2026-03-26 20:03:52 +00:00
parent 3f9bc1f484
commit a4e7132244
28 changed files with 487 additions and 47 deletions

47
CHANGELOG.md Normal file
View File

@@ -0,0 +1,47 @@
# Changelog
All notable changes to DearDiary will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- **Task System**: AI journal generation now creates tasks that track:
- Request sent to AI provider (full prompt + config)
- Response received from AI
- Status: pending, completed, failed
- Error messages if failed
- `Task` model in database for logging
- `GET /api/v1/journal/:date/tasks` endpoint
- `GET /api/v1/tasks/:id` endpoint
- **Theme System**: Light/Dark/System theme toggle
- **Branding**: "DearDiary.io" logo in navbar
### Changed
- **Renamed project from "TotalRecall" to "DearDiary"**
- Journal generation now returns `{ journal, task }` on success
- Auth redirect now works properly (PrivateRoute component)
- Android app package: `com.totalrecall``com.deardiary`
### Fixed
- Ollama support: properly routes to configured baseUrl
- Anthropic API integration
## [0.1.0] - 2026-03-26
### Added
- User authentication (register/login)
- API key authentication for app access
- Entry CRUD (text, voice, health types)
- Day aggregation and history
- Journal generation with multiple AI providers:
- OpenAI (GPT-4)
- Anthropic (Claude)
- Ollama (local)
- LM Studio (local)
- Settings page for AI configuration
- React frontend with dark theme
- Native Android app (Kotlin/Compose)
- Docker deployment
- Prisma ORM with SQLite (extensible to PostgreSQL/MySQL)

46
Dockerfile Normal file
View File

@@ -0,0 +1,46 @@
# Multi-stage build: Backend + Frontend
FROM oven/bun:1.1-alpine AS backend-builder
WORKDIR /app/backend
COPY backend/package*.json ./
RUN bun install
COPY backend/prisma ./prisma
RUN bunx prisma generate
COPY backend/src ./src
RUN bun build src/index.ts --outdir ./dist --target bun
FROM node:20-alpine AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm install
COPY frontend ./
RUN npm run build
FROM oven/bun:1.1-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs || true
# Install nginx for serving frontend
RUN apk add --no-cache nginx
# Copy backend
COPY --from=backend-builder /app/backend/dist ./dist
COPY --from=backend-builder /app/backend/node_modules ./node_modules
COPY backend/package.json .
COPY backend/prisma ./prisma
# Copy frontend build
COPY --from=frontend-builder /app/frontend/dist ./public
# Setup nginx
COPY nginx.conf /etc/nginx/http.d/default.conf
RUN mkdir -p /data /run /app/nginx_tmp /var/lib/nginx/logs && chmod 777 /var/lib/nginx/logs /var/lib/nginx/tmp && chown -R bun:nodejs /data /app
# Start everything as root
CMD sh -c "nginx -g 'daemon off;' & bunx prisma db push --accept-data-loss & bun ./dist/index.js"

View File

@@ -4,6 +4,8 @@
AI-powered daily journal that captures life through multiple input methods and generates thoughtful, reflective journal entries. AI-powered daily journal that captures life through multiple input methods and generates thoughtful, reflective journal entries.
See [CHANGELOG.md](./CHANGELOG.md) for detailed version history.
## Features ## Features
- **Multiple Input Types**: Text notes, photos, voice memos, health data - **Multiple Input Types**: Text notes, photos, voice memos, health data

View File

@@ -4,11 +4,11 @@ plugins {
} }
android { android {
namespace = "com.totalrecall" namespace = "com.deardiary"
compileSdk = 34 compileSdk = 34
defaultConfig { defaultConfig {
applicationId = "com.totalrecall" applicationId = "com.deardiary"
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 1

View File

@@ -18,12 +18,12 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.TotalRecall" android:theme="@style/Theme.DearDiary"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.TotalRecall"> android:theme="@style/Theme.DearDiary">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />

View File

@@ -1,4 +1,4 @@
package com.totalrecall package com.deardiary
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
@@ -8,8 +8,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.totalrecall.ui.AppNavigation import com.deardiary.ui.AppNavigation
import com.totalrecall.ui.TotalRecallTheme import com.deardiary.ui.TotalRecallTheme
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

View File

@@ -1,4 +1,4 @@
package com.totalrecall.api package com.deardiary.api
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName

View File

@@ -1,4 +1,4 @@
package com.totalrecall.repository package com.deardiary.repository
import android.content.Context import android.content.Context
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
@@ -6,7 +6,7 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import com.totalrecall.api.* import com.deardiary.api.*
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "totalrecall") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "totalrecall")

View File

@@ -1,4 +1,4 @@
package com.totalrecall.ui package com.deardiary.ui
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -10,12 +10,12 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.totalrecall.ui.auth.AuthScreen import com.deardiary.ui.auth.AuthScreen
import com.totalrecall.ui.history.HistoryScreen import com.deardiary.ui.history.HistoryScreen
import com.totalrecall.ui.home.HomeScreen import com.deardiary.ui.home.HomeScreen
import com.totalrecall.ui.journal.JournalScreen import com.deardiary.ui.journal.JournalScreen
import com.totalrecall.ui.settings.SettingsScreen import com.deardiary.ui.settings.SettingsScreen
import com.totalrecall.viewmodel.MainViewModel import com.deardiary.viewmodel.MainViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
sealed class Screen(val route: String) { sealed class Screen(val route: String) {

View File

@@ -1,4 +1,4 @@
package com.totalrecall.ui package com.deardiary.ui
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.* import androidx.compose.material3.*

View File

@@ -1,4 +1,4 @@
package com.totalrecall.ui.auth package com.deardiary.ui.auth
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
@@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.totalrecall.viewmodel.UiState import com.deardiary.viewmodel.UiState
@Composable @Composable
fun AuthScreen( fun AuthScreen(

View File

@@ -1,4 +1,4 @@
package com.totalrecall.ui.history package com.deardiary.ui.history
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -12,7 +12,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.totalrecall.api.DayInfo import com.deardiary.api.DayInfo
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*

View File

@@ -1,4 +1,4 @@
package com.totalrecall.ui.home package com.deardiary.ui.home
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@@ -11,8 +11,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.totalrecall.api.DayResponse import com.deardiary.api.DayResponse
import com.totalrecall.api.Entry import com.deardiary.api.Entry
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*

View File

@@ -1,4 +1,4 @@
package com.totalrecall.ui.journal package com.deardiary.ui.journal
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@@ -12,7 +12,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.totalrecall.api.Journal import com.deardiary.api.Journal
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*

View File

@@ -1,4 +1,4 @@
package com.totalrecall.ui.settings package com.deardiary.ui.settings
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@@ -14,7 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.totalrecall.api.Settings import com.deardiary.api.Settings
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable

View File

@@ -1,11 +1,11 @@
package com.totalrecall.viewmodel package com.deardiary.viewmodel
import android.app.Application import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.totalrecall.BuildConfig import com.deardiary.BuildConfig
import com.totalrecall.api.* import com.deardiary.api.*
import com.totalrecall.repository.Repository import com.deardiary.repository.Repository
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">TotalRecall</string> <string name="app_name">DearDiary</string>
</resources> </resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.TotalRecall" parent="android:Theme.Material.Light.NoActionBar"> <style name="Theme.DearDiary" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">@color/black</item> <item name="android:statusBarColor">@color/black</item>
</style> </style>
</resources> </resources>

24
docker-compose.yml Normal file
View File

@@ -0,0 +1,24 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
- "5173:80"
environment:
- DATABASE_URL=file:/data/deardiary.db
- MEDIA_DIR=/data/media
- JWT_SECRET=${JWT_SECRET:-change-me-in-production}
- PORT=3000
- CORS_ORIGIN=${CORS_ORIGIN:-*}
volumes:
- ./data:/data
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -58,10 +58,17 @@ function App() {
<div className="min-h-screen bg-slate-950 text-slate-100"> <div className="min-h-screen bg-slate-950 text-slate-100">
{isAuthenticated && ( {isAuthenticated && (
<nav className="bg-slate-900 border-b border-slate-800 sticky top-0 z-50"> <nav className="bg-slate-900 border-b border-slate-800 sticky top-0 z-50">
<div className="max-w-4xl mx-auto px-4 py-3 flex gap-6"> <div className="max-w-4xl mx-auto px-4 py-3 flex items-center justify-between">
<a href="/" className="text-slate-300 hover:text-white transition">Today</a> <a href="/" className="flex items-center gap-2">
<a href="/history" className="text-slate-300 hover:text-white transition">History</a> <span className="text-lg font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
<a href="/settings" className="text-slate-300 hover:text-white transition">Settings</a> DearDiary.io
</span>
</a>
<div className="flex gap-6">
<a href="/" className="text-slate-300 hover:text-white transition">Today</a>
<a href="/history" className="text-slate-300 hover:text-white transition">History</a>
<a href="/settings" className="text-slate-300 hover:text-white transition">Settings</a>
</div>
</div> </div>
</nav> </nav>
)} )}

View File

@@ -2,6 +2,74 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
body { :root {
@apply bg-slate-950 text-slate-100; --bg-primary: #020917;
--bg-secondary: #0f172a;
--bg-tertiary: #1e293b;
--text-primary: #e2e8f0;
--text-secondary: #94a3b8;
--border-color: #334155;
}
.light {
--bg-primary: #ffffff;
--bg-secondary: #f8fafc;
--bg-tertiary: #f1f5f9;
--text-primary: #1e293b;
--text-secondary: #64748b;
--border-color: #e2e8f0;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color 0.2s, color 0.2s;
}
.bg-slate-950 { background-color: var(--bg-primary); }
.bg-slate-900 { background-color: var(--bg-secondary); }
.bg-slate-800 { background-color: var(--bg-tertiary); }
.bg-slate-700 { background-color: #475569; }
.text-slate-100 { color: var(--text-primary); }
.text-slate-200 { color: var(--text-primary); }
.text-slate-300 { color: var(--text-primary); }
.text-slate-400 { color: var(--text-secondary); }
.text-slate-500 { color: var(--text-secondary); }
.border-slate-800 { border-color: var(--border-color); }
.border-slate-700 { border-color: var(--border-color); }
.light .bg-slate-900 { background-color: #f1f5f9; }
.light .bg-slate-800 { background-color: #e2e8f0; }
.light .bg-slate-700 { background-color: #cbd5e1; }
.light .text-slate-300 { color: #475569; }
.light .text-slate-400 { color: #64748b; }
.light .border-slate-800 { border-color: #e2e8f0; }
.bg-white { background-color: var(--bg-primary); }
.bg-black { background-color: var(--bg-secondary); }
.ring-offset-slate-950 { --tw-ring-offset-color: var(--bg-primary); }
.dark .ring-offset-slate-950 { --tw-ring-offset-color: #020917; }
.light .ring-offset-slate-950 { --tw-ring-offset-color: #ffffff; }
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
} }

View File

@@ -0,0 +1,83 @@
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
type Theme = 'light' | 'dark' | 'system';
type ResolvedTheme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
resolvedTheme: ResolvedTheme;
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextType>({
theme: 'system',
resolvedTheme: 'dark',
setTheme: () => {},
});
export function useTheme() {
return useContext(ThemeContext);
}
function getSystemTheme(): ResolvedTheme {
if (typeof window !== 'undefined' && window.matchMedia) {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return 'dark';
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => {
if (typeof window !== 'undefined') {
return (localStorage.getItem('theme') as Theme) || 'system';
}
return 'system';
});
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() => {
if (theme === 'system') {
return getSystemTheme();
}
return theme;
});
useEffect(() => {
const root = document.documentElement;
if (theme === 'system') {
const systemTheme = getSystemTheme();
setResolvedTheme(systemTheme);
root.classList.remove('light', 'dark');
root.classList.add(systemTheme);
} else {
setResolvedTheme(theme);
root.classList.remove('light', 'dark');
root.classList.add(theme);
}
}, [theme]);
useEffect(() => {
if (theme === 'system') {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e: MediaQueryListEvent) => {
const newTheme = e.matches ? 'dark' : 'light';
setResolvedTheme(newTheme);
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(newTheme);
};
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}
}, [theme]);
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
localStorage.setItem('theme', newTheme);
};
return (
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}

View File

@@ -126,13 +126,21 @@ class ApiClient {
} }
async generateJournal(date: string) { async generateJournal(date: string) {
return this.request<Journal>('POST', `/journal/generate/${date}`); return this.request<{ journal: Journal; task: Task }>('POST', `/journal/generate/${date}`);
} }
async getJournal(date: string) { async getJournal(date: string) {
return this.request<Journal>('GET', `/journal/${date}`); return this.request<Journal>('GET', `/journal/${date}`);
} }
async getJournalTasks(date: string) {
return this.request<Task[]>('GET', `/journal/${date}/tasks`);
}
async getTask(taskId: string) {
return this.request<Task>('GET', `/tasks/${taskId}`);
}
async getSettings() { async getSettings() {
return this.request<Settings>('GET', '/settings'); return this.request<Settings>('GET', '/settings');
} }
@@ -160,6 +168,21 @@ export interface Journal {
generatedAt: string; generatedAt: string;
} }
export interface Task {
id: string;
journalId: string;
type: string;
status: 'pending' | 'completed' | 'failed';
provider: string;
model?: string;
prompt?: string;
request?: string;
response?: string;
error?: string;
createdAt: string;
completedAt?: string;
}
export interface Settings { export interface Settings {
aiProvider: 'openai' | 'anthropic' | 'ollama' | 'lmstudio'; aiProvider: 'openai' | 'anthropic' | 'ollama' | 'lmstudio';
aiApiKey?: string; aiApiKey?: string;

View File

@@ -1,10 +1,17 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import App from './App'; import App from './App';
import { ThemeProvider } from './lib/ThemeContext';
import './index.css'; import './index.css';
const savedTheme = localStorage.getItem('theme') || 'system';
const isDark = savedTheme === 'dark' || (savedTheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.add(isDark ? 'dark' : 'light');
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<App /> <ThemeProvider>
<App />
</ThemeProvider>
</React.StrictMode>, </React.StrictMode>,
); );

View File

@@ -1,15 +1,19 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { api, Journal } from '../lib/api'; import { api, Journal, Task } from '../lib/api';
export default function JournalPage() { export default function JournalPage() {
const { date } = useParams<{ date: string }>(); const { date } = useParams<{ date: string }>();
const [journal, setJournal] = useState<Journal | null>(null); const [journal, setJournal] = useState<Journal | null>(null);
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [generating, setGenerating] = useState(false); const [generating, setGenerating] = useState(false);
useEffect(() => { useEffect(() => {
if (date) loadJournal(); if (date) {
loadJournal();
loadTasks();
}
}, [date]); }, [date]);
const loadJournal = async () => { const loadJournal = async () => {
@@ -22,12 +26,21 @@ export default function JournalPage() {
setLoading(false); setLoading(false);
}; };
const loadTasks = async () => {
if (!date) return;
const res = await api.getJournalTasks(date);
if (res.data) {
setTasks(res.data);
}
};
const handleGenerate = async () => { const handleGenerate = async () => {
if (!date) return; if (!date) return;
setGenerating(true); setGenerating(true);
const res = await api.generateJournal(date); const res = await api.generateJournal(date);
if (res.data) { if (res.data) {
setJournal(res.data); setJournal(res.data.journal);
setTasks(prev => [res.data!.task, ...prev]);
} }
setGenerating(false); setGenerating(false);
}; };
@@ -81,6 +94,63 @@ export default function JournalPage() {
</div> </div>
</div> </div>
)} )}
{tasks.length > 0 && (
<div className="mt-8">
<h2 className="text-lg font-medium mb-4">Generation History</h2>
<div className="space-y-3">
{tasks.map(task => (
<details key={task.id} className="bg-slate-900 rounded-lg border border-slate-800">
<summary className="p-4 cursor-pointer flex items-center justify-between">
<div className="flex items-center gap-3">
<span className={`w-2 h-2 rounded-full ${
task.status === 'completed' ? 'bg-green-500' :
task.status === 'failed' ? 'bg-red-500' : 'bg-yellow-500'
}`} />
<span className="font-medium">
{task.provider.charAt(0).toUpperCase() + task.provider.slice(1)}
{task.model && ` - ${task.model}`}
</span>
<span className="text-sm text-slate-400">
{new Date(task.createdAt).toLocaleString()}
</span>
</div>
<span className={`text-xs px-2 py-1 rounded ${
task.status === 'completed' ? 'bg-green-500/20 text-green-400' :
task.status === 'failed' ? 'bg-red-500/20 text-red-400' : 'bg-yellow-500/20 text-yellow-400'
}`}>
{task.status}
</span>
</summary>
<div className="px-4 pb-4 space-y-3">
{task.error && (
<div className="bg-red-500/10 border border-red-500/20 rounded p-3">
<p className="text-sm text-red-400 font-medium mb-1">Error:</p>
<pre className="text-sm text-red-300 whitespace-pre-wrap">{task.error}</pre>
</div>
)}
{task.request && (
<div>
<p className="text-sm text-slate-400 font-medium mb-1">Request:</p>
<pre className="bg-slate-950 rounded p-3 text-xs text-slate-300 overflow-x-auto max-h-64">
{JSON.stringify(JSON.parse(task.request), null, 2)}
</pre>
</div>
)}
{task.response && (
<div>
<p className="text-sm text-slate-400 font-medium mb-1">Response:</p>
<pre className="bg-slate-950 rounded p-3 text-xs text-slate-300 overflow-x-auto max-h-64">
{JSON.stringify(JSON.parse(task.response), null, 2)}
</pre>
</div>
)}
</div>
</details>
))}
</div>
</div>
)}
</div> </div>
); );
} }

View File

@@ -1,11 +1,13 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { api, Settings } from '../lib/api'; import { api, Settings } from '../lib/api';
import { useTheme } from '../lib/ThemeContext';
export default function SettingsPage() { export default function SettingsPage() {
const [settings, setSettings] = useState<Partial<Settings>>({}); const [settings, setSettings] = useState<Partial<Settings>>({});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false); const [saved, setSaved] = useState(false);
const { theme, setTheme } = useTheme();
useEffect(() => { useEffect(() => {
loadSettings(); loadSettings();
@@ -47,6 +49,46 @@ export default function SettingsPage() {
<h1 className="text-2xl font-bold mb-6">Settings</h1> <h1 className="text-2xl font-bold mb-6">Settings</h1>
<div className="space-y-6"> <div className="space-y-6">
<section className="bg-slate-900 rounded-xl p-6 border border-slate-800">
<h2 className="text-lg font-medium mb-4">Appearance</h2>
<div className="space-y-3">
<label className="block text-sm text-slate-400 mb-2">Theme</label>
<div className="flex gap-2">
<button
onClick={() => setTheme('light')}
className={`flex-1 px-4 py-2 rounded-lg border transition ${
theme === 'light'
? 'bg-blue-600 border-blue-600 text-white'
: 'bg-slate-800 border-slate-700 hover:border-slate-600'
}`}
>
Light
</button>
<button
onClick={() => setTheme('dark')}
className={`flex-1 px-4 py-2 rounded-lg border transition ${
theme === 'dark'
? 'bg-blue-600 border-blue-600 text-white'
: 'bg-slate-800 border-slate-700 hover:border-slate-600'
}`}
>
Dark
</button>
<button
onClick={() => setTheme('system')}
className={`flex-1 px-4 py-2 rounded-lg border transition ${
theme === 'system'
? 'bg-blue-600 border-blue-600 text-white'
: 'bg-slate-800 border-slate-700 hover:border-slate-600'
}`}
>
System
</button>
</div>
</div>
</section>
<section className="bg-slate-900 rounded-xl p-6 border border-slate-800"> <section className="bg-slate-900 rounded-xl p-6 border border-slate-800">
<h2 className="text-lg font-medium mb-4">AI Provider</h2> <h2 className="text-lg font-medium mb-4">AI Provider</h2>

View File

@@ -1 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/EntryInput.tsx","./src/components/EntryList.tsx","./src/lib/api.ts","./src/pages/Auth.tsx","./src/pages/Day.tsx","./src/pages/History.tsx","./src/pages/Home.tsx","./src/pages/Journal.tsx","./src/pages/Settings.tsx"],"version":"5.9.3"} {"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/EntryInput.tsx","./src/components/EntryList.tsx","./src/lib/ThemeContext.tsx","./src/lib/api.ts","./src/pages/Auth.tsx","./src/pages/Day.tsx","./src/pages/History.tsx","./src/pages/Home.tsx","./src/pages/Journal.tsx","./src/pages/Settings.tsx"],"version":"5.9.3"}

21
nginx.conf Normal file
View File

@@ -0,0 +1,21 @@
server {
listen 80;
root /app/public;
index index.html;
client_body_temp_path /app/nginx_tmp/client_body;
proxy_temp_path /app/nginx_tmp/proxy;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}