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:
47
CHANGELOG.md
Normal file
47
CHANGELOG.md
Normal 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
46
Dockerfile
Normal 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"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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?) {
|
||||||
@@ -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
|
||||||
@@ -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")
|
||||||
|
|
||||||
@@ -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) {
|
||||||
@@ -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.*
|
||||||
@@ -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(
|
||||||
@@ -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.*
|
||||||
|
|
||||||
@@ -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.*
|
||||||
|
|
||||||
@@ -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.*
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
24
docker-compose.yml
Normal 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
|
||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
83
frontend/src/lib/ThemeContext.tsx
Normal file
83
frontend/src/lib/ThemeContext.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
21
nginx.conf
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user