feat: v0.0.1 - Groq provider, timezone, journal context, test connection, task logging

Added:
- Groq AI provider (free, fast with llama-3.3-70b-versatile)
- Timezone setting (22 timezones)
- Journal context: include previous journals (3/7/14/30 days)
- Test connection button for AI providers
- Per-provider settings (API key, model, base URL remembered)
- Detailed task logging (full prompts and responses)
- Tasks page with expandable details
- Progress modal with steps and AI output details

Fixed:
- Groq API endpoint (https://api.groq.com/openai/v1/chat/completions)
- Ollama baseUrl leaking to other providers
- Database schema references
- Proper Prisma migrations (data-safe)

Changed:
- Default AI: OpenAI → Groq
- Project renamed: TotalRecall → DearDiary
- Strict anti-hallucination prompt
- Docker uses prisma migrate deploy (non-destructive)
This commit is contained in:
lotherk
2026-03-26 21:56:29 +00:00
parent 37871271cc
commit 5c217853de
27 changed files with 1026 additions and 260 deletions

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#1e293b" />
<link rel="manifest" href="/manifest.json" />
<title>TotalRecall - AI Journal</title>
<title>DearDiary - AI Journal</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,11 +1,11 @@
{
"name": "totalrecall-frontend",
"name": "deardiary-frontend",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "totalrecall-frontend",
"name": "deardiary-frontend",
"version": "1.0.0",
"dependencies": {
"react": "^18.3.1",

View File

@@ -1,7 +1,7 @@
{
"name": "totalrecall-frontend",
"name": "deardiary-frontend",
"private": true,
"version": "1.0.0",
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,6 +1,6 @@
{
"name": "TotalRecall",
"short_name": "TotalRecall",
"name": "DearDiary",
"short_name": "DearDiary",
"description": "AI-powered daily journal that captures life through multiple input methods",
"start_url": "/",
"display": "standalone",

View File

@@ -6,6 +6,7 @@ import Home from './pages/Home';
import History from './pages/History';
import Day from './pages/Day';
import Journal from './pages/Journal';
import Tasks from './pages/Tasks';
import Settings from './pages/Settings';
import { useTheme } from './lib/ThemeContext';
@@ -88,6 +89,9 @@ function App() {
<Route path="/journal/:date" element={
<PrivateRoute><Journal /></PrivateRoute>
} />
<Route path="/tasks/:date" element={
<PrivateRoute><Tasks /></PrivateRoute>
} />
<Route path="/settings" element={
<PrivateRoute><Settings /></PrivateRoute>
} />

View File

@@ -184,12 +184,19 @@ export interface Task {
}
export interface Settings {
aiProvider: 'openai' | 'anthropic' | 'ollama' | 'lmstudio';
aiProvider: 'openai' | 'anthropic' | 'ollama' | 'lmstudio' | 'groq';
aiApiKey?: string;
aiModel: string;
aiBaseUrl?: string;
journalPrompt: string;
language: string;
timezone: string;
journalContextDays: number;
providerSettings?: Record<string, {
apiKey?: string;
model?: string;
baseUrl?: string;
}>;
}
export const api = new ApiClient();

View File

@@ -44,7 +44,7 @@ export default function Auth({ onAuth }: { onAuth: () => void }) {
return (
<div className="min-h-screen flex items-center justify-center p-4">
<div className="w-full max-w-md">
<h1 className="text-3xl font-bold text-center mb-8 text-slate-100">TotalRecall</h1>
<h1 className="text-3xl font-bold text-center mb-8 text-slate-100">DearDiary</h1>
<div className="bg-slate-900 rounded-xl p-6 border border-slate-800">
<div className="flex gap-4 mb-6">

View File

@@ -1,6 +1,124 @@
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { api, Journal, Task } from '../lib/api';
import { useTheme } from '../lib/ThemeContext';
const detailsStyles = `
details[open] .details-arrow {
transform: rotate(90deg);
}
details summary::-webkit-details-marker {
display: none;
}
`;
function GeneratingModal({
isOpen,
provider,
model,
step,
response
}: {
isOpen: boolean;
provider: string;
model?: string;
step: number;
response?: string;
}) {
const { resolvedTheme } = useTheme();
if (!isOpen) return null;
const steps = [
{ label: 'Sending entries to AI...', done: step > 0 },
{ label: 'Waiting for response...', done: step > 1 },
{ label: 'Processing & formatting...', done: step > 2 },
];
const formatResponse = (str: string | undefined): string | null => {
if (!str) return null;
try {
const parsed = JSON.parse(str);
if (parsed.content?.[0]?.text) {
return parsed.content[0].text;
}
if (parsed.choices?.[0]?.message?.content) {
return parsed.choices[0].message.content;
}
return JSON.stringify(parsed, null, 2);
} catch {
return str;
}
};
const output = formatResponse(response);
return (
<>
<style>{detailsStyles}</style>
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
<div className={`relative ${resolvedTheme === 'dark' ? 'bg-slate-800' : 'bg-white'} rounded-2xl p-8 shadow-2xl max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto`}>
<div className="text-center mb-6">
<div className="w-16 h-16 mx-auto mb-6 relative">
<div className="absolute inset-0 border-4 border-slate-600 rounded-full" />
<div className="absolute inset-0 border-4 border-purple-500 rounded-full border-t-transparent animate-spin" />
</div>
<h3 className="text-xl font-semibold mb-4">Generating Journal</h3>
<div className="space-y-3 text-sm text-left">
{steps.map((s, i) => (
<div key={i} className="flex items-center gap-3">
<div className={`w-5 h-5 rounded-full flex items-center justify-center text-xs font-bold ${
s.done
? 'bg-green-500 text-white'
: step === i + 1
? 'bg-yellow-500 text-white animate-pulse'
: 'bg-slate-600 text-slate-400'
}`}>
{s.done ? '✓' : i + 1}
</div>
<span className={s.done ? 'text-slate-300' : step === i + 1 ? 'text-yellow-300' : 'text-slate-500'}>
{s.label}
</span>
</div>
))}
</div>
<div className="mt-4 w-full bg-slate-700 rounded-full h-2">
<div
className="h-2 bg-purple-500 rounded-full transition-all duration-300"
style={{ width: `${(step / 3) * 100}%` }}
/>
</div>
</div>
<details className="mt-4 border border-slate-600 rounded-lg">
<summary className="px-4 py-3 cursor-pointer text-sm text-slate-400 hover:text-slate-300 flex items-center gap-2">
<span className="transform transition-transform details-arrow"></span>
Details (AI Output)
</summary>
<div className="px-4 pb-4">
{output ? (
<pre className="mt-2 p-3 bg-slate-900 rounded-lg text-xs text-slate-300 max-h-48 overflow-auto whitespace-pre-wrap">
{output}
</pre>
) : (
<p className="mt-2 text-xs text-slate-500 italic">Waiting for response...</p>
)}
</div>
</details>
<div className="mt-4 text-xs text-slate-500 text-center">
{provider.charAt(0).toUpperCase() + provider.slice(1)}
{model && `${model}`}
</div>
</div>
</div>
</>
);
}
export default function JournalPage() {
const { date } = useParams<{ date: string }>();
@@ -8,6 +126,10 @@ export default function JournalPage() {
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
const [generating, setGenerating] = useState(false);
const [currentProvider, setCurrentProvider] = useState('');
const [currentModel, setCurrentModel] = useState('');
const [generatingStep, setGeneratingStep] = useState(0);
const [currentResponse, setCurrentResponse] = useState<string | undefined>();
useEffect(() => {
if (date) {
@@ -36,13 +158,23 @@ export default function JournalPage() {
const handleGenerate = async () => {
if (!date) return;
setGenerating(true);
setGeneratingStep(1);
setCurrentProvider('ai');
setCurrentModel('');
setCurrentResponse(undefined);
const res = await api.generateJournal(date);
setGeneratingStep(3);
if (res.data) {
setCurrentResponse(res.data.task.response);
setJournal(res.data.journal);
setCurrentProvider(res.data.task.provider);
setCurrentModel(res.data.task.model || '');
setTasks(prev => [res.data!.task, ...prev]);
}
setGenerating(false);
setTimeout(() => setGenerating(false), 500);
};
const formatDate = (dateStr: string) => {
@@ -53,104 +185,100 @@ export default function JournalPage() {
if (!date) return null;
return (
<div className="max-w-4xl mx-auto p-4">
<div className="mb-6">
<a href={`/day/${date}`} className="text-slate-400 hover:text-white text-sm mb-1 inline-block"> Back to day</a>
<h1 className="text-2xl font-bold">Journal</h1>
<p className="text-slate-400">{formatDate(date)}</p>
</div>
{loading ? (
<div className="text-center py-12 text-slate-400">Loading...</div>
) : !journal ? (
<div className="text-center py-12">
<p className="text-slate-400 mb-4">No journal generated yet</p>
<button
onClick={handleGenerate}
disabled={generating}
className="px-6 py-3 bg-purple-600 hover:bg-purple-700 rounded-lg font-medium transition disabled:opacity-50"
>
{generating ? 'Generating...' : 'Generate Journal'}
</button>
<>
<GeneratingModal
isOpen={generating}
provider={currentProvider}
model={currentModel}
step={generatingStep}
response={currentResponse}
/>
<div className="max-w-4xl mx-auto p-4">
<div className="mb-6">
<a href={`/day/${date}`} className="text-slate-400 hover:text-white text-sm mb-1 inline-block"> Back to day</a>
<h1 className="text-2xl font-bold">Journal</h1>
<p className="text-slate-400">{formatDate(date)}</p>
</div>
) : (
<div>
<div className="text-sm text-slate-400 mb-4">
Generated {new Date(journal.generatedAt).toLocaleString()} {journal.entryCount} entries
</div>
<div className="prose prose-invert prose-slate max-w-none">
<div className="bg-slate-900 rounded-xl p-6 border border-slate-800 whitespace-pre-wrap leading-relaxed">
{journal.content}
</div>
</div>
<div className="mt-6 flex gap-4">
{loading ? (
<div className="text-center py-12 text-slate-400">Loading...</div>
) : !journal || journal.content === 'Generating...' ? (
<div className="text-center py-12">
<p className="text-slate-400 mb-4">No journal generated yet</p>
<button
onClick={handleGenerate}
disabled={generating}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 rounded-lg text-sm transition disabled:opacity-50"
className="px-6 py-3 bg-purple-600 hover:bg-purple-700 rounded-lg font-medium transition disabled:opacity-50"
>
Regenerate
Generate Journal
</button>
</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 className="text-sm text-slate-400 mb-4">
Generated {new Date(journal.generatedAt).toLocaleString()} {journal.entryCount} entries
</div>
<div className="prose prose-invert prose-slate max-w-none">
<div className="bg-slate-900 rounded-xl p-6 border border-slate-800 whitespace-pre-wrap leading-relaxed">
{journal.content}
</div>
</div>
<div className="mt-6 flex gap-4">
<button
onClick={handleGenerate}
disabled={generating}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 rounded-lg text-sm transition disabled:opacity-50"
>
Regenerate
</button>
<a
href={`/tasks/${date}`}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 rounded-lg text-sm transition"
>
View Tasks
</a>
</div>
</div>
</div>
)}
</div>
)}
{tasks.length > 0 && (
<div className="mt-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-medium">Generation Tasks</h2>
<a
href={`/tasks/${date}`}
className="text-sm text-purple-400 hover:text-purple-300"
>
View all
</a>
</div>
<div className="space-y-2">
{tasks.slice(0, 3).map(task => (
<details key={task.id} className="bg-slate-900 rounded-lg border border-slate-800">
<summary className="p-3 cursor-pointer flex items-center justify-between text-sm">
<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)}
</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>
</details>
))}
</div>
</div>
)}
</div>
</>
);
}

View File

@@ -2,11 +2,43 @@ import { useState, useEffect } from 'react';
import { api, Settings } from '../lib/api';
import { useTheme } from '../lib/ThemeContext';
interface ProviderSettings {
apiKey?: string;
model?: string;
baseUrl?: string;
}
interface FullSettings extends Settings {
providerSettings?: Record<string, ProviderSettings>;
}
const DEFAULT_MODELS: Record<string, string> = {
groq: 'llama-3.3-70b-versatile',
openai: 'gpt-4o',
anthropic: 'claude-3-5-sonnet-20241022',
ollama: 'llama3.2',
lmstudio: 'local-model',
};
const DEFAULT_BASE_URLS: Record<string, string> = {
ollama: 'http://localhost:11434',
lmstudio: 'http://localhost:1234/v1',
};
export default function SettingsPage() {
const [settings, setSettings] = useState<Partial<Settings>>({});
const [settings, setSettings] = useState<FullSettings>({
aiProvider: 'groq',
aiModel: 'llama-3.3-70b-versatile',
journalPrompt: '',
language: 'en',
timezone: 'UTC',
journalContextDays: 10,
});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
const { theme, setTheme } = useTheme();
useEffect(() => {
@@ -17,25 +49,133 @@ export default function SettingsPage() {
setLoading(true);
const res = await api.getSettings();
if (res.data) {
setSettings(res.data);
const data = res.data as FullSettings;
if (data.providerSettings && typeof data.providerSettings === 'string') {
try {
data.providerSettings = JSON.parse(data.providerSettings);
} catch {
data.providerSettings = {};
}
}
if (!data.providerSettings) {
data.providerSettings = {};
}
setSettings(data);
}
setLoading(false);
};
const getCurrentProviderSettings = (): ProviderSettings => {
const provider = settings.aiProvider || 'groq';
return settings.providerSettings?.[provider] || {
apiKey: settings.aiApiKey,
model: settings.aiModel,
baseUrl: settings.aiBaseUrl,
};
};
const updateProviderSettings = (updates: Partial<ProviderSettings>) => {
const provider = settings.aiProvider || 'groq';
const current = getCurrentProviderSettings();
const newProviderSettings = {
...settings.providerSettings,
[provider]: { ...current, ...updates },
};
setSettings({
...settings,
providerSettings: newProviderSettings,
aiProvider: provider,
});
};
const handleProviderChange = (provider: string) => {
const currentProvider = settings.aiProvider || 'groq';
const currentSettings = getCurrentProviderSettings();
const newProviderSettings = {
...settings.providerSettings,
[currentProvider]: currentSettings,
};
const newProviderSettingsData = newProviderSettings[provider] || {};
setSettings({
...settings,
aiProvider: provider as FullSettings['aiProvider'],
aiApiKey: newProviderSettingsData.apiKey,
aiModel: newProviderSettingsData.model || DEFAULT_MODELS[provider] || '',
aiBaseUrl: newProviderSettingsData.baseUrl,
providerSettings: newProviderSettings,
});
setTestResult(null);
};
const handleSave = async () => {
setSaving(true);
setSaved(false);
await api.updateSettings(settings);
const provider = settings.aiProvider || 'groq';
const currentSettings = getCurrentProviderSettings();
const newProviderSettings = {
...settings.providerSettings,
[provider]: currentSettings,
};
await api.updateSettings({
...settings,
providerSettings: newProviderSettings,
});
setSaving(false);
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
const handleTest = async () => {
setTesting(true);
setTestResult(null);
const currentSettings = getCurrentProviderSettings();
const provider = settings.aiProvider || 'groq';
try {
const response = await fetch('/api/v1/ai/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('apiKey')}`,
},
body: JSON.stringify({
provider,
apiKey: currentSettings.apiKey,
model: currentSettings.model || DEFAULT_MODELS[provider],
baseUrl: (provider === 'ollama' || provider === 'lmstudio')
? (currentSettings.baseUrl || DEFAULT_BASE_URLS[provider])
: undefined,
}),
});
const data = await response.json();
if (data.data?.valid) {
setTestResult({ success: true, message: 'Connection successful!' });
} else {
setTestResult({ success: false, message: data.error?.message || 'Connection failed' });
}
} catch (err) {
setTestResult({ success: false, message: 'Connection failed: ' + (err instanceof Error ? err.message : 'Unknown error') });
}
setTesting(false);
};
const handleLogout = () => {
api.clearApiKey();
window.location.href = '/login';
};
const currentProviderSettings = getCurrentProviderSettings();
const provider = settings.aiProvider || 'groq';
if (loading) {
return (
<div className="max-w-2xl mx-auto p-4">
@@ -96,10 +236,11 @@ export default function SettingsPage() {
<div>
<label className="block text-sm text-slate-400 mb-1">Provider</label>
<select
value={settings.aiProvider || 'openai'}
onChange={(e) => setSettings({ ...settings, aiProvider: e.target.value as Settings['aiProvider'] })}
value={provider}
onChange={(e) => handleProviderChange(e.target.value)}
className="w-full px-4 py-2 bg-slate-800 rounded-lg border border-slate-700 focus:border-blue-500 focus:outline-none"
>
<option value="groq">Groq (Free, Fast)</option>
<option value="openai">OpenAI (GPT-4)</option>
<option value="anthropic">Anthropic (Claude)</option>
<option value="ollama">Ollama (Local)</option>
@@ -107,27 +248,27 @@ export default function SettingsPage() {
</select>
</div>
{(settings.aiProvider === 'openai' || settings.aiProvider === 'anthropic') && (
{(provider === 'openai' || provider === 'anthropic' || provider === 'groq') && (
<div>
<label className="block text-sm text-slate-400 mb-1">API Key</label>
<input
type="password"
value={settings.aiApiKey || ''}
onChange={(e) => setSettings({ ...settings, aiApiKey: e.target.value })}
placeholder="sk-..."
value={currentProviderSettings.apiKey || ''}
onChange={(e) => updateProviderSettings({ apiKey: e.target.value })}
placeholder={provider === 'groq' ? 'gsk_...' : 'sk-...'}
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.aiProvider === 'ollama' || settings.aiProvider === 'lmstudio') && (
{(provider === 'ollama' || provider === 'lmstudio') && (
<div>
<label className="block text-sm text-slate-400 mb-1">Base URL</label>
<input
type="text"
value={settings.aiBaseUrl || ''}
onChange={(e) => setSettings({ ...settings, aiBaseUrl: e.target.value })}
placeholder={settings.aiProvider === 'ollama' ? 'http://localhost:11434' : 'http://localhost:1234/v1'}
value={currentProviderSettings.baseUrl || ''}
onChange={(e) => updateProviderSettings({ baseUrl: e.target.value })}
placeholder={DEFAULT_BASE_URLS[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>
@@ -137,12 +278,28 @@ export default function SettingsPage() {
<label className="block text-sm text-slate-400 mb-1">Model</label>
<input
type="text"
value={settings.aiModel || ''}
onChange={(e) => setSettings({ ...settings, aiModel: e.target.value })}
placeholder={settings.aiProvider === 'openai' ? 'gpt-4' : settings.aiProvider === 'anthropic' ? 'claude-3-sonnet-20240229' : 'llama3.2'}
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>
<div className="flex gap-3 items-center">
<button
onClick={handleTest}
disabled={testing}
className="px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg text-sm transition disabled:opacity-50"
>
{testing ? 'Testing...' : 'Test Connection'}
</button>
{testResult && (
<span className={`text-sm ${testResult.success ? 'text-green-400' : 'text-red-400'}`}>
{testResult.message}
</span>
)}
</div>
</div>
</section>
@@ -176,6 +333,53 @@ export default function SettingsPage() {
<option value="zh"></option>
</select>
</div>
<div>
<label className="block text-sm text-slate-400 mb-1">Timezone</label>
<select
value={settings.timezone || 'UTC'}
onChange={(e) => setSettings({ ...settings, timezone: e.target.value })}
className="w-full px-4 py-2 bg-slate-800 rounded-lg border border-slate-700 focus:border-blue-500 focus:outline-none"
>
<option value="Pacific/Honolulu">Hawaii (HST)</option>
<option value="America/Anchorage">Alaska (AKST)</option>
<option value="America/Los_Angeles">Pacific (PST/PDT)</option>
<option value="America/Denver">Mountain (MST/MDT)</option>
<option value="America/Chicago">Central (CST/CDT)</option>
<option value="America/New_York">Eastern (EST/EDT)</option>
<option value="America/Sao_Paulo">São Paulo (BRT)</option>
<option value="Atlantic/Azores">Azores (AZOT)</option>
<option value="UTC">UTC</option>
<option value="Europe/London">London (GMT/BST)</option>
<option value="Europe/Paris">Paris (CET/CEST)</option>
<option value="Europe/Berlin">Berlin (CET/CEST)</option>
<option value="Africa/Cairo">Cairo (EET)</option>
<option value="Europe/Moscow">Moscow (MSK)</option>
<option value="Asia/Dubai">Dubai (GST)</option>
<option value="Asia/Kolkata">India (IST)</option>
<option value="Asia/Bangkok">Bangkok (ICT)</option>
<option value="Asia/Shanghai">Beijing (CST)</option>
<option value="Asia/Tokyo">Tokyo (JST)</option>
<option value="Australia/Sydney">Sydney (AEST/AEDT)</option>
<option value="Pacific/Auckland">Auckland (NZST)</option>
</select>
</div>
<div>
<label className="block text-sm text-slate-400 mb-1">Include Previous Journals in Context</label>
<select
value={settings.journalContextDays || 10}
onChange={(e) => setSettings({ ...settings, journalContextDays: parseInt(e.target.value) })}
className="w-full px-4 py-2 bg-slate-800 rounded-lg border border-slate-700 focus:border-blue-500 focus:outline-none"
>
<option value="0">None - only today's entries</option>
<option value="3">Last 3 days</option>
<option value="7">Last 7 days</option>
<option value="14">Last 14 days</option>
<option value="30">Last 30 days</option>
</select>
<p className="text-xs text-slate-500 mt-1">AI will see recent journal summaries to maintain context</p>
</div>
</div>
</section>

View File

@@ -0,0 +1,171 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { api, Task } from '../lib/api';
export default function TasksPage() {
const { date } = useParams<{ date: string }>();
const navigate = useNavigate();
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
const [expandedTask, setExpandedTask] = useState<string | null>(null);
useEffect(() => {
if (date) {
loadTasks();
}
}, [date]);
const loadTasks = async () => {
if (!date) return;
setLoading(true);
const res = await api.getJournalTasks(date);
if (res.data) {
setTasks(res.data);
}
setLoading(false);
};
const formatDate = (dateStr: string) => {
const d = new Date(dateStr + 'T12:00:00');
return d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
};
const formatTime = (dateStr: string) => {
return new Date(dateStr).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
};
const prettyJson = (str: string | undefined) => {
if (!str) return null;
try {
return JSON.stringify(JSON.parse(str), null, 2);
} catch {
return str;
}
};
if (!date) return null;
return (
<div className="max-w-4xl mx-auto p-4">
<div className="mb-6">
<button
onClick={() => navigate(`/journal/${date}`)}
className="text-slate-400 hover:text-white text-sm mb-1"
>
Back to journal
</button>
<h1 className="text-2xl font-bold">Generation Tasks</h1>
<p className="text-slate-400">{formatDate(date)}</p>
</div>
{loading ? (
<div className="text-center py-12 text-slate-400">Loading...</div>
) : tasks.length === 0 ? (
<div className="text-center py-12">
<p className="text-slate-400 mb-4">No tasks yet</p>
<button
onClick={() => navigate(`/journal/${date}`)}
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg text-sm"
>
Generate Journal
</button>
</div>
) : (
<div className="space-y-4">
{tasks.map(task => (
<div
key={task.id}
className="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden"
>
<button
onClick={() => setExpandedTask(expandedTask === task.id ? null : task.id)}
className="w-full p-4 flex items-center justify-between text-left"
>
<div className="flex items-center gap-4">
<span className={`w-3 h-3 rounded-full ${
task.status === 'completed' ? 'bg-green-500' :
task.status === 'failed' ? 'bg-red-500' : 'bg-yellow-500 animate-pulse'
}`} />
<div>
<p className="font-medium">
{task.provider.charAt(0).toUpperCase() + task.provider.slice(1)}
{task.model && `${task.model}`}
</p>
<p className="text-sm text-slate-400">
{formatTime(task.createdAt)}
{task.completedAt && `${formatTime(task.completedAt)}`}
</p>
</div>
</div>
<div className="flex items-center gap-3">
<span className={`text-xs px-3 py-1 rounded-full ${
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>
<span className="text-slate-400">
{expandedTask === task.id ? '▲' : '▼'}
</span>
</div>
</button>
{expandedTask === task.id && (
<div className="border-t border-slate-800 p-4 space-y-4">
{task.error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-4">
<p className="text-sm font-medium text-red-400 mb-2">Error</p>
<pre className="text-sm text-red-300 whitespace-pre-wrap">{task.error}</pre>
</div>
)}
{task.prompt && (
<div>
<p className="text-sm font-medium text-slate-400 mb-2">System Prompt</p>
<div className="bg-slate-950 rounded-lg p-3 max-h-40 overflow-y-auto">
<pre className="text-xs text-slate-300 whitespace-pre-wrap">{task.prompt}</pre>
</div>
</div>
)}
{task.request && (
<div>
<p className="text-sm font-medium text-slate-400 mb-2">Request</p>
<div className="bg-slate-950 rounded-lg p-3 max-h-64 overflow-y-auto">
<pre className="text-xs text-slate-300 whitespace-pre-wrap font-mono">
{prettyJson(task.request)}
</pre>
</div>
</div>
)}
{task.response && (
<div>
<p className="text-sm font-medium text-slate-400 mb-2">Response</p>
<details className="bg-slate-950 rounded-lg">
<summary className="p-3 cursor-pointer text-sm text-slate-400 hover:text-slate-300">
View full response
</summary>
<pre className="px-3 pb-3 text-xs text-slate-300 whitespace-pre-wrap font-mono overflow-x-auto">
{prettyJson(task.response)}
</pre>
</details>
</div>
)}
{!task.error && !task.request && !task.response && (
<p className="text-sm text-slate-500 italic">No details available</p>
)}
</div>
)}
</div>
))}
</div>
)}
</div>
);
}

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/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"}
{"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","./src/pages/Tasks.tsx"],"version":"5.9.3"}