clawport-ui 0.6.0 → 0.6.2
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 +4 -1
- package/clawport-logo.png +0 -0
- package/components/MobileSidebar.tsx +14 -29
- package/components/Sidebar.tsx +7 -17
- package/components/chat/ConversationView.tsx +4 -3
- package/components/kanban/TicketDetailPanel.tsx +3 -2
- package/docs/COMPONENTS.md +27 -0
- package/lib/conversations.ts +2 -1
- package/lib/id.ts +15 -0
- package/lib/kanban/automation.ts +3 -2
- package/lib/kanban/store.ts +2 -1
- package/package.json +2 -1
- package/public/clawport-logo.png +0 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
+
<img src="https://raw.githubusercontent.com/JohnRiceML/clawport-ui/main/clawport-logo.png" alt="ClawPort" width="160" />
|
|
4
|
+
|
|
3
5
|
# ClawPort
|
|
4
6
|
|
|
5
7
|
**A visual command centre for your AI agent team.**
|
|
@@ -14,7 +16,7 @@
|
|
|
14
16
|
|
|
15
17
|
---
|
|
16
18
|
|
|
17
|
-
ClawPort is an open-source dashboard for managing, monitoring, and talking directly to your [OpenClaw](https://openclaw.ai) AI agents. It connects to your local OpenClaw gateway and gives you an org chart, direct agent chat with vision and voice, a kanban board, cron monitoring, an activity console with live log streaming, and a memory browser -- all in one place.
|
|
19
|
+
ClawPort is an open-source dashboard for managing, monitoring, and talking directly to your [OpenClaw](https://openclaw.ai) AI agents. It connects to your local OpenClaw gateway and gives you an org chart, direct agent chat with vision and voice, a kanban board, cron monitoring, cost tracking, an activity console with live log streaming, and a memory browser -- all in one place.
|
|
18
20
|
|
|
19
21
|
No separate AI API keys needed. Everything routes through your OpenClaw gateway.
|
|
20
22
|
|
|
@@ -84,6 +86,7 @@ npm run dev
|
|
|
84
86
|
- **Chat** -- Streaming text chat, image attachments with vision, voice messages with waveform playback, file attachments, clipboard paste and drag-and-drop. Conversations persist locally.
|
|
85
87
|
- **Kanban** -- Task board for managing work across agents. Drag-and-drop cards with agent assignment and chat context.
|
|
86
88
|
- **Cron Monitor** -- Live status of all scheduled jobs. Filter by status, sort errors to top, expand for details. Auto-refreshes every 60 seconds.
|
|
89
|
+
- **Cost Dashboard** -- Token usage and cost analysis across all cron jobs. Daily cost chart, per-job breakdown, model distribution, anomaly detection, week-over-week trends, and cache savings.
|
|
87
90
|
- **Activity Console** -- Log browser for historical events plus a floating live stream widget. Click any log row to expand the raw JSON. The live stream widget persists across page navigation.
|
|
88
91
|
- **Memory Browser** -- Read team memory, long-term memory, and daily logs. Markdown rendering, JSON syntax highlighting, search, and download. Guide tab with categorized best practices.
|
|
89
92
|
- **Agent Detail** -- Full profile per agent: SOUL.md viewer, tools, hierarchy, crons, voice ID, and direct chat link.
|
|
Binary file
|
|
@@ -109,23 +109,16 @@ export function MobileSidebar({
|
|
|
109
109
|
}}
|
|
110
110
|
/>
|
|
111
111
|
) : (
|
|
112
|
-
<
|
|
112
|
+
<img
|
|
113
|
+
src="/clawport-logo.png"
|
|
114
|
+
alt=""
|
|
113
115
|
style={{
|
|
114
|
-
width: '
|
|
115
|
-
height: '
|
|
116
|
-
|
|
117
|
-
background: settings.accentColor
|
|
118
|
-
? `linear-gradient(135deg, ${settings.accentColor}, ${settings.accentColor}dd)`
|
|
119
|
-
: 'transparent',
|
|
120
|
-
display: 'flex',
|
|
121
|
-
alignItems: 'center',
|
|
122
|
-
justifyContent: 'center',
|
|
123
|
-
fontSize: '13px',
|
|
116
|
+
width: '48px',
|
|
117
|
+
height: '48px',
|
|
118
|
+
objectFit: 'contain',
|
|
124
119
|
flexShrink: 0,
|
|
125
120
|
}}
|
|
126
|
-
|
|
127
|
-
{settings.portalEmoji ?? '\ud83e\udd9e'}
|
|
128
|
-
</span>
|
|
121
|
+
/>
|
|
129
122
|
)}
|
|
130
123
|
<span
|
|
131
124
|
style={{
|
|
@@ -198,24 +191,16 @@ export function MobileSidebar({
|
|
|
198
191
|
}}
|
|
199
192
|
/>
|
|
200
193
|
) : (
|
|
201
|
-
<
|
|
194
|
+
<img
|
|
195
|
+
src="/clawport-logo.png"
|
|
196
|
+
alt=""
|
|
202
197
|
style={{
|
|
203
|
-
width: '
|
|
204
|
-
height: '
|
|
205
|
-
|
|
206
|
-
background: settings.accentColor
|
|
207
|
-
? `linear-gradient(135deg, ${settings.accentColor}, ${settings.accentColor}dd)`
|
|
208
|
-
: 'transparent',
|
|
209
|
-
boxShadow: 'var(--shadow-card)',
|
|
210
|
-
display: 'flex',
|
|
211
|
-
alignItems: 'center',
|
|
212
|
-
justifyContent: 'center',
|
|
213
|
-
fontSize: '18px',
|
|
198
|
+
width: '72px',
|
|
199
|
+
height: '72px',
|
|
200
|
+
objectFit: 'contain',
|
|
214
201
|
flexShrink: 0,
|
|
215
202
|
}}
|
|
216
|
-
|
|
217
|
-
{settings.portalEmoji ?? '\ud83e\udd9e'}
|
|
218
|
-
</div>
|
|
203
|
+
/>
|
|
219
204
|
)}
|
|
220
205
|
<div>
|
|
221
206
|
<div
|
package/components/Sidebar.tsx
CHANGED
|
@@ -51,26 +51,16 @@ export function Sidebar() {
|
|
|
51
51
|
}}
|
|
52
52
|
/>
|
|
53
53
|
) : (
|
|
54
|
-
<
|
|
54
|
+
<img
|
|
55
|
+
src="/clawport-logo.png"
|
|
56
|
+
alt=""
|
|
55
57
|
style={{
|
|
56
|
-
width: '
|
|
57
|
-
height: '
|
|
58
|
-
|
|
59
|
-
background: settings.iconBgHidden
|
|
60
|
-
? 'transparent'
|
|
61
|
-
: settings.accentColor
|
|
62
|
-
? `linear-gradient(135deg, ${settings.accentColor}, ${settings.accentColor}dd)`
|
|
63
|
-
: 'transparent',
|
|
64
|
-
boxShadow: settings.iconBgHidden ? 'none' : 'var(--shadow-card)',
|
|
65
|
-
display: 'flex',
|
|
66
|
-
alignItems: 'center',
|
|
67
|
-
justifyContent: 'center',
|
|
68
|
-
fontSize: settings.iconBgHidden ? '28px' : '18px',
|
|
58
|
+
width: '72px',
|
|
59
|
+
height: '72px',
|
|
60
|
+
objectFit: 'contain',
|
|
69
61
|
flexShrink: 0,
|
|
70
62
|
}}
|
|
71
|
-
|
|
72
|
-
{settings.portalEmoji ?? '\ud83e\udd9e'}
|
|
73
|
-
</div>
|
|
63
|
+
/>
|
|
74
64
|
)}
|
|
75
65
|
<div>
|
|
76
66
|
<div
|
|
@@ -5,6 +5,7 @@ import type { Agent } from '@/lib/types'
|
|
|
5
5
|
import type { Conversation, ConversationStore, Message, MediaAttachment } from '@/lib/conversations'
|
|
6
6
|
import { parseMedia, addMessage, updateLastMessage } from '@/lib/conversations'
|
|
7
7
|
import { buildApiContent } from '@/lib/multimodal'
|
|
8
|
+
import { generateId } from '@/lib/id'
|
|
8
9
|
import { useSettings } from '@/app/settings-provider'
|
|
9
10
|
import { FileAttachment } from './FileAttachment'
|
|
10
11
|
import { MediaPreview } from './MediaPreview'
|
|
@@ -329,14 +330,14 @@ export function ConversationView({ agent, conversation, onUpdate, onBack }: Conv
|
|
|
329
330
|
}
|
|
330
331
|
|
|
331
332
|
const userMsg: Message = {
|
|
332
|
-
id:
|
|
333
|
+
id: generateId(),
|
|
333
334
|
role: 'user',
|
|
334
335
|
content,
|
|
335
336
|
timestamp: Date.now(),
|
|
336
337
|
media: hasMedia ? mediaToSend : undefined,
|
|
337
338
|
}
|
|
338
339
|
|
|
339
|
-
const assistantMsgId =
|
|
340
|
+
const assistantMsgId = generateId()
|
|
340
341
|
const assistantMsg: Message = {
|
|
341
342
|
id: assistantMsgId,
|
|
342
343
|
role: 'assistant',
|
|
@@ -572,7 +573,7 @@ export function ConversationView({ agent, conversation, onUpdate, onBack }: Conv
|
|
|
572
573
|
[agent.id]: {
|
|
573
574
|
agentId: agent.id,
|
|
574
575
|
messages: [{
|
|
575
|
-
id:
|
|
576
|
+
id: generateId(),
|
|
576
577
|
role: 'assistant' as const,
|
|
577
578
|
content: `I'm ${agent.name}. ${agent.description} What do you need?`,
|
|
578
579
|
timestamp: Date.now(),
|
|
@@ -6,6 +6,7 @@ import type { Agent } from '@/lib/types'
|
|
|
6
6
|
import type { KanbanTicket, TicketStatus, TicketPriority } from '@/lib/kanban/types'
|
|
7
7
|
import { PRIORITY_COLORS, ROLE_LABELS, COLUMNS } from '@/lib/kanban/types'
|
|
8
8
|
import { AgentAvatar } from '@/components/AgentAvatar'
|
|
9
|
+
import { generateId } from '@/lib/id'
|
|
9
10
|
|
|
10
11
|
/* ── Chat message type (local to kanban) ─────────────── */
|
|
11
12
|
|
|
@@ -249,13 +250,13 @@ export function TicketDetailPanel({
|
|
|
249
250
|
if (!text || isStreaming || !agent) return
|
|
250
251
|
|
|
251
252
|
const userMsg: ChatMessage = {
|
|
252
|
-
id:
|
|
253
|
+
id: generateId(),
|
|
253
254
|
role: 'user',
|
|
254
255
|
content: text,
|
|
255
256
|
timestamp: Date.now(),
|
|
256
257
|
}
|
|
257
258
|
|
|
258
|
-
const assistantMsgId =
|
|
259
|
+
const assistantMsgId = generateId()
|
|
259
260
|
const assistantMsg: ChatMessage = {
|
|
260
261
|
id: assistantMsgId,
|
|
261
262
|
role: 'assistant',
|
package/docs/COMPONENTS.md
CHANGED
|
@@ -50,6 +50,8 @@ RootLayout (app/layout.tsx)
|
|
|
50
50
|
CronsPage (app/crons/page.tsx)
|
|
51
51
|
WeeklySchedule (components/crons/WeeklySchedule.tsx)
|
|
52
52
|
PipelineGraph (components/crons/PipelineGraph.tsx)
|
|
53
|
+
CostsPage (app/costs/page.tsx)
|
|
54
|
+
CostsPage (components/costs/CostsPage.tsx)
|
|
53
55
|
ActivityPage (app/activity/page.tsx)
|
|
54
56
|
LogBrowser (components/activity/LogBrowser.tsx)
|
|
55
57
|
MemoryPage (app/memory/page.tsx)
|
|
@@ -847,6 +849,31 @@ interface AgentNodeData {
|
|
|
847
849
|
|
|
848
850
|
---
|
|
849
851
|
|
|
852
|
+
## Cost Components
|
|
853
|
+
|
|
854
|
+
### CostsPage
|
|
855
|
+
|
|
856
|
+
**File:** `components/costs/CostsPage.tsx`
|
|
857
|
+
**Purpose:** Full cost dashboard with token usage analysis, daily cost chart, per-job breakdown, model distribution, anomaly detection, and week-over-week trends.
|
|
858
|
+
|
|
859
|
+
**Key state:**
|
|
860
|
+
- `summary` (useState) -- `CostSummary` fetched from `/api/costs`
|
|
861
|
+
- `crons` (useState) -- `CronJob[]` for job name resolution
|
|
862
|
+
- `hover` (useState) -- chart hover state for daily cost bars
|
|
863
|
+
|
|
864
|
+
**Implementation:**
|
|
865
|
+
- Fetches cost data from `/api/costs` (computed from cron run token usage)
|
|
866
|
+
- Summary cards: total cost, top spender, week-over-week change (with trend arrows), cache savings
|
|
867
|
+
- Daily cost bar chart (SVG) with hover tooltips
|
|
868
|
+
- Per-job cost table sorted by total cost descending
|
|
869
|
+
- Model breakdown showing token distribution percentages
|
|
870
|
+
- Anomaly alerts for runs exceeding 5x median token usage
|
|
871
|
+
- All cost computation in `lib/costs.ts` (pricing lookup, job aggregation, daily rollup, anomaly detection)
|
|
872
|
+
|
|
873
|
+
**Used by:** `app/costs/page.tsx`
|
|
874
|
+
|
|
875
|
+
---
|
|
876
|
+
|
|
850
877
|
## Activity Components
|
|
851
878
|
|
|
852
879
|
### ActivityPage
|
package/lib/conversations.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { Agent } from './types'
|
|
4
|
+
import { generateId } from './id'
|
|
4
5
|
|
|
5
6
|
export type MediaType = 'image' | 'audio' | 'file'
|
|
6
7
|
|
|
@@ -56,7 +57,7 @@ export function getOrCreateConversation(store: ConversationStore, agent: Agent):
|
|
|
56
57
|
return {
|
|
57
58
|
agentId: agent.id,
|
|
58
59
|
messages: [{
|
|
59
|
-
id:
|
|
60
|
+
id: generateId(),
|
|
60
61
|
role: 'assistant',
|
|
61
62
|
content: `I'm ${agent.name}. ${agent.description} What do you need?`,
|
|
62
63
|
timestamp: Date.now(),
|
package/lib/id.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a unique ID, with a fallback for non-secure contexts
|
|
3
|
+
* where crypto.randomUUID() is not available (e.g. HTTP, older browsers).
|
|
4
|
+
*/
|
|
5
|
+
export function generateId(): string {
|
|
6
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
7
|
+
return crypto.randomUUID()
|
|
8
|
+
}
|
|
9
|
+
// Fallback: generate a UUID v4-like string
|
|
10
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
11
|
+
const r = (Math.random() * 16) | 0
|
|
12
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
|
13
|
+
return v.toString(16)
|
|
14
|
+
})
|
|
15
|
+
}
|
package/lib/kanban/automation.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { KanbanTicket, TeamRole } from './types'
|
|
4
|
+
import { generateId } from '../id'
|
|
4
5
|
|
|
5
6
|
/* ── Role-specific work prompts ──────────────────────── */
|
|
6
7
|
|
|
@@ -159,8 +160,8 @@ export function persistWorkChat(
|
|
|
159
160
|
): void {
|
|
160
161
|
const now = Date.now()
|
|
161
162
|
const messages = [
|
|
162
|
-
{ id:
|
|
163
|
-
{ id:
|
|
163
|
+
{ id: generateId(), role: 'user' as const, content: prompt, timestamp: now },
|
|
164
|
+
{ id: generateId(), role: 'assistant' as const, content: response, timestamp: now + 1 },
|
|
164
165
|
]
|
|
165
166
|
|
|
166
167
|
fetch(`/api/kanban/chat-history/${ticketId}`, {
|
package/lib/kanban/store.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { KanbanTicket, TicketStatus, TicketPriority, WorkState } from './types'
|
|
4
|
+
import { generateId } from '../id'
|
|
4
5
|
|
|
5
6
|
export type KanbanStore = Record<string, KanbanTicket>
|
|
6
7
|
|
|
@@ -82,7 +83,7 @@ export function createTicket(
|
|
|
82
83
|
workResult?: KanbanTicket['workResult']
|
|
83
84
|
},
|
|
84
85
|
): KanbanStore {
|
|
85
|
-
const id =
|
|
86
|
+
const id = generateId()
|
|
86
87
|
const now = Date.now()
|
|
87
88
|
return {
|
|
88
89
|
...store,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawport-ui",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Open-source dashboard for managing, monitoring, and chatting with your OpenClaw AI agents.",
|
|
5
5
|
"homepage": "https://clawport.dev",
|
|
6
6
|
"repository": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"app/",
|
|
27
27
|
"bin/",
|
|
28
|
+
"clawport-logo.png",
|
|
28
29
|
"components/",
|
|
29
30
|
"docs/",
|
|
30
31
|
"lib/",
|
|
Binary file
|