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 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
- <span
112
+ <img
113
+ src="/clawport-logo.png"
114
+ alt=""
113
115
  style={{
114
- width: '24px',
115
- height: '24px',
116
- borderRadius: '6px',
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
- <div
194
+ <img
195
+ src="/clawport-logo.png"
196
+ alt=""
202
197
  style={{
203
- width: '36px',
204
- height: '36px',
205
- borderRadius: '10px',
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
@@ -51,26 +51,16 @@ export function Sidebar() {
51
51
  }}
52
52
  />
53
53
  ) : (
54
- <div
54
+ <img
55
+ src="/clawport-logo.png"
56
+ alt=""
55
57
  style={{
56
- width: '36px',
57
- height: '36px',
58
- borderRadius: '10px',
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: crypto.randomUUID(),
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 = crypto.randomUUID()
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: crypto.randomUUID(),
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: crypto.randomUUID(),
253
+ id: generateId(),
253
254
  role: 'user',
254
255
  content: text,
255
256
  timestamp: Date.now(),
256
257
  }
257
258
 
258
- const assistantMsgId = crypto.randomUUID()
259
+ const assistantMsgId = generateId()
259
260
  const assistantMsg: ChatMessage = {
260
261
  id: assistantMsgId,
261
262
  role: 'assistant',
@@ -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
@@ -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: crypto.randomUUID(),
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
+ }
@@ -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: crypto.randomUUID(), role: 'user' as const, content: prompt, timestamp: now },
163
- { id: crypto.randomUUID(), role: 'assistant' as const, content: response, timestamp: now + 1 },
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}`, {
@@ -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 = crypto.randomUUID()
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.0",
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