botschat 0.1.0
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/LICENSE +201 -0
- package/README.md +213 -0
- package/migrations/0001_initial.sql +88 -0
- package/migrations/0002_rename_projects_to_channels.sql +53 -0
- package/migrations/0003_messages.sql +14 -0
- package/migrations/0004_jobs.sql +15 -0
- package/migrations/0005_deleted_cron_jobs.sql +6 -0
- package/migrations/0006_tasks_add_model.sql +2 -0
- package/migrations/0007_sessions.sql +25 -0
- package/migrations/0008_remove_openclaw_fields.sql +8 -0
- package/package.json +53 -0
- package/packages/api/package.json +17 -0
- package/packages/api/src/do/connection-do.ts +929 -0
- package/packages/api/src/env.ts +8 -0
- package/packages/api/src/index.ts +297 -0
- package/packages/api/src/routes/agents.ts +68 -0
- package/packages/api/src/routes/auth.ts +105 -0
- package/packages/api/src/routes/channels.ts +185 -0
- package/packages/api/src/routes/jobs.ts +65 -0
- package/packages/api/src/routes/models.ts +22 -0
- package/packages/api/src/routes/pairing.ts +76 -0
- package/packages/api/src/routes/projects.ts +177 -0
- package/packages/api/src/routes/sessions.ts +171 -0
- package/packages/api/src/routes/tasks.ts +375 -0
- package/packages/api/src/routes/upload.ts +52 -0
- package/packages/api/src/utils/auth.ts +101 -0
- package/packages/api/src/utils/id.ts +19 -0
- package/packages/api/tsconfig.json +18 -0
- package/packages/plugin/dist/index.d.ts +19 -0
- package/packages/plugin/dist/index.d.ts.map +1 -0
- package/packages/plugin/dist/index.js +17 -0
- package/packages/plugin/dist/index.js.map +1 -0
- package/packages/plugin/dist/src/accounts.d.ts +12 -0
- package/packages/plugin/dist/src/accounts.d.ts.map +1 -0
- package/packages/plugin/dist/src/accounts.js +103 -0
- package/packages/plugin/dist/src/accounts.js.map +1 -0
- package/packages/plugin/dist/src/channel.d.ts +206 -0
- package/packages/plugin/dist/src/channel.d.ts.map +1 -0
- package/packages/plugin/dist/src/channel.js +1248 -0
- package/packages/plugin/dist/src/channel.js.map +1 -0
- package/packages/plugin/dist/src/runtime.d.ts +3 -0
- package/packages/plugin/dist/src/runtime.d.ts.map +1 -0
- package/packages/plugin/dist/src/runtime.js +18 -0
- package/packages/plugin/dist/src/runtime.js.map +1 -0
- package/packages/plugin/dist/src/types.d.ts +179 -0
- package/packages/plugin/dist/src/types.d.ts.map +1 -0
- package/packages/plugin/dist/src/types.js +6 -0
- package/packages/plugin/dist/src/types.js.map +1 -0
- package/packages/plugin/dist/src/ws-client.d.ts +51 -0
- package/packages/plugin/dist/src/ws-client.d.ts.map +1 -0
- package/packages/plugin/dist/src/ws-client.js +170 -0
- package/packages/plugin/dist/src/ws-client.js.map +1 -0
- package/packages/plugin/openclaw.plugin.json +11 -0
- package/packages/plugin/package.json +39 -0
- package/packages/plugin/tsconfig.json +20 -0
- package/packages/web/dist/assets/index-C-wI8eHy.css +1 -0
- package/packages/web/dist/assets/index-CbPEKHLG.js +93 -0
- package/packages/web/dist/index.html +17 -0
- package/packages/web/index.html +16 -0
- package/packages/web/package.json +29 -0
- package/packages/web/postcss.config.js +6 -0
- package/packages/web/src/App.tsx +827 -0
- package/packages/web/src/api.ts +242 -0
- package/packages/web/src/components/ChatWindow.tsx +864 -0
- package/packages/web/src/components/CronDetail.tsx +943 -0
- package/packages/web/src/components/CronSidebar.tsx +123 -0
- package/packages/web/src/components/DebugLogPanel.tsx +258 -0
- package/packages/web/src/components/IconRail.tsx +163 -0
- package/packages/web/src/components/JobList.tsx +120 -0
- package/packages/web/src/components/LoginPage.tsx +178 -0
- package/packages/web/src/components/MessageContent.tsx +1082 -0
- package/packages/web/src/components/ModelSelect.tsx +87 -0
- package/packages/web/src/components/ScheduleEditor.tsx +403 -0
- package/packages/web/src/components/SessionTabs.tsx +246 -0
- package/packages/web/src/components/Sidebar.tsx +331 -0
- package/packages/web/src/components/TaskBar.tsx +413 -0
- package/packages/web/src/components/ThreadPanel.tsx +212 -0
- package/packages/web/src/debug-log.ts +58 -0
- package/packages/web/src/index.css +170 -0
- package/packages/web/src/main.tsx +10 -0
- package/packages/web/src/store.ts +492 -0
- package/packages/web/src/ws.ts +99 -0
- package/packages/web/tailwind.config.js +65 -0
- package/packages/web/tsconfig.json +18 -0
- package/packages/web/vite.config.ts +20 -0
- package/scripts/dev.sh +122 -0
- package/tsconfig.json +18 -0
- package/wrangler.toml +40 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useAppState, useAppDispatch } from "../store";
|
|
3
|
+
|
|
4
|
+
function relativeTime(ts: number): string {
|
|
5
|
+
const now = Date.now() / 1000;
|
|
6
|
+
const diff = now - ts;
|
|
7
|
+
if (diff < 60) return "just now";
|
|
8
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
9
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
10
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function CronSidebar() {
|
|
14
|
+
const state = useAppState();
|
|
15
|
+
const dispatch = useAppDispatch();
|
|
16
|
+
|
|
17
|
+
const handleSelect = (taskId: string) => {
|
|
18
|
+
dispatch({ type: "SELECT_CRON_TASK", taskId });
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className="flex flex-col h-full"
|
|
24
|
+
style={{ width: 220, minWidth: 160, background: "var(--bg-secondary)", borderRight: "1px solid var(--border)" }}
|
|
25
|
+
>
|
|
26
|
+
{/* Header */}
|
|
27
|
+
<div className="px-4 py-3 flex items-center gap-2">
|
|
28
|
+
<span className="text-[--text-sidebar-active] font-bold text-h2 truncate flex-1">
|
|
29
|
+
Automations
|
|
30
|
+
</span>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
{/* Connection status */}
|
|
34
|
+
<div className="px-4 pb-2">
|
|
35
|
+
<div className="flex items-center gap-1.5">
|
|
36
|
+
<div
|
|
37
|
+
className="w-2 h-2 rounded-full"
|
|
38
|
+
style={{ background: state.openclawConnected ? "var(--accent-green)" : "var(--accent-red)" }}
|
|
39
|
+
/>
|
|
40
|
+
<span className="text-tiny text-[--text-muted]">
|
|
41
|
+
{state.openclawConnected ? "OpenClaw connected" : "OpenClaw offline"}
|
|
42
|
+
</span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{/* Task count */}
|
|
47
|
+
<div className="px-4 pb-2">
|
|
48
|
+
<span className="text-tiny text-[--text-muted]">
|
|
49
|
+
{state.cronTasks.length} cron job{state.cronTasks.length !== 1 ? "s" : ""}
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{/* Job list */}
|
|
54
|
+
<div className="flex-1 overflow-y-auto sidebar-scroll">
|
|
55
|
+
{state.cronTasks.length === 0 ? (
|
|
56
|
+
<div className="px-4 py-8 text-center">
|
|
57
|
+
<svg
|
|
58
|
+
className="w-10 h-10 mx-auto mb-3"
|
|
59
|
+
fill="none"
|
|
60
|
+
viewBox="0 0 24 24"
|
|
61
|
+
stroke="currentColor"
|
|
62
|
+
strokeWidth={1}
|
|
63
|
+
style={{ color: "var(--text-muted)" }}
|
|
64
|
+
>
|
|
65
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
66
|
+
</svg>
|
|
67
|
+
<p className="text-caption text-[--text-muted]">
|
|
68
|
+
No automations yet.
|
|
69
|
+
</p>
|
|
70
|
+
<p className="text-tiny text-[--text-muted] mt-1">
|
|
71
|
+
Cron jobs from OpenClaw will appear here automatically.
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
) : (
|
|
75
|
+
state.cronTasks.map((task) => {
|
|
76
|
+
const isSelected = state.selectedCronTaskId === task.id;
|
|
77
|
+
const isEnabled = task.enabled;
|
|
78
|
+
// Determine status dot color
|
|
79
|
+
let dotColor = "var(--accent-green)"; // enabled
|
|
80
|
+
if (!isEnabled) dotColor = "var(--text-muted)"; // paused
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<button
|
|
84
|
+
key={task.id}
|
|
85
|
+
onClick={() => handleSelect(task.id)}
|
|
86
|
+
className="w-full text-left py-2 transition-colors"
|
|
87
|
+
style={{
|
|
88
|
+
paddingLeft: isSelected ? 13 : 16,
|
|
89
|
+
paddingRight: 16,
|
|
90
|
+
background: isSelected ? "var(--bg-hover)" : undefined,
|
|
91
|
+
borderLeft: isSelected ? "3px solid var(--bg-active)" : "3px solid transparent",
|
|
92
|
+
color: isSelected ? "var(--text-sidebar-active)" : "var(--text-sidebar)",
|
|
93
|
+
}}
|
|
94
|
+
onMouseEnter={(e) => { if (!isSelected) e.currentTarget.style.background = "var(--sidebar-hover)"; }}
|
|
95
|
+
onMouseLeave={(e) => { if (!isSelected) e.currentTarget.style.background = isSelected ? "var(--bg-hover)" : ""; }}
|
|
96
|
+
>
|
|
97
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
98
|
+
<div
|
|
99
|
+
className="w-2 h-2 rounded-full flex-shrink-0"
|
|
100
|
+
style={{ background: dotColor }}
|
|
101
|
+
/>
|
|
102
|
+
<span className={`text-body truncate ${isSelected ? "font-bold" : ""}`}>
|
|
103
|
+
{task.name}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
<div className="flex items-center gap-2 mt-0.5 pl-4">
|
|
107
|
+
<span className="text-tiny truncate" style={{ color: "var(--text-muted)" }}>
|
|
108
|
+
{task.schedule ?? "no schedule"}
|
|
109
|
+
</span>
|
|
110
|
+
{!isEnabled && (
|
|
111
|
+
<span className="text-tiny" style={{ color: "var(--text-muted)" }}>
|
|
112
|
+
paused
|
|
113
|
+
</span>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</button>
|
|
117
|
+
);
|
|
118
|
+
})
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { getLogEntries, subscribeLog, clearLog, type LogEntry, type LogLevel } from "../debug-log";
|
|
3
|
+
|
|
4
|
+
const LEVEL_COLORS: Record<LogLevel, string> = {
|
|
5
|
+
info: "var(--text-muted)",
|
|
6
|
+
warn: "var(--accent-yellow)",
|
|
7
|
+
error: "var(--accent-red)",
|
|
8
|
+
"ws-in": "#6BCB77",
|
|
9
|
+
"ws-out": "#4D96FF",
|
|
10
|
+
api: "#C77DFF",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const LEVEL_LABELS: Record<LogLevel, string> = {
|
|
14
|
+
info: "INF",
|
|
15
|
+
warn: "WRN",
|
|
16
|
+
error: "ERR",
|
|
17
|
+
"ws-in": "WS\u2193",
|
|
18
|
+
"ws-out": "WS\u2191",
|
|
19
|
+
api: "API",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function formatTs(ts: number): string {
|
|
23
|
+
const d = new Date(ts);
|
|
24
|
+
return d.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }) +
|
|
25
|
+
"." + String(d.getMilliseconds()).padStart(3, "0");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function DebugLogPanel() {
|
|
29
|
+
const entries = useSyncExternalStore(subscribeLog, getLogEntries);
|
|
30
|
+
const [open, setOpen] = useState(false);
|
|
31
|
+
const [filter, setFilter] = useState<LogLevel | "all">("all");
|
|
32
|
+
const [expandedIds, setExpandedIds] = useState<Set<number>>(new Set());
|
|
33
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
34
|
+
const autoScrollRef = useRef(true);
|
|
35
|
+
|
|
36
|
+
// Auto-scroll to bottom when new entries arrive
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (open && autoScrollRef.current && scrollRef.current) {
|
|
39
|
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
40
|
+
}
|
|
41
|
+
}, [entries.length, open]);
|
|
42
|
+
|
|
43
|
+
const handleScroll = () => {
|
|
44
|
+
if (!scrollRef.current) return;
|
|
45
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
|
46
|
+
autoScrollRef.current = scrollHeight - scrollTop - clientHeight < 40;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const toggleExpand = (id: number) => {
|
|
50
|
+
setExpandedIds((prev) => {
|
|
51
|
+
const next = new Set(prev);
|
|
52
|
+
if (next.has(id)) next.delete(id);
|
|
53
|
+
else next.add(id);
|
|
54
|
+
return next;
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const filtered = filter === "all" ? entries : entries.filter((e) => e.level === filter);
|
|
59
|
+
const entryCount = entries.length;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
style={{
|
|
64
|
+
flexShrink: 0,
|
|
65
|
+
fontFamily: "var(--font-mono)",
|
|
66
|
+
fontSize: 12,
|
|
67
|
+
lineHeight: 1.5,
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{/* Toggle bar */}
|
|
71
|
+
<div
|
|
72
|
+
onClick={() => setOpen(!open)}
|
|
73
|
+
style={{
|
|
74
|
+
display: "flex",
|
|
75
|
+
alignItems: "center",
|
|
76
|
+
gap: 8,
|
|
77
|
+
padding: "3px 12px",
|
|
78
|
+
background: "var(--bg-secondary)",
|
|
79
|
+
borderTop: "1px solid var(--border)",
|
|
80
|
+
cursor: "pointer",
|
|
81
|
+
userSelect: "none",
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<svg
|
|
85
|
+
width={10} height={10}
|
|
86
|
+
viewBox="0 0 10 10"
|
|
87
|
+
style={{ transform: open ? "rotate(180deg)" : "rotate(0deg)", transition: "transform 0.15s", flexShrink: 0 }}
|
|
88
|
+
>
|
|
89
|
+
<path d="M1 7L5 3l4 4" stroke="var(--text-muted)" strokeWidth={1.5} fill="none" />
|
|
90
|
+
</svg>
|
|
91
|
+
<span style={{ color: "var(--text-muted)", fontWeight: 600, fontSize: 11 }}>
|
|
92
|
+
Debug Log
|
|
93
|
+
</span>
|
|
94
|
+
<span style={{ color: "var(--text-muted)", fontSize: 10 }}>
|
|
95
|
+
({entryCount})
|
|
96
|
+
</span>
|
|
97
|
+
{/* Quick filter pills when open */}
|
|
98
|
+
{open && (
|
|
99
|
+
<div style={{ display: "flex", gap: 2, marginLeft: 8 }} onClick={(e) => e.stopPropagation()}>
|
|
100
|
+
{(["all", "ws-in", "ws-out", "api", "info", "warn", "error"] as const).map((lvl) => (
|
|
101
|
+
<button
|
|
102
|
+
key={lvl}
|
|
103
|
+
onClick={() => setFilter(lvl)}
|
|
104
|
+
style={{
|
|
105
|
+
padding: "1px 6px",
|
|
106
|
+
borderRadius: 3,
|
|
107
|
+
border: "none",
|
|
108
|
+
fontSize: 10,
|
|
109
|
+
fontFamily: "var(--font-mono)",
|
|
110
|
+
cursor: "pointer",
|
|
111
|
+
background: filter === lvl ? "var(--bg-active)" : "var(--bg-hover)",
|
|
112
|
+
color: filter === lvl ? "#fff" : (lvl === "all" ? "var(--text-muted)" : LEVEL_COLORS[lvl as LogLevel]),
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{lvl === "all" ? "ALL" : LEVEL_LABELS[lvl as LogLevel]}
|
|
116
|
+
</button>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
{open && (
|
|
121
|
+
<button
|
|
122
|
+
onClick={(e) => { e.stopPropagation(); clearLog(); }}
|
|
123
|
+
style={{
|
|
124
|
+
marginLeft: "auto",
|
|
125
|
+
padding: "1px 8px",
|
|
126
|
+
borderRadius: 3,
|
|
127
|
+
border: "none",
|
|
128
|
+
fontSize: 10,
|
|
129
|
+
fontFamily: "var(--font-mono)",
|
|
130
|
+
cursor: "pointer",
|
|
131
|
+
background: "var(--bg-hover)",
|
|
132
|
+
color: "var(--text-muted)",
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
Clear
|
|
136
|
+
</button>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{/* Log content */}
|
|
141
|
+
{open && (
|
|
142
|
+
<div
|
|
143
|
+
ref={scrollRef}
|
|
144
|
+
onScroll={handleScroll}
|
|
145
|
+
style={{
|
|
146
|
+
height: 220,
|
|
147
|
+
overflowY: "auto",
|
|
148
|
+
overflowX: "hidden",
|
|
149
|
+
background: "var(--bg-primary)",
|
|
150
|
+
borderTop: "1px solid var(--border)",
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
{filtered.length === 0 && (
|
|
154
|
+
<div style={{ padding: "16px 12px", color: "var(--text-muted)", textAlign: "center" }}>
|
|
155
|
+
No log entries yet.
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
{filtered.map((entry) => (
|
|
159
|
+
<LogRow
|
|
160
|
+
key={entry.id}
|
|
161
|
+
entry={entry}
|
|
162
|
+
expanded={expandedIds.has(entry.id)}
|
|
163
|
+
onToggleExpand={() => toggleExpand(entry.id)}
|
|
164
|
+
/>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function LogRow({
|
|
173
|
+
entry,
|
|
174
|
+
expanded,
|
|
175
|
+
onToggleExpand,
|
|
176
|
+
}: {
|
|
177
|
+
entry: LogEntry;
|
|
178
|
+
expanded: boolean;
|
|
179
|
+
onToggleExpand: () => void;
|
|
180
|
+
}) {
|
|
181
|
+
const levelColor = LEVEL_COLORS[entry.level];
|
|
182
|
+
const levelLabel = LEVEL_LABELS[entry.level];
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div
|
|
186
|
+
style={{
|
|
187
|
+
padding: "1px 12px",
|
|
188
|
+
borderBottom: "1px solid var(--border)",
|
|
189
|
+
wordBreak: "break-all",
|
|
190
|
+
}}
|
|
191
|
+
onMouseEnter={(e) => (e.currentTarget.style.background = "var(--bg-hover)")}
|
|
192
|
+
onMouseLeave={(e) => (e.currentTarget.style.background = "")}
|
|
193
|
+
>
|
|
194
|
+
<div style={{ display: "flex", gap: 8, alignItems: "flex-start" }}>
|
|
195
|
+
{/* Timestamp */}
|
|
196
|
+
<span style={{ color: "var(--text-muted)", flexShrink: 0, width: 85 }}>
|
|
197
|
+
{formatTs(entry.ts)}
|
|
198
|
+
</span>
|
|
199
|
+
{/* Level badge */}
|
|
200
|
+
<span
|
|
201
|
+
style={{
|
|
202
|
+
color: levelColor,
|
|
203
|
+
fontWeight: 700,
|
|
204
|
+
flexShrink: 0,
|
|
205
|
+
width: 28,
|
|
206
|
+
textAlign: "center",
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
{levelLabel}
|
|
210
|
+
</span>
|
|
211
|
+
{/* Tag */}
|
|
212
|
+
<span style={{ color: "var(--text-secondary)", flexShrink: 0, minWidth: 50 }}>
|
|
213
|
+
[{entry.tag}]
|
|
214
|
+
</span>
|
|
215
|
+
{/* Message */}
|
|
216
|
+
<span style={{ color: "var(--text-primary)", flex: 1 }}>
|
|
217
|
+
{entry.message}
|
|
218
|
+
</span>
|
|
219
|
+
{/* Expand toggle for detail */}
|
|
220
|
+
{entry.detail && (
|
|
221
|
+
<button
|
|
222
|
+
onClick={onToggleExpand}
|
|
223
|
+
style={{
|
|
224
|
+
border: "none",
|
|
225
|
+
background: "none",
|
|
226
|
+
cursor: "pointer",
|
|
227
|
+
color: "var(--text-muted)",
|
|
228
|
+
fontSize: 10,
|
|
229
|
+
padding: "0 4px",
|
|
230
|
+
flexShrink: 0,
|
|
231
|
+
}}
|
|
232
|
+
>
|
|
233
|
+
{expanded ? "\u25BC" : "\u25B6"}
|
|
234
|
+
</button>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
{/* Expanded detail */}
|
|
238
|
+
{expanded && entry.detail && (
|
|
239
|
+
<pre
|
|
240
|
+
style={{
|
|
241
|
+
margin: "2px 0 4px 121px",
|
|
242
|
+
padding: "4px 8px",
|
|
243
|
+
background: "var(--code-bg)",
|
|
244
|
+
borderRadius: 3,
|
|
245
|
+
color: "var(--text-secondary)",
|
|
246
|
+
fontSize: 11,
|
|
247
|
+
whiteSpace: "pre-wrap",
|
|
248
|
+
wordBreak: "break-all",
|
|
249
|
+
maxHeight: 200,
|
|
250
|
+
overflowY: "auto",
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
{entry.detail}
|
|
254
|
+
</pre>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useAppState, useAppDispatch, type ActiveView } from "../store";
|
|
3
|
+
import { setToken } from "../api";
|
|
4
|
+
import { dlog } from "../debug-log";
|
|
5
|
+
|
|
6
|
+
type IconRailProps = {
|
|
7
|
+
onToggleTheme: () => void;
|
|
8
|
+
onOpenSettings: () => void;
|
|
9
|
+
theme: "dark" | "light";
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function IconRail({ onToggleTheme, onOpenSettings, theme }: IconRailProps) {
|
|
13
|
+
const state = useAppState();
|
|
14
|
+
const dispatch = useAppDispatch();
|
|
15
|
+
|
|
16
|
+
const handleLogout = () => {
|
|
17
|
+
dlog.info("Auth", `Logout — user ${state.user?.email}`);
|
|
18
|
+
setToken(null);
|
|
19
|
+
dispatch({ type: "LOGOUT" });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const setView = (view: ActiveView) => {
|
|
23
|
+
dlog.info("Nav", `Switch view → ${view}`);
|
|
24
|
+
dispatch({ type: "SET_ACTIVE_VIEW", view });
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const userInitial = state.user?.displayName?.[0]?.toUpperCase()
|
|
28
|
+
?? state.user?.email?.[0]?.toUpperCase()
|
|
29
|
+
?? "?";
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className="flex flex-col items-center py-3 gap-2 h-full"
|
|
34
|
+
style={{ width: 68, background: "var(--bg-primary)", borderRight: "1px solid var(--border)" }}
|
|
35
|
+
>
|
|
36
|
+
{/* Workspace icon */}
|
|
37
|
+
<button
|
|
38
|
+
className="w-9 h-9 rounded-lg flex items-center justify-center text-white font-bold text-sm hover:rounded-xl transition-all"
|
|
39
|
+
style={{ background: "#1264A3" }}
|
|
40
|
+
title="BotsChat"
|
|
41
|
+
>
|
|
42
|
+
BC
|
|
43
|
+
</button>
|
|
44
|
+
|
|
45
|
+
<div className="w-8 border-t my-1" style={{ borderColor: "var(--sidebar-divider)" }} />
|
|
46
|
+
|
|
47
|
+
{/* Home */}
|
|
48
|
+
<RailIcon
|
|
49
|
+
label="Home"
|
|
50
|
+
active={false}
|
|
51
|
+
icon={
|
|
52
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
53
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12l8.954-8.955a1.126 1.126 0 011.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
|
|
54
|
+
</svg>
|
|
55
|
+
}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
{/* Messages */}
|
|
59
|
+
<RailIcon
|
|
60
|
+
label="Messages"
|
|
61
|
+
active={state.activeView === "messages"}
|
|
62
|
+
onClick={() => setView("messages")}
|
|
63
|
+
icon={
|
|
64
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
65
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" />
|
|
66
|
+
</svg>
|
|
67
|
+
}
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
{/* Automations */}
|
|
71
|
+
<RailIcon
|
|
72
|
+
label="Automations"
|
|
73
|
+
active={state.activeView === "automations"}
|
|
74
|
+
onClick={() => setView("automations")}
|
|
75
|
+
icon={
|
|
76
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
77
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
78
|
+
</svg>
|
|
79
|
+
}
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
<div className="flex-1" />
|
|
83
|
+
|
|
84
|
+
{/* Settings */}
|
|
85
|
+
<RailIcon
|
|
86
|
+
label="Settings"
|
|
87
|
+
active={false}
|
|
88
|
+
onClick={onOpenSettings}
|
|
89
|
+
icon={
|
|
90
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
91
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
|
92
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
93
|
+
</svg>
|
|
94
|
+
}
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
{/* Theme toggle */}
|
|
98
|
+
<RailIcon
|
|
99
|
+
label={theme === "dark" ? "Light mode" : "Dark mode"}
|
|
100
|
+
active={false}
|
|
101
|
+
onClick={onToggleTheme}
|
|
102
|
+
icon={
|
|
103
|
+
theme === "dark" ? (
|
|
104
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
105
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
|
106
|
+
</svg>
|
|
107
|
+
) : (
|
|
108
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
109
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
|
110
|
+
</svg>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
{/* User avatar */}
|
|
116
|
+
<button
|
|
117
|
+
onClick={handleLogout}
|
|
118
|
+
className="w-7 h-7 rounded flex items-center justify-center text-xs font-bold text-white mt-1"
|
|
119
|
+
style={{ background: "#9B59B6" }}
|
|
120
|
+
title={`${state.user?.displayName ?? state.user?.email} (click to logout)`}
|
|
121
|
+
>
|
|
122
|
+
{userInitial}
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function RailIcon({
|
|
129
|
+
label,
|
|
130
|
+
active,
|
|
131
|
+
icon,
|
|
132
|
+
onClick,
|
|
133
|
+
}: {
|
|
134
|
+
label: string;
|
|
135
|
+
active: boolean;
|
|
136
|
+
icon: React.ReactNode;
|
|
137
|
+
onClick?: () => void;
|
|
138
|
+
}) {
|
|
139
|
+
return (
|
|
140
|
+
<div className="relative flex items-center justify-center">
|
|
141
|
+
{/* Active indicator - left bar */}
|
|
142
|
+
{active && (
|
|
143
|
+
<div
|
|
144
|
+
className="absolute left-0 w-[3px] h-5 rounded-r-sm"
|
|
145
|
+
style={{ left: -4, background: "var(--text-sidebar-active)" }}
|
|
146
|
+
/>
|
|
147
|
+
)}
|
|
148
|
+
<button
|
|
149
|
+
onClick={onClick}
|
|
150
|
+
className={`w-9 h-9 rounded-lg flex items-center justify-center transition-colors ${
|
|
151
|
+
active ? "text-[--text-sidebar-active]" : "text-[--text-sidebar] hover:text-[--text-sidebar-active]"
|
|
152
|
+
}`}
|
|
153
|
+
style={active ? { background: "var(--sidebar-hover)" } : undefined}
|
|
154
|
+
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = "var(--sidebar-hover)"; }}
|
|
155
|
+
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = ""; }}
|
|
156
|
+
title={label}
|
|
157
|
+
aria-label={label}
|
|
158
|
+
>
|
|
159
|
+
{icon}
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Job } from "../api";
|
|
3
|
+
|
|
4
|
+
type JobListProps = {
|
|
5
|
+
jobs: Job[];
|
|
6
|
+
selectedJobId: string | null;
|
|
7
|
+
onSelectJob: (jobId: string) => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function statusLabel(status: string): string {
|
|
11
|
+
switch (status) {
|
|
12
|
+
case "ok": return "OK";
|
|
13
|
+
case "error": return "ERR";
|
|
14
|
+
case "skipped": return "SKIP";
|
|
15
|
+
case "running": return "RUN";
|
|
16
|
+
default: return status.toUpperCase();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function statusColors(status: string): { bg: string; fg: string } {
|
|
21
|
+
switch (status) {
|
|
22
|
+
case "ok": return { bg: "rgba(43,172,118,0.15)", fg: "var(--accent-green)" };
|
|
23
|
+
case "error": return { bg: "rgba(224,30,90,0.15)", fg: "var(--accent-red)" };
|
|
24
|
+
case "running": return { bg: "rgba(29,155,209,0.15)", fg: "var(--text-link)" };
|
|
25
|
+
default: return { bg: "rgba(232,162,48,0.15)", fg: "var(--accent-yellow)" };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function JobList({ jobs, selectedJobId, onSelectJob }: JobListProps) {
|
|
30
|
+
if (jobs.length === 0) {
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className="flex items-center justify-center"
|
|
34
|
+
style={{
|
|
35
|
+
width: 192,
|
|
36
|
+
borderRight: "1px solid var(--border)",
|
|
37
|
+
background: "var(--bg-surface)",
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<div className="text-center p-4">
|
|
41
|
+
<svg
|
|
42
|
+
className="w-8 h-8 mx-auto mb-2"
|
|
43
|
+
fill="none"
|
|
44
|
+
viewBox="0 0 24 24"
|
|
45
|
+
stroke="currentColor"
|
|
46
|
+
strokeWidth={1.5}
|
|
47
|
+
style={{ color: "var(--text-muted)" }}
|
|
48
|
+
>
|
|
49
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
50
|
+
</svg>
|
|
51
|
+
<p className="text-tiny" style={{ color: "var(--text-muted)" }}>
|
|
52
|
+
No runs yet.
|
|
53
|
+
<br />
|
|
54
|
+
Waiting for schedule...
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
className="overflow-y-auto"
|
|
64
|
+
style={{
|
|
65
|
+
width: 192,
|
|
66
|
+
borderRight: "1px solid var(--border)",
|
|
67
|
+
background: "var(--bg-surface)",
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<div className="px-3 py-2" style={{ borderBottom: "1px solid var(--border)" }}>
|
|
71
|
+
<span className="text-tiny uppercase tracking-wider font-bold" style={{ color: "var(--text-muted)" }}>
|
|
72
|
+
Job History
|
|
73
|
+
</span>
|
|
74
|
+
<span className="text-tiny ml-1" style={{ color: "var(--text-muted)" }}>
|
|
75
|
+
({jobs.length})
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
{jobs.map((job, idx) => {
|
|
79
|
+
const colors = statusColors(job.status);
|
|
80
|
+
const displayNum = job.number || jobs.length - idx;
|
|
81
|
+
return (
|
|
82
|
+
<button
|
|
83
|
+
key={job.id}
|
|
84
|
+
onClick={() => onSelectJob(job.id)}
|
|
85
|
+
className={`w-full text-left px-3 py-2 hover:bg-[--bg-hover] transition-colors ${
|
|
86
|
+
selectedJobId === job.id ? "bg-[--bg-hover]" : ""
|
|
87
|
+
}`}
|
|
88
|
+
style={{
|
|
89
|
+
borderBottom: "1px solid var(--border)",
|
|
90
|
+
...(selectedJobId === job.id ? { borderLeft: "3px solid var(--bg-active)" } : {}),
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<div className="flex items-center justify-between">
|
|
94
|
+
<span className="text-tiny font-mono" style={{ color: "var(--text-muted)" }}>
|
|
95
|
+
#{displayNum}
|
|
96
|
+
</span>
|
|
97
|
+
<span
|
|
98
|
+
className="text-tiny px-1.5 py-0.5 rounded-sm font-bold"
|
|
99
|
+
style={{ background: colors.bg, color: colors.fg }}
|
|
100
|
+
>
|
|
101
|
+
{statusLabel(job.status)}
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
<div className="text-tiny mt-0.5" style={{ color: "var(--text-muted)" }}>
|
|
105
|
+
{job.time}
|
|
106
|
+
{job.durationMs != null && (
|
|
107
|
+
<span className="ml-1">({(job.durationMs / 1000).toFixed(1)}s)</span>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
{job.summary && (
|
|
111
|
+
<div className="text-caption mt-1 truncate" style={{ color: "var(--text-secondary)" }}>
|
|
112
|
+
{job.summary}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</button>
|
|
116
|
+
);
|
|
117
|
+
})}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|