chronicle-ai 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -0
- package/package.json +86 -0
- package/packages/README.md +139 -0
- package/packages/cli/README.md +92 -0
- package/packages/cli/bin/chronicle.js +2 -0
- package/packages/cli/dist/commands/add.d.ts +2 -0
- package/packages/cli/dist/commands/add.d.ts.map +1 -0
- package/packages/cli/dist/commands/add.js +82 -0
- package/packages/cli/dist/commands/add.js.map +1 -0
- package/packages/cli/dist/commands/diff.d.ts +11 -0
- package/packages/cli/dist/commands/diff.d.ts.map +1 -0
- package/packages/cli/dist/commands/diff.js +164 -0
- package/packages/cli/dist/commands/diff.js.map +1 -0
- package/packages/cli/dist/commands/init.d.ts +2 -0
- package/packages/cli/dist/commands/init.d.ts.map +1 -0
- package/packages/cli/dist/commands/init.js +54 -0
- package/packages/cli/dist/commands/init.js.map +1 -0
- package/packages/cli/dist/commands/list.d.ts +2 -0
- package/packages/cli/dist/commands/list.d.ts.map +1 -0
- package/packages/cli/dist/commands/list.js +62 -0
- package/packages/cli/dist/commands/list.js.map +1 -0
- package/packages/cli/dist/commands/log.d.ts +4 -0
- package/packages/cli/dist/commands/log.d.ts.map +1 -0
- package/packages/cli/dist/commands/log.js +223 -0
- package/packages/cli/dist/commands/log.js.map +1 -0
- package/packages/cli/dist/commands/pause.d.ts +3 -0
- package/packages/cli/dist/commands/pause.d.ts.map +1 -0
- package/packages/cli/dist/commands/pause.js +49 -0
- package/packages/cli/dist/commands/pause.js.map +1 -0
- package/packages/cli/dist/commands/queue.d.ts +2 -0
- package/packages/cli/dist/commands/queue.d.ts.map +1 -0
- package/packages/cli/dist/commands/queue.js +96 -0
- package/packages/cli/dist/commands/queue.js.map +1 -0
- package/packages/cli/dist/commands/remove.d.ts +2 -0
- package/packages/cli/dist/commands/remove.d.ts.map +1 -0
- package/packages/cli/dist/commands/remove.js +80 -0
- package/packages/cli/dist/commands/remove.js.map +1 -0
- package/packages/cli/dist/commands/reset.d.ts +5 -0
- package/packages/cli/dist/commands/reset.d.ts.map +1 -0
- package/packages/cli/dist/commands/reset.js +90 -0
- package/packages/cli/dist/commands/reset.js.map +1 -0
- package/packages/cli/dist/commands/restart.d.ts +2 -0
- package/packages/cli/dist/commands/restart.d.ts.map +1 -0
- package/packages/cli/dist/commands/restart.js +72 -0
- package/packages/cli/dist/commands/restart.js.map +1 -0
- package/packages/cli/dist/commands/start.d.ts +2 -0
- package/packages/cli/dist/commands/start.d.ts.map +1 -0
- package/packages/cli/dist/commands/start.js +127 -0
- package/packages/cli/dist/commands/start.js.map +1 -0
- package/packages/cli/dist/commands/status.d.ts +2 -0
- package/packages/cli/dist/commands/status.d.ts.map +1 -0
- package/packages/cli/dist/commands/status.js +181 -0
- package/packages/cli/dist/commands/status.js.map +1 -0
- package/packages/cli/dist/commands/stop.d.ts +2 -0
- package/packages/cli/dist/commands/stop.d.ts.map +1 -0
- package/packages/cli/dist/commands/stop.js +64 -0
- package/packages/cli/dist/commands/stop.js.map +1 -0
- package/packages/cli/dist/index.d.ts +2 -0
- package/packages/cli/dist/index.d.ts.map +1 -0
- package/packages/cli/dist/index.js +85 -0
- package/packages/cli/dist/index.js.map +1 -0
- package/packages/cli/dist/utils/paths.d.ts +26 -0
- package/packages/cli/dist/utils/paths.d.ts.map +1 -0
- package/packages/cli/dist/utils/paths.js +183 -0
- package/packages/cli/dist/utils/paths.js.map +1 -0
- package/packages/cli/package.json +25 -0
- package/packages/daemon/README.md +83 -0
- package/packages/daemon/dist/ai_test.d.ts +2 -0
- package/packages/daemon/dist/ai_test.d.ts.map +1 -0
- package/packages/daemon/dist/ai_test.js +4 -0
- package/packages/daemon/dist/ai_test.js.map +1 -0
- package/packages/daemon/dist/index.d.ts +2 -0
- package/packages/daemon/dist/index.d.ts.map +1 -0
- package/packages/daemon/dist/index.js +147 -0
- package/packages/daemon/dist/index.js.map +1 -0
- package/packages/daemon/dist/jobs/AIProcessor.d.ts +6 -0
- package/packages/daemon/dist/jobs/AIProcessor.d.ts.map +1 -0
- package/packages/daemon/dist/jobs/AIProcessor.js +58 -0
- package/packages/daemon/dist/jobs/AIProcessor.js.map +1 -0
- package/packages/daemon/dist/jobs/DocProcessor.d.ts +8 -0
- package/packages/daemon/dist/jobs/DocProcessor.d.ts.map +1 -0
- package/packages/daemon/dist/jobs/DocProcessor.js +336 -0
- package/packages/daemon/dist/jobs/DocProcessor.js.map +1 -0
- package/packages/daemon/dist/jobs/FileProcessor.d.ts +7 -0
- package/packages/daemon/dist/jobs/FileProcessor.d.ts.map +1 -0
- package/packages/daemon/dist/jobs/FileProcessor.js +29 -0
- package/packages/daemon/dist/jobs/FileProcessor.js.map +1 -0
- package/packages/daemon/dist/jobs/SystemProcessor.d.ts +13 -0
- package/packages/daemon/dist/jobs/SystemProcessor.d.ts.map +1 -0
- package/packages/daemon/dist/jobs/SystemProcessor.js +38 -0
- package/packages/daemon/dist/jobs/SystemProcessor.js.map +1 -0
- package/packages/daemon/dist/jobs/UpdateProcessor.d.ts +21 -0
- package/packages/daemon/dist/jobs/UpdateProcessor.d.ts.map +1 -0
- package/packages/daemon/dist/jobs/UpdateProcessor.js +222 -0
- package/packages/daemon/dist/jobs/UpdateProcessor.js.map +1 -0
- package/packages/daemon/dist/services/AIService.d.ts +90 -0
- package/packages/daemon/dist/services/AIService.d.ts.map +1 -0
- package/packages/daemon/dist/services/AIService.js +451 -0
- package/packages/daemon/dist/services/AIService.js.map +1 -0
- package/packages/daemon/dist/services/ConfigService.d.ts +30 -0
- package/packages/daemon/dist/services/ConfigService.d.ts.map +1 -0
- package/packages/daemon/dist/services/ConfigService.js +69 -0
- package/packages/daemon/dist/services/ConfigService.js.map +1 -0
- package/packages/daemon/dist/services/DatabaseService.d.ts +204 -0
- package/packages/daemon/dist/services/DatabaseService.d.ts.map +1 -0
- package/packages/daemon/dist/services/DatabaseService.js +692 -0
- package/packages/daemon/dist/services/DatabaseService.js.map +1 -0
- package/packages/daemon/dist/services/GitService.d.ts +12 -0
- package/packages/daemon/dist/services/GitService.d.ts.map +1 -0
- package/packages/daemon/dist/services/GitService.js +68 -0
- package/packages/daemon/dist/services/GitService.js.map +1 -0
- package/packages/daemon/dist/services/QueueService.d.ts +16 -0
- package/packages/daemon/dist/services/QueueService.d.ts.map +1 -0
- package/packages/daemon/dist/services/QueueService.js +87 -0
- package/packages/daemon/dist/services/QueueService.js.map +1 -0
- package/packages/daemon/dist/services/TriggerService.d.ts +37 -0
- package/packages/daemon/dist/services/TriggerService.d.ts.map +1 -0
- package/packages/daemon/dist/services/TriggerService.js +150 -0
- package/packages/daemon/dist/services/TriggerService.js.map +1 -0
- package/packages/daemon/dist/services/WatcherService.d.ts +12 -0
- package/packages/daemon/dist/services/WatcherService.d.ts.map +1 -0
- package/packages/daemon/dist/services/WatcherService.js +77 -0
- package/packages/daemon/dist/services/WatcherService.js.map +1 -0
- package/packages/daemon/dist/services/index.d.ts +2 -0
- package/packages/daemon/dist/services/index.d.ts.map +1 -0
- package/packages/daemon/dist/services/index.js +18 -0
- package/packages/daemon/dist/services/index.js.map +1 -0
- package/packages/daemon/dist/test-ignore.js +0 -0
- package/packages/daemon/package.json +28 -0
- package/packages/ui/app/actions.ts +73 -0
- package/packages/ui/app/api/ai-activity/route.ts +14 -0
- package/packages/ui/app/globals.css +98 -0
- package/packages/ui/app/layout.tsx +29 -0
- package/packages/ui/app/page.tsx +109 -0
- package/packages/ui/components/AutoRefresh.tsx +18 -0
- package/packages/ui/components/DocumentationViewer.tsx +276 -0
- package/packages/ui/components/FileList.tsx +69 -0
- package/packages/ui/components/HeaderWithThinking.tsx +102 -0
- package/packages/ui/components/JobQueue.tsx +194 -0
- package/packages/ui/components/JobQueueWrapper.tsx +31 -0
- package/packages/ui/components/MermaidInit.tsx +24 -0
- package/packages/ui/components/ProjectContentArea.tsx +186 -0
- package/packages/ui/components/ProjectList.tsx +136 -0
- package/packages/ui/components/ThinkingDrawer.tsx +377 -0
- package/packages/ui/components/ThinkingPanel.tsx +63 -0
- package/packages/ui/components/TriggerSettings.tsx +185 -0
- package/packages/ui/components/VersionSelector.tsx +132 -0
- package/packages/ui/lib/db.ts +521 -0
- package/packages/ui/next-env.d.ts +5 -0
- package/packages/ui/next.config.js +4 -0
- package/packages/ui/package.json +32 -0
- package/packages/ui/postcss.config.js +6 -0
- package/packages/ui/public/logo.png +0 -0
- package/packages/ui/tailwind.config.ts +32 -0
- package/packages/ui/tsconfig.json +40 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { Brain } from 'lucide-react';
|
|
5
|
+
import { ThinkingDrawer } from './ThinkingDrawer';
|
|
6
|
+
import { clsx } from 'clsx';
|
|
7
|
+
|
|
8
|
+
interface AIActivity {
|
|
9
|
+
id: number;
|
|
10
|
+
project_id: number | null;
|
|
11
|
+
version_id: number | null;
|
|
12
|
+
activity_type: 'analyze' | 'generate' | 'plan' | 'embed' | 'other';
|
|
13
|
+
prompt_summary: string | null;
|
|
14
|
+
response_preview: string | null;
|
|
15
|
+
thinking_summary: string | null;
|
|
16
|
+
tokens_used: number | null;
|
|
17
|
+
duration_ms: number | null;
|
|
18
|
+
status: 'running' | 'completed' | 'error';
|
|
19
|
+
created_at: number;
|
|
20
|
+
project_path?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
initialActivities: AIActivity[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function HeaderWithThinking({ initialActivities }: Props) {
|
|
28
|
+
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
|
29
|
+
const [activities, setActivities] = useState<AIActivity[]>(initialActivities);
|
|
30
|
+
|
|
31
|
+
// Poll for new activities every 3 seconds
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const interval = setInterval(async () => {
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch('/api/ai-activity');
|
|
36
|
+
if (res.ok) {
|
|
37
|
+
const data = await res.json();
|
|
38
|
+
setActivities(data);
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// Silently fail
|
|
42
|
+
}
|
|
43
|
+
}, 3000);
|
|
44
|
+
|
|
45
|
+
return () => clearInterval(interval);
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
const runningCount = activities.filter(a => a.status === 'running').length;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<header className="h-12 border-b border-gray-200 flex items-center justify-between px-4 bg-gray-50 flex-none select-none">
|
|
53
|
+
<div className="flex items-center gap-2">
|
|
54
|
+
<img src="/logo.png" alt="Chronicle Logo" className="w-6 h-6 object-contain" />
|
|
55
|
+
<h1 className="font-bold text-lg tracking-tight text-gray-800">
|
|
56
|
+
Chronicle <span className="text-gray-400 font-normal text-sm">Daemon</span>
|
|
57
|
+
</h1>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="flex gap-3 items-center">
|
|
60
|
+
{/* Thinking Toggle Button */}
|
|
61
|
+
<button
|
|
62
|
+
onClick={() => setIsDrawerOpen(true)}
|
|
63
|
+
className={clsx(
|
|
64
|
+
"flex items-center gap-1.5 px-2.5 py-1.5 rounded text-xs font-medium transition-all",
|
|
65
|
+
runningCount > 0
|
|
66
|
+
? "bg-purple-100 text-purple-700 hover:bg-purple-200 ring-2 ring-purple-300 ring-offset-1"
|
|
67
|
+
: activities.length > 0
|
|
68
|
+
? "bg-purple-50 text-purple-600 hover:bg-purple-100"
|
|
69
|
+
: "bg-gray-100 text-gray-500 hover:bg-gray-200"
|
|
70
|
+
)}
|
|
71
|
+
title="Open Thinking Platform"
|
|
72
|
+
>
|
|
73
|
+
<Brain size={14} className={runningCount > 0 ? 'animate-pulse' : ''} />
|
|
74
|
+
<span>Thinking</span>
|
|
75
|
+
{activities.length > 0 && (
|
|
76
|
+
<span className={clsx(
|
|
77
|
+
"text-[9px] px-1.5 py-0.5 rounded-full font-bold",
|
|
78
|
+
runningCount > 0
|
|
79
|
+
? "bg-purple-500 text-white animate-pulse"
|
|
80
|
+
: "bg-purple-200 text-purple-700"
|
|
81
|
+
)}>
|
|
82
|
+
{runningCount > 0 ? `${runningCount} ACTIVE` : activities.length}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
</button>
|
|
86
|
+
|
|
87
|
+
{/* Status Indicator */}
|
|
88
|
+
<div className="flex items-center gap-1.5 opacity-60">
|
|
89
|
+
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500"></div>
|
|
90
|
+
<span className="text-xs font-bold text-gray-600">ONLINE</span>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</header>
|
|
94
|
+
|
|
95
|
+
<ThinkingDrawer
|
|
96
|
+
isOpen={isDrawerOpen}
|
|
97
|
+
onClose={() => setIsDrawerOpen(false)}
|
|
98
|
+
activities={activities}
|
|
99
|
+
/>
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Activity, ChevronUp, ChevronDown, CheckCircle2, AlertCircle, Clock, Loader2, XCircle, Trash2 } from 'lucide-react';
|
|
4
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { clsx } from 'clsx';
|
|
7
|
+
|
|
8
|
+
interface JobRecord {
|
|
9
|
+
id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
data: string;
|
|
12
|
+
status: string;
|
|
13
|
+
created_at: number;
|
|
14
|
+
updated_at: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
jobs: JobRecord[];
|
|
19
|
+
onKillJob?: () => void;
|
|
20
|
+
onClearQueue?: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Format job data into readable text
|
|
24
|
+
function formatJobData(data: string): string {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(data);
|
|
27
|
+
if (parsed.filePath) {
|
|
28
|
+
const filename = parsed.filePath.split('/').pop();
|
|
29
|
+
return `${filename}${parsed.event ? ` (${parsed.event})` : ''}`;
|
|
30
|
+
}
|
|
31
|
+
if (parsed.projectPath) {
|
|
32
|
+
const folder = parsed.projectPath.split('/').pop();
|
|
33
|
+
return folder || 'project';
|
|
34
|
+
}
|
|
35
|
+
if (parsed.path) {
|
|
36
|
+
const folder = parsed.path.split('/').pop();
|
|
37
|
+
return folder || 'path';
|
|
38
|
+
}
|
|
39
|
+
const keys = Object.keys(parsed);
|
|
40
|
+
if (keys.length > 0) {
|
|
41
|
+
const firstKey = keys[0];
|
|
42
|
+
const value = String(parsed[firstKey]).split('/').pop();
|
|
43
|
+
return value || parsed[firstKey];
|
|
44
|
+
}
|
|
45
|
+
return '';
|
|
46
|
+
} catch {
|
|
47
|
+
return data.slice(0, 50);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function JobQueue({ jobs, onKillJob, onClearQueue }: Props) {
|
|
52
|
+
// false = collapsed (header only), true = expanded
|
|
53
|
+
const [isExpanded, setIsExpanded] = useState(true);
|
|
54
|
+
|
|
55
|
+
// Get processing job for collapsed view
|
|
56
|
+
const processingJob = jobs.find(j => j.status === 'processing');
|
|
57
|
+
|
|
58
|
+
// Also find recently completed/failed jobs (last 5 seconds) to show activity even if fast
|
|
59
|
+
const recentJob = jobs.find(j =>
|
|
60
|
+
j.status !== 'pending' &&
|
|
61
|
+
j.status !== 'processing' &&
|
|
62
|
+
(Date.now() - (j.updated_at * 1000)) < 5000
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const activeJob = processingJob || recentJob;
|
|
66
|
+
const pendingCount = jobs.filter(j => j.status === 'pending').length;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className={clsx(
|
|
70
|
+
"bg-gray-900 text-gray-400 flex flex-col transition-all duration-300 ease-in-out",
|
|
71
|
+
isExpanded ? "h-72" : "h-10"
|
|
72
|
+
)}>
|
|
73
|
+
{/* Header - always visible */}
|
|
74
|
+
<div className="px-4 py-2.5 bg-gray-950 border-b border-gray-800 flex justify-between items-center select-none flex-none">
|
|
75
|
+
<div className="flex items-center gap-3">
|
|
76
|
+
<div className="flex items-center gap-2">
|
|
77
|
+
<Activity size={14} className={clsx(activeJob ? "text-blue-400 animate-pulse" : "text-gray-600")} />
|
|
78
|
+
<span className="text-xs font-bold uppercase tracking-wider font-mono text-gray-500">System Jobs</span>
|
|
79
|
+
</div>
|
|
80
|
+
{pendingCount > 0 && (
|
|
81
|
+
<span className="px-1.5 py-0.5 rounded-full bg-blue-500/10 text-blue-400 text-[10px] font-mono border border-blue-500/20">
|
|
82
|
+
{pendingCount} pending
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
<div className="text-[9px] text-gray-700 font-mono">
|
|
87
|
+
Updated: {new Date().toLocaleTimeString()}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{/* Active job shown in header when collapsed */}
|
|
91
|
+
{!isExpanded && activeJob && (
|
|
92
|
+
<div className="flex items-center gap-2 flex-1 justify-center mx-4">
|
|
93
|
+
{activeJob.status === 'processing' ? (
|
|
94
|
+
<Loader2 size={12} className="text-blue-400 animate-spin" />
|
|
95
|
+
) : (
|
|
96
|
+
<CheckCircle2 size={12} className="text-green-500" />
|
|
97
|
+
)}
|
|
98
|
+
<span className="font-mono text-xs text-blue-300">{activeJob.name}</span>
|
|
99
|
+
<span className="text-xs text-gray-500">{formatJobData(activeJob.data)}</span>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
{!isExpanded && !processingJob && (
|
|
104
|
+
<div className="flex-1 text-center">
|
|
105
|
+
<span className="text-[10px] text-gray-600 font-mono">Idle</span>
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{/* Action buttons */}
|
|
110
|
+
<div className="flex items-center gap-1">
|
|
111
|
+
{processingJob && (
|
|
112
|
+
<button
|
|
113
|
+
onClick={onKillJob}
|
|
114
|
+
className="px-2 py-1 text-[10px] font-mono text-red-400 hover:bg-red-500/20 rounded transition-colors flex items-center gap-1"
|
|
115
|
+
title="Kill current job"
|
|
116
|
+
>
|
|
117
|
+
<XCircle size={12} />
|
|
118
|
+
Kill
|
|
119
|
+
</button>
|
|
120
|
+
)}
|
|
121
|
+
{pendingCount > 0 && (
|
|
122
|
+
<button
|
|
123
|
+
onClick={onClearQueue}
|
|
124
|
+
className="px-2 py-1 text-[10px] font-mono text-orange-400 hover:bg-orange-500/20 rounded transition-colors flex items-center gap-1"
|
|
125
|
+
title="Clear all pending jobs"
|
|
126
|
+
>
|
|
127
|
+
<Trash2 size={12} />
|
|
128
|
+
Clear
|
|
129
|
+
</button>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{/* Chevron button - only this controls expand/collapse */}
|
|
133
|
+
<button
|
|
134
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
135
|
+
className="p-1.5 hover:bg-gray-800 rounded transition-colors ml-1"
|
|
136
|
+
title={isExpanded ? "Collapse" : "Expand"}
|
|
137
|
+
>
|
|
138
|
+
{isExpanded ? <ChevronDown size={16} className="text-gray-400" /> : <ChevronUp size={16} className="text-gray-400" />}
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Content Area - only visible when expanded */}
|
|
144
|
+
{isExpanded && (
|
|
145
|
+
<div className="flex-1 overflow-y-auto p-2 space-y-0.5 bg-gray-900">
|
|
146
|
+
{jobs.map(job => (
|
|
147
|
+
<div
|
|
148
|
+
key={job.id}
|
|
149
|
+
className={clsx(
|
|
150
|
+
"group flex items-center justify-between px-3 py-2 rounded transition-colors border border-transparent",
|
|
151
|
+
job.status === 'processing' ? "bg-blue-500/10 border-blue-500/20" : "hover:bg-white/5 hover:border-white/5"
|
|
152
|
+
)}
|
|
153
|
+
>
|
|
154
|
+
<div className="flex items-center gap-3 overflow-hidden">
|
|
155
|
+
<StatusIcon status={job.status} />
|
|
156
|
+
<span className={clsx(
|
|
157
|
+
"font-mono text-xs font-bold whitespace-nowrap",
|
|
158
|
+
job.status === 'processing' ? "text-blue-300" : "text-gray-300"
|
|
159
|
+
)}>
|
|
160
|
+
{job.name}
|
|
161
|
+
</span>
|
|
162
|
+
<span className="text-xs text-gray-500 truncate max-w-[400px] group-hover:text-gray-400 transition-colors">
|
|
163
|
+
{formatJobData(job.data)}
|
|
164
|
+
</span>
|
|
165
|
+
</div>
|
|
166
|
+
<span className="text-[10px] text-gray-600 font-mono whitespace-nowrap pl-4 flex items-center gap-1" suppressHydrationWarning>
|
|
167
|
+
<Clock size={10} />
|
|
168
|
+
{formatDistanceToNow(job.created_at * 1000, { addSuffix: true })}
|
|
169
|
+
</span>
|
|
170
|
+
</div>
|
|
171
|
+
))}
|
|
172
|
+
{jobs.length === 0 && (
|
|
173
|
+
<div className="p-8 text-center text-gray-700 text-xs font-mono">
|
|
174
|
+
System idle. No jobs.
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function StatusIcon({ status }: { status: string }) {
|
|
184
|
+
switch (status) {
|
|
185
|
+
case 'processing':
|
|
186
|
+
return <Loader2 size={14} className="text-blue-400 animate-spin" />;
|
|
187
|
+
case 'completed':
|
|
188
|
+
return <CheckCircle2 size={14} className="text-emerald-500" />;
|
|
189
|
+
case 'failed':
|
|
190
|
+
return <AlertCircle size={14} className="text-red-500" />;
|
|
191
|
+
default:
|
|
192
|
+
return <div className="w-2 h-2 rounded-full bg-gray-600" />;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { JobQueue } from './JobQueue';
|
|
4
|
+
import { clearQueueAction, killCurrentJobAction } from '../app/actions';
|
|
5
|
+
|
|
6
|
+
interface JobRecord {
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
data: string;
|
|
10
|
+
status: string;
|
|
11
|
+
created_at: number;
|
|
12
|
+
updated_at: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function JobQueueWrapper({ jobs }: { jobs: JobRecord[] }) {
|
|
16
|
+
const handleKillJob = async () => {
|
|
17
|
+
await killCurrentJobAction();
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const handleClearQueue = async () => {
|
|
21
|
+
await clearQueueAction();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<JobQueue
|
|
26
|
+
jobs={jobs}
|
|
27
|
+
onKillJob={handleKillJob}
|
|
28
|
+
onClearQueue={handleClearQueue}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
export function MermaidInit() {
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
// Wait for mermaid to load and initialize
|
|
8
|
+
const initMermaid = () => {
|
|
9
|
+
if (typeof window !== 'undefined' && (window as any).mermaid) {
|
|
10
|
+
(window as any).mermaid.initialize({
|
|
11
|
+
startOnLoad: true,
|
|
12
|
+
theme: 'neutral',
|
|
13
|
+
securityLevel: 'loose'
|
|
14
|
+
});
|
|
15
|
+
} else {
|
|
16
|
+
// Retry if mermaid isn't loaded yet
|
|
17
|
+
setTimeout(initMermaid, 100);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
initMermaid();
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { FileText, Clock, RefreshCw } from 'lucide-react';
|
|
5
|
+
import { clsx } from 'clsx';
|
|
6
|
+
import { DocumentationViewer } from './DocumentationViewer';
|
|
7
|
+
import { FileList } from './FileList';
|
|
8
|
+
import { TriggerSettings } from './TriggerSettings';
|
|
9
|
+
|
|
10
|
+
interface DocFile {
|
|
11
|
+
name: string;
|
|
12
|
+
path: string;
|
|
13
|
+
content: string;
|
|
14
|
+
type: 'root' | 'docs';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface FileRecord {
|
|
18
|
+
id: number;
|
|
19
|
+
path: string;
|
|
20
|
+
hash: string;
|
|
21
|
+
last_modified: number;
|
|
22
|
+
last_indexed: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TriggerConfig {
|
|
26
|
+
on_change: { enabled: boolean; debounce_ms: number };
|
|
27
|
+
on_commit: { enabled: boolean };
|
|
28
|
+
schedule: { enabled: boolean; interval_hours: number };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface DocVersion {
|
|
32
|
+
id: number;
|
|
33
|
+
version_number: number;
|
|
34
|
+
trigger_type: string;
|
|
35
|
+
commit_hash: string | null;
|
|
36
|
+
status: 'draft' | 'pending_review' | 'active' | 'archived';
|
|
37
|
+
created_at: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ThoughtLog {
|
|
41
|
+
doc_path: string;
|
|
42
|
+
thinking_summary: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface Props {
|
|
46
|
+
docs: DocFile[];
|
|
47
|
+
files: FileRecord[];
|
|
48
|
+
projectName: string;
|
|
49
|
+
projectPath: string;
|
|
50
|
+
hasProject: boolean;
|
|
51
|
+
triggerConfig: TriggerConfig | null;
|
|
52
|
+
docStage?: { currentStage: number; totalStages: number } | null;
|
|
53
|
+
isPaused: boolean;
|
|
54
|
+
docVersions?: DocVersion[];
|
|
55
|
+
activeVersionId?: number | null;
|
|
56
|
+
thoughtLogs?: ThoughtLog[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type Tab = 'docs' | 'files';
|
|
60
|
+
|
|
61
|
+
export function ProjectContentArea({
|
|
62
|
+
docs,
|
|
63
|
+
files,
|
|
64
|
+
projectName,
|
|
65
|
+
projectPath,
|
|
66
|
+
hasProject,
|
|
67
|
+
triggerConfig,
|
|
68
|
+
docStage,
|
|
69
|
+
isPaused,
|
|
70
|
+
docVersions = [],
|
|
71
|
+
activeVersionId = null,
|
|
72
|
+
thoughtLogs = []
|
|
73
|
+
}: Props) {
|
|
74
|
+
const [activeTab, setActiveTab] = useState<Tab>('docs');
|
|
75
|
+
const [isUpdating, setIsUpdating] = useState(false);
|
|
76
|
+
|
|
77
|
+
if (!hasProject) {
|
|
78
|
+
return <FileList files={files} title="Recent Files" />;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex flex-col h-full">
|
|
83
|
+
{/* Tab Header */}
|
|
84
|
+
<div className="flex items-center gap-1 px-2 py-1.5 border-b border-gray-200 bg-gray-50 flex-none">
|
|
85
|
+
<button
|
|
86
|
+
onClick={() => setActiveTab('docs')}
|
|
87
|
+
className={clsx(
|
|
88
|
+
"flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors",
|
|
89
|
+
activeTab === 'docs'
|
|
90
|
+
? "bg-white shadow-sm text-blue-600 ring-1 ring-gray-200"
|
|
91
|
+
: "text-gray-600 hover:bg-gray-100"
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
<FileText size={12} />
|
|
95
|
+
Documentation
|
|
96
|
+
</button>
|
|
97
|
+
<button
|
|
98
|
+
onClick={() => setActiveTab('files')}
|
|
99
|
+
className={clsx(
|
|
100
|
+
"flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors",
|
|
101
|
+
activeTab === 'files'
|
|
102
|
+
? "bg-white shadow-sm text-blue-600 ring-1 ring-gray-200"
|
|
103
|
+
: "text-gray-600 hover:bg-gray-100"
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
<Clock size={12} />
|
|
107
|
+
Recent Files
|
|
108
|
+
</button>
|
|
109
|
+
<div className="flex-1" />
|
|
110
|
+
|
|
111
|
+
{/* Trigger Settings */}
|
|
112
|
+
{triggerConfig && (
|
|
113
|
+
<TriggerSettings
|
|
114
|
+
config={triggerConfig}
|
|
115
|
+
projectPath={projectPath}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{/* Update Docs Button */}
|
|
120
|
+
{hasProject && (
|
|
121
|
+
<button
|
|
122
|
+
onClick={async () => {
|
|
123
|
+
setIsUpdating(true);
|
|
124
|
+
try {
|
|
125
|
+
const { triggerDocUpdate } = await import('../app/actions');
|
|
126
|
+
await triggerDocUpdate(projectPath);
|
|
127
|
+
} finally {
|
|
128
|
+
// Small delay so user sees the feedback
|
|
129
|
+
setTimeout(() => setIsUpdating(false), 1000);
|
|
130
|
+
}
|
|
131
|
+
}}
|
|
132
|
+
disabled={isUpdating}
|
|
133
|
+
className={clsx(
|
|
134
|
+
"flex items-center gap-1.5 px-2 py-1 rounded ml-2 text-[10px] font-bold uppercase tracking-wider transition-colors",
|
|
135
|
+
isUpdating
|
|
136
|
+
? "bg-blue-200 text-blue-500 cursor-wait"
|
|
137
|
+
: "bg-blue-100 text-blue-700 hover:bg-blue-200"
|
|
138
|
+
)}
|
|
139
|
+
title="Trigger incremental doc update"
|
|
140
|
+
>
|
|
141
|
+
<RefreshCw size={10} className={isUpdating ? 'animate-spin' : ''} />
|
|
142
|
+
{isUpdating ? 'Updating...' : 'Update'}
|
|
143
|
+
</button>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{/* Pause/Resume Control */}
|
|
147
|
+
{hasProject && (
|
|
148
|
+
<button
|
|
149
|
+
onClick={async () => {
|
|
150
|
+
const { togglePause } = await import('../app/actions');
|
|
151
|
+
await togglePause(projectPath, !isPaused);
|
|
152
|
+
}}
|
|
153
|
+
className={clsx(
|
|
154
|
+
"flex items-center gap-1.5 px-2 py-1 rounded ml-2 text-[10px] font-bold uppercase tracking-wider transition-colors",
|
|
155
|
+
isPaused
|
|
156
|
+
? "bg-amber-100 text-amber-700 hover:bg-amber-200"
|
|
157
|
+
: "bg-gray-100 text-gray-500 hover:bg-gray-200"
|
|
158
|
+
)}
|
|
159
|
+
title={isPaused ? "Resume Updates" : "Pause Updates"}
|
|
160
|
+
>
|
|
161
|
+
{isPaused ? "Paused" : "Active"}
|
|
162
|
+
</button>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
<span className="text-[10px] text-gray-400 font-mono ml-2 truncate max-w-[200px]">{projectName}</span>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{/* Tab Content */}
|
|
169
|
+
<div className="flex-1 overflow-hidden">
|
|
170
|
+
{activeTab === 'docs' ? (
|
|
171
|
+
<DocumentationViewer
|
|
172
|
+
docs={docs}
|
|
173
|
+
projectName={projectName}
|
|
174
|
+
docStage={docStage}
|
|
175
|
+
versions={docVersions}
|
|
176
|
+
currentVersionId={activeVersionId}
|
|
177
|
+
thoughtLogs={thoughtLogs}
|
|
178
|
+
/>
|
|
179
|
+
) : (
|
|
180
|
+
<FileList files={files} title={projectName} />
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Folder, ExternalLink, Plus, Trash2 } from 'lucide-react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { clsx } from 'clsx';
|
|
6
|
+
import { addProjectAction, removeProjectAction } from '../app/actions';
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
|
|
9
|
+
interface Project {
|
|
10
|
+
id: number;
|
|
11
|
+
path: string;
|
|
12
|
+
created_at: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ProjectList({ projects, activeProjectId }: { projects: Project[], activeProjectId?: string }) {
|
|
16
|
+
const router = useRouter();
|
|
17
|
+
const [isAdding, setIsAdding] = useState(false);
|
|
18
|
+
const [removingPath, setRemovingPath] = useState<string | null>(null);
|
|
19
|
+
|
|
20
|
+
const handleRemove = async (projectPath: string) => {
|
|
21
|
+
await removeProjectAction(projectPath);
|
|
22
|
+
setRemovingPath(null);
|
|
23
|
+
router.push('/');
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex flex-col h-full bg-gray-50 border-r border-gray-200">
|
|
28
|
+
{/* Confirmation Modal */}
|
|
29
|
+
{removingPath && (
|
|
30
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
31
|
+
<div className="bg-white rounded-lg shadow-xl p-4 max-w-sm mx-4">
|
|
32
|
+
<h3 className="font-bold text-gray-800 mb-2">Remove Project?</h3>
|
|
33
|
+
<p className="text-sm text-gray-600 mb-4">
|
|
34
|
+
Stop monitoring <span className="font-mono text-xs bg-gray-100 px-1 rounded">{removingPath.split('/').pop()}</span>?
|
|
35
|
+
<br />
|
|
36
|
+
<span className="text-xs text-gray-400">Files will remain on disk but won't be tracked.</span>
|
|
37
|
+
</p>
|
|
38
|
+
<div className="flex justify-end gap-2">
|
|
39
|
+
<button
|
|
40
|
+
onClick={() => setRemovingPath(null)}
|
|
41
|
+
className="px-3 py-1.5 text-sm text-gray-600 hover:text-gray-800"
|
|
42
|
+
>
|
|
43
|
+
Cancel
|
|
44
|
+
</button>
|
|
45
|
+
<button
|
|
46
|
+
onClick={() => handleRemove(removingPath)}
|
|
47
|
+
className="px-3 py-1.5 text-sm bg-red-500 text-white rounded hover:bg-red-600"
|
|
48
|
+
>
|
|
49
|
+
Remove
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
<div className="p-3 border-b border-gray-200 bg-gray-100 flex items-center justify-between">
|
|
57
|
+
<h2 className="text-xs font-bold uppercase tracking-wider text-gray-500 flex items-center gap-2">
|
|
58
|
+
<Folder size={14} />
|
|
59
|
+
<span>Projects</span>
|
|
60
|
+
</h2>
|
|
61
|
+
<button
|
|
62
|
+
onClick={() => setIsAdding(!isAdding)}
|
|
63
|
+
className="p-1 hover:bg-gray-200 rounded text-gray-600 transition-colors"
|
|
64
|
+
title="Add Project"
|
|
65
|
+
>
|
|
66
|
+
<Plus size={14} />
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{isAdding && (
|
|
71
|
+
<form action={async (formData) => {
|
|
72
|
+
await addProjectAction(formData);
|
|
73
|
+
setIsAdding(false);
|
|
74
|
+
}} className="p-2 border-b border-gray-200 bg-white">
|
|
75
|
+
<input
|
|
76
|
+
name="path"
|
|
77
|
+
autoFocus
|
|
78
|
+
placeholder="/absolute/path/to/repo"
|
|
79
|
+
className="w-full text-xs font-mono border border-gray-300 rounded px-2 py-1 mb-1 focus:outline-none focus:border-blue-500"
|
|
80
|
+
/>
|
|
81
|
+
<div className="flex justify-end gap-2">
|
|
82
|
+
<button type="button" onClick={() => setIsAdding(false)} className="text-[10px] text-gray-500 hover:text-gray-800">Cancel</button>
|
|
83
|
+
<button type="submit" className="text-[10px] bg-blue-500 text-white px-2 py-0.5 rounded hover:bg-blue-600">Add</button>
|
|
84
|
+
</div>
|
|
85
|
+
</form>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
<div className="flex-1 overflow-y-auto p-1 space-y-0.5">
|
|
89
|
+
<div
|
|
90
|
+
onClick={() => router.push('/')}
|
|
91
|
+
className={clsx(
|
|
92
|
+
"group flex items-center gap-2 p-2 rounded cursor-pointer transition-all",
|
|
93
|
+
!activeProjectId ? "bg-white shadow-sm ring-1 ring-gray-200" : "hover:bg-gray-100 text-gray-600"
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
<Folder size={14} className={clsx(!activeProjectId ? "text-blue-500" : "text-gray-400")} />
|
|
97
|
+
<span className="font-mono text-xs font-medium">All Projects</span>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{projects.map(project => {
|
|
101
|
+
const isActive = activeProjectId === String(project.id);
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
key={project.id}
|
|
105
|
+
onClick={() => router.push(`/?projectId=${project.id}`)}
|
|
106
|
+
className={clsx(
|
|
107
|
+
"group flex items-center gap-2 p-2 rounded cursor-pointer transition-all",
|
|
108
|
+
isActive ? "bg-white shadow-sm ring-1 ring-gray-200" : "hover:bg-gray-100 text-gray-600"
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
<Folder size={14} className={clsx(isActive ? "text-blue-500" : "text-gray-400")} />
|
|
112
|
+
<div className="flex flex-col min-w-0 flex-1">
|
|
113
|
+
<span className="font-mono text-xs font-medium truncate" title={project.path}>
|
|
114
|
+
{project.path.split('/').pop()}
|
|
115
|
+
</span>
|
|
116
|
+
<span className="text-[10px] text-gray-400 truncate hidden group-hover:block">
|
|
117
|
+
{project.path}
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
<button
|
|
121
|
+
onClick={(e) => {
|
|
122
|
+
e.stopPropagation();
|
|
123
|
+
setRemovingPath(project.path);
|
|
124
|
+
}}
|
|
125
|
+
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-100 rounded text-gray-400 hover:text-red-500 transition-all"
|
|
126
|
+
title="Remove project"
|
|
127
|
+
>
|
|
128
|
+
<Trash2 size={12} />
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
})}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|