clawport-ui 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/.env.example +35 -0
- package/BRANDING.md +131 -0
- package/CLAUDE.md +252 -0
- package/README.md +262 -0
- package/SETUP.md +337 -0
- package/app/agents/[id]/page.tsx +727 -0
- package/app/api/agents/route.ts +12 -0
- package/app/api/chat/[id]/route.ts +139 -0
- package/app/api/cron-runs/route.ts +13 -0
- package/app/api/crons/route.ts +12 -0
- package/app/api/kanban/chat/[id]/route.ts +119 -0
- package/app/api/kanban/chat-history/[ticketId]/route.ts +36 -0
- package/app/api/memory/route.ts +12 -0
- package/app/api/transcribe/route.ts +37 -0
- package/app/api/tts/route.ts +42 -0
- package/app/chat/[id]/page.tsx +10 -0
- package/app/chat/page.tsx +200 -0
- package/app/crons/page.tsx +870 -0
- package/app/docs/page.tsx +399 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +692 -0
- package/app/kanban/page.tsx +327 -0
- package/app/layout.tsx +45 -0
- package/app/memory/page.tsx +685 -0
- package/app/page.tsx +817 -0
- package/app/providers.tsx +37 -0
- package/app/settings/page.tsx +901 -0
- package/app/settings-provider.tsx +209 -0
- package/components/AgentAvatar.tsx +54 -0
- package/components/AgentNode.tsx +122 -0
- package/components/Breadcrumbs.tsx +126 -0
- package/components/DynamicFavicon.tsx +62 -0
- package/components/ErrorState.tsx +97 -0
- package/components/FeedView.tsx +494 -0
- package/components/GlobalSearch.tsx +571 -0
- package/components/GridView.tsx +532 -0
- package/components/ManorMap.tsx +157 -0
- package/components/MobileSidebar.tsx +251 -0
- package/components/NavLinks.tsx +271 -0
- package/components/OnboardingWizard.tsx +1067 -0
- package/components/Sidebar.tsx +115 -0
- package/components/ThemeToggle.tsx +108 -0
- package/components/chat/AgentList.tsx +537 -0
- package/components/chat/ConversationView.tsx +1047 -0
- package/components/chat/FileAttachment.tsx +140 -0
- package/components/chat/MediaPreview.tsx +111 -0
- package/components/chat/VoiceMessage.tsx +139 -0
- package/components/crons/PipelineGraph.tsx +327 -0
- package/components/crons/WeeklySchedule.tsx +630 -0
- package/components/docs/AgentsSection.tsx +209 -0
- package/components/docs/ApiReferenceSection.tsx +256 -0
- package/components/docs/ArchitectureSection.tsx +221 -0
- package/components/docs/ComponentsSection.tsx +253 -0
- package/components/docs/CronSystemSection.tsx +235 -0
- package/components/docs/DocSection.tsx +346 -0
- package/components/docs/GettingStartedSection.tsx +169 -0
- package/components/docs/ThemingSection.tsx +257 -0
- package/components/docs/TroubleshootingSection.tsx +200 -0
- package/components/kanban/AgentPicker.tsx +321 -0
- package/components/kanban/CreateTicketModal.tsx +333 -0
- package/components/kanban/KanbanBoard.tsx +70 -0
- package/components/kanban/KanbanColumn.tsx +166 -0
- package/components/kanban/TicketCard.tsx +245 -0
- package/components/kanban/TicketDetailPanel.tsx +850 -0
- package/components/ui/badge.tsx +48 -0
- package/components/ui/button.tsx +64 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/skeleton.tsx +27 -0
- package/components/ui/tabs.tsx +91 -0
- package/components/ui/tooltip.tsx +57 -0
- package/components.json +23 -0
- package/docs/API.md +648 -0
- package/docs/COMPONENTS.md +1059 -0
- package/docs/THEMING.md +795 -0
- package/lib/agents-registry.ts +35 -0
- package/lib/agents.json +282 -0
- package/lib/agents.test.ts +367 -0
- package/lib/agents.ts +32 -0
- package/lib/anthropic.test.ts +422 -0
- package/lib/anthropic.ts +220 -0
- package/lib/api-error.ts +16 -0
- package/lib/audio-recorder.test.ts +72 -0
- package/lib/audio-recorder.ts +169 -0
- package/lib/conversations.test.ts +331 -0
- package/lib/conversations.ts +117 -0
- package/lib/cron-pipelines.test.ts +69 -0
- package/lib/cron-pipelines.ts +58 -0
- package/lib/cron-runs.test.ts +118 -0
- package/lib/cron-runs.ts +67 -0
- package/lib/cron-utils.test.ts +222 -0
- package/lib/cron-utils.ts +160 -0
- package/lib/crons.test.ts +502 -0
- package/lib/crons.ts +114 -0
- package/lib/env.test.ts +44 -0
- package/lib/env.ts +14 -0
- package/lib/kanban/automation.test.ts +245 -0
- package/lib/kanban/automation.ts +143 -0
- package/lib/kanban/chat-store.test.ts +149 -0
- package/lib/kanban/chat-store.ts +81 -0
- package/lib/kanban/store.test.ts +238 -0
- package/lib/kanban/store.ts +98 -0
- package/lib/kanban/types.ts +50 -0
- package/lib/kanban/useAgentWork.ts +78 -0
- package/lib/memory.ts +45 -0
- package/lib/multimodal.test.ts +219 -0
- package/lib/multimodal.ts +68 -0
- package/lib/pipeline.integration.test.ts +343 -0
- package/lib/sanitize.ts +194 -0
- package/lib/settings.test.ts +137 -0
- package/lib/settings.ts +94 -0
- package/lib/styles.ts +24 -0
- package/lib/themes.ts +9 -0
- package/lib/transcribe.test.ts +141 -0
- package/lib/transcribe.ts +111 -0
- package/lib/types.ts +66 -0
- package/lib/utils.ts +6 -0
- package/lib/validation.test.ts +132 -0
- package/lib/validation.ts +80 -0
- package/next.config.ts +7 -0
- package/package.json +56 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/setup.mjs +215 -0
- package/tsconfig.json +34 -0
- package/vitest.config.ts +17 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcribe audio via the server-side Whisper API.
|
|
3
|
+
* Returns the transcript text, or null if transcription failed.
|
|
4
|
+
*/
|
|
5
|
+
export async function transcribeViaApi(audioBlob: Blob): Promise<string | null> {
|
|
6
|
+
try {
|
|
7
|
+
const form = new FormData()
|
|
8
|
+
form.append('audio', audioBlob, 'voice.webm')
|
|
9
|
+
const res = await fetch('/api/transcribe', { method: 'POST', body: form })
|
|
10
|
+
if (!res.ok) return null
|
|
11
|
+
const data = await res.json()
|
|
12
|
+
return data.text || null
|
|
13
|
+
} catch {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Transcribe audio using the browser's Web Speech API (SpeechRecognition).
|
|
20
|
+
* This is a fallback that works without any server — uses the browser's
|
|
21
|
+
* built-in speech recognition engine.
|
|
22
|
+
*
|
|
23
|
+
* Note: This re-plays the audio through an Audio element and listens via
|
|
24
|
+
* SpeechRecognition. It requires the browser to support both APIs.
|
|
25
|
+
* Returns null if the browser doesn't support SpeechRecognition.
|
|
26
|
+
*/
|
|
27
|
+
export async function transcribeViaBrowser(audioBlob: Blob): Promise<string | null> {
|
|
28
|
+
const SpeechRecognition = (
|
|
29
|
+
(globalThis as Record<string, unknown>).SpeechRecognition ||
|
|
30
|
+
(globalThis as Record<string, unknown>).webkitSpeechRecognition
|
|
31
|
+
) as (new () => SpeechRecognitionInstance) | undefined
|
|
32
|
+
|
|
33
|
+
if (!SpeechRecognition) return null
|
|
34
|
+
|
|
35
|
+
return new Promise<string | null>((resolve) => {
|
|
36
|
+
const recognition = new SpeechRecognition()
|
|
37
|
+
recognition.continuous = false
|
|
38
|
+
recognition.interimResults = false
|
|
39
|
+
recognition.lang = 'en-US'
|
|
40
|
+
|
|
41
|
+
let resolved = false
|
|
42
|
+
|
|
43
|
+
recognition.onresult = (event: SpeechRecognitionEvent) => {
|
|
44
|
+
if (resolved) return
|
|
45
|
+
resolved = true
|
|
46
|
+
const transcript = event.results[0]?.[0]?.transcript || ''
|
|
47
|
+
resolve(transcript || null)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
recognition.onerror = () => {
|
|
51
|
+
if (!resolved) { resolved = true; resolve(null) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
recognition.onnomatch = () => {
|
|
55
|
+
if (!resolved) { resolved = true; resolve(null) }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
recognition.onend = () => {
|
|
59
|
+
if (!resolved) { resolved = true; resolve(null) }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Play the audio so the mic picks it up — but SpeechRecognition
|
|
63
|
+
// uses the system mic, not internal audio. Instead, we start recognition
|
|
64
|
+
// alongside playback and hope the user's environment allows it.
|
|
65
|
+
// In practice this is best-effort.
|
|
66
|
+
const url = URL.createObjectURL(audioBlob)
|
|
67
|
+
const audio = new Audio(url)
|
|
68
|
+
|
|
69
|
+
recognition.start()
|
|
70
|
+
audio.play().catch(() => {})
|
|
71
|
+
|
|
72
|
+
// Timeout after 10 seconds
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
if (!resolved) {
|
|
75
|
+
resolved = true
|
|
76
|
+
recognition.stop()
|
|
77
|
+
resolve(null)
|
|
78
|
+
}
|
|
79
|
+
URL.revokeObjectURL(url)
|
|
80
|
+
}, 10000)
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Transcribe audio with automatic fallback:
|
|
86
|
+
* 1. Try server-side Whisper API
|
|
87
|
+
* 2. Return result or null
|
|
88
|
+
*/
|
|
89
|
+
export async function transcribe(audioBlob: Blob): Promise<{ text: string | null; source: 'whisper' | 'failed' }> {
|
|
90
|
+
const whisperResult = await transcribeViaApi(audioBlob)
|
|
91
|
+
if (whisperResult) return { text: whisperResult, source: 'whisper' }
|
|
92
|
+
|
|
93
|
+
return { text: null, source: 'failed' }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Types for the Web Speech API (not in all TS libs)
|
|
97
|
+
interface SpeechRecognitionEvent {
|
|
98
|
+
results: { [index: number]: { [index: number]: { transcript: string } } }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface SpeechRecognitionInstance {
|
|
102
|
+
continuous: boolean
|
|
103
|
+
interimResults: boolean
|
|
104
|
+
lang: string
|
|
105
|
+
onresult: ((event: SpeechRecognitionEvent) => void) | null
|
|
106
|
+
onerror: (() => void) | null
|
|
107
|
+
onnomatch: (() => void) | null
|
|
108
|
+
onend: (() => void) | null
|
|
109
|
+
start: () => void
|
|
110
|
+
stop: () => void
|
|
111
|
+
}
|
package/lib/types.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Shared types for ClawPort
|
|
2
|
+
|
|
3
|
+
export interface Agent {
|
|
4
|
+
id: string // slug, e.g. "vera"
|
|
5
|
+
name: string // display name, e.g. "VERA"
|
|
6
|
+
title: string // role title, e.g. "Chief Strategy Officer"
|
|
7
|
+
reportsTo: string | null // parent agent id (null for root orchestrator)
|
|
8
|
+
directReports: string[] // child agent ids
|
|
9
|
+
soulPath: string | null // absolute path to SOUL.md
|
|
10
|
+
soul: string | null // full SOUL.md content
|
|
11
|
+
voiceId: string | null // ElevenLabs voice ID
|
|
12
|
+
color: string // hex color for node
|
|
13
|
+
emoji: string // emoji identifier
|
|
14
|
+
tools: string[] // list of tools this agent has access to
|
|
15
|
+
crons: CronJob[] // associated cron jobs
|
|
16
|
+
memoryPath: string | null
|
|
17
|
+
description: string // one-liner description
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CronDelivery {
|
|
21
|
+
mode: string
|
|
22
|
+
channel: string
|
|
23
|
+
to: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CronRun {
|
|
27
|
+
ts: number
|
|
28
|
+
jobId: string
|
|
29
|
+
status: 'ok' | 'error'
|
|
30
|
+
summary: string | null
|
|
31
|
+
error: string | null
|
|
32
|
+
durationMs: number
|
|
33
|
+
deliveryStatus: string | null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CronJob {
|
|
37
|
+
id: string
|
|
38
|
+
name: string
|
|
39
|
+
schedule: string // raw cron expression
|
|
40
|
+
scheduleDescription: string // human-readable (e.g., "Daily at 8 AM")
|
|
41
|
+
timezone: string | null // extracted from schedule object if present
|
|
42
|
+
status: 'ok' | 'error' | 'idle'
|
|
43
|
+
lastRun: string | null
|
|
44
|
+
nextRun: string | null
|
|
45
|
+
lastError: string | null
|
|
46
|
+
agentId: string | null // which agent this belongs to (matched by name)
|
|
47
|
+
description: string | null
|
|
48
|
+
enabled: boolean
|
|
49
|
+
delivery: CronDelivery | null
|
|
50
|
+
lastDurationMs: number | null
|
|
51
|
+
consecutiveErrors: number
|
|
52
|
+
lastDeliveryStatus: string | null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ChatMessage {
|
|
56
|
+
role: 'user' | 'assistant'
|
|
57
|
+
content: string
|
|
58
|
+
timestamp: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface MemoryFile {
|
|
62
|
+
label: string
|
|
63
|
+
path: string
|
|
64
|
+
content: string
|
|
65
|
+
lastModified: string
|
|
66
|
+
}
|
package/lib/utils.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { validateMessages, validateChatMessages } from './validation'
|
|
3
|
+
|
|
4
|
+
// --- validateMessages ---
|
|
5
|
+
|
|
6
|
+
describe('validateMessages', () => {
|
|
7
|
+
it('accepts plain text messages', () => {
|
|
8
|
+
const result = validateMessages([
|
|
9
|
+
{ role: 'user', content: 'hello' },
|
|
10
|
+
{ role: 'assistant', content: 'hi there' },
|
|
11
|
+
])
|
|
12
|
+
expect(result).toHaveLength(2)
|
|
13
|
+
expect(result[0].content).toBe('hello')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('accepts multimodal content array with text parts', () => {
|
|
17
|
+
const result = validateMessages([
|
|
18
|
+
{ role: 'user', content: [{ type: 'text', text: 'describe this' }] },
|
|
19
|
+
])
|
|
20
|
+
expect(result).toHaveLength(1)
|
|
21
|
+
expect(Array.isArray(result[0].content)).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('accepts image_url content parts', () => {
|
|
25
|
+
const result = validateMessages([
|
|
26
|
+
{
|
|
27
|
+
role: 'user',
|
|
28
|
+
content: [
|
|
29
|
+
{ type: 'text', text: 'what is this?' },
|
|
30
|
+
{ type: 'image_url', image_url: { url: 'data:image/png;base64,abc123' } },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
])
|
|
34
|
+
expect(result).toHaveLength(1)
|
|
35
|
+
const parts = result[0].content as Array<{ type: string }>
|
|
36
|
+
expect(parts).toHaveLength(2)
|
|
37
|
+
expect(parts[1].type).toBe('image_url')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('accepts system role messages', () => {
|
|
41
|
+
const result = validateMessages([
|
|
42
|
+
{ role: 'system', content: 'You are helpful.' },
|
|
43
|
+
])
|
|
44
|
+
expect(result[0].role).toBe('system')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('throws on non-array input', () => {
|
|
48
|
+
expect(() => validateMessages('not an array')).toThrow('must be an array')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('throws on invalid role', () => {
|
|
52
|
+
expect(() => validateMessages([{ role: 'admin', content: 'hi' }])).toThrow('role')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('throws on missing content', () => {
|
|
56
|
+
expect(() => validateMessages([{ role: 'user' }])).toThrow('content')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('throws on number content', () => {
|
|
60
|
+
expect(() => validateMessages([{ role: 'user', content: 42 }])).toThrow('content')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('throws on empty content array', () => {
|
|
64
|
+
expect(() => validateMessages([{ role: 'user', content: [] }])).toThrow('content')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('throws on malformed content part (missing type)', () => {
|
|
68
|
+
expect(() => validateMessages([
|
|
69
|
+
{ role: 'user', content: [{ text: 'no type field' }] },
|
|
70
|
+
])).toThrow('content')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('throws on invalid content part type', () => {
|
|
74
|
+
expect(() => validateMessages([
|
|
75
|
+
{ role: 'user', content: [{ type: 'video', url: 'x' }] },
|
|
76
|
+
])).toThrow('content')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('throws on image_url part without url string', () => {
|
|
80
|
+
expect(() => validateMessages([
|
|
81
|
+
{ role: 'user', content: [{ type: 'image_url', image_url: {} }] },
|
|
82
|
+
])).toThrow('content')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('throws on text part without text string', () => {
|
|
86
|
+
expect(() => validateMessages([
|
|
87
|
+
{ role: 'user', content: [{ type: 'text', text: 123 }] },
|
|
88
|
+
])).toThrow('content')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('throws on non-object message', () => {
|
|
92
|
+
expect(() => validateMessages([null])).toThrow('must be an object')
|
|
93
|
+
expect(() => validateMessages(['string'])).toThrow('must be an object')
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// --- validateChatMessages (legacy wrapper) ---
|
|
98
|
+
|
|
99
|
+
describe('validateChatMessages', () => {
|
|
100
|
+
it('returns ok:true with validated messages', () => {
|
|
101
|
+
const result = validateChatMessages({
|
|
102
|
+
messages: [{ role: 'user', content: 'test' }],
|
|
103
|
+
})
|
|
104
|
+
expect(result.ok).toBe(true)
|
|
105
|
+
if (result.ok) {
|
|
106
|
+
expect(result.messages).toHaveLength(1)
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('returns ok:false for non-object body', () => {
|
|
111
|
+
const result = validateChatMessages(null)
|
|
112
|
+
expect(result.ok).toBe(false)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('returns ok:false for invalid messages', () => {
|
|
116
|
+
const result = validateChatMessages({ messages: 'not an array' })
|
|
117
|
+
expect(result.ok).toBe(false)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('accepts multimodal messages through legacy wrapper', () => {
|
|
121
|
+
const result = validateChatMessages({
|
|
122
|
+
messages: [{
|
|
123
|
+
role: 'user',
|
|
124
|
+
content: [
|
|
125
|
+
{ type: 'text', text: 'check this' },
|
|
126
|
+
{ type: 'image_url', image_url: { url: 'data:image/png;base64,x' } },
|
|
127
|
+
],
|
|
128
|
+
}],
|
|
129
|
+
})
|
|
130
|
+
expect(result.ok).toBe(true)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Validation for chat API messages — supports both plain text and multimodal (vision) content
|
|
2
|
+
|
|
3
|
+
export type TextContentPart = { type: 'text'; text: string }
|
|
4
|
+
export type ImageContentPart = { type: 'image_url'; image_url: { url: string } }
|
|
5
|
+
export type ContentPart = TextContentPart | ImageContentPart
|
|
6
|
+
|
|
7
|
+
export type MessageContent = string | ContentPart[]
|
|
8
|
+
|
|
9
|
+
export interface ApiMessage {
|
|
10
|
+
role: 'user' | 'assistant' | 'system'
|
|
11
|
+
content: MessageContent
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isValidContentPart(part: unknown): part is ContentPart {
|
|
15
|
+
if (typeof part !== 'object' || part === null) return false
|
|
16
|
+
const p = part as Record<string, unknown>
|
|
17
|
+
|
|
18
|
+
if (p.type === 'text') {
|
|
19
|
+
return typeof p.text === 'string'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (p.type === 'image_url') {
|
|
23
|
+
const imgUrl = p.image_url
|
|
24
|
+
if (typeof imgUrl !== 'object' || imgUrl === null) return false
|
|
25
|
+
return typeof (imgUrl as Record<string, unknown>).url === 'string'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isValidContent(content: unknown): content is MessageContent {
|
|
32
|
+
if (typeof content === 'string') return true
|
|
33
|
+
|
|
34
|
+
if (Array.isArray(content)) {
|
|
35
|
+
return content.length > 0 && content.every(isValidContentPart)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validateMessages(messages: unknown): ApiMessage[] {
|
|
42
|
+
if (!Array.isArray(messages)) {
|
|
43
|
+
throw new Error('messages must be an array')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return messages.map((msg, i) => {
|
|
47
|
+
if (typeof msg !== 'object' || msg === null) {
|
|
48
|
+
throw new Error(`messages[${i}] must be an object`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const m = msg as Record<string, unknown>
|
|
52
|
+
|
|
53
|
+
if (typeof m.role !== 'string' || !['user', 'assistant', 'system'].includes(m.role)) {
|
|
54
|
+
throw new Error(`messages[${i}].role must be "user", "assistant", or "system"`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!isValidContent(m.content)) {
|
|
58
|
+
throw new Error(`messages[${i}].content must be a string or array of content parts`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { role: m.role as ApiMessage['role'], content: m.content as MessageContent }
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Legacy export for backward compatibility with existing test
|
|
66
|
+
export type ValidatedChatMessage = ApiMessage
|
|
67
|
+
export type ValidationResult = { ok: true; messages: ApiMessage[] } | { ok: false; error: string }
|
|
68
|
+
|
|
69
|
+
export function validateChatMessages(body: unknown): ValidationResult {
|
|
70
|
+
if (body === null || typeof body !== 'object') {
|
|
71
|
+
return { ok: false, error: 'Request body must be a JSON object.' }
|
|
72
|
+
}
|
|
73
|
+
const { messages } = body as Record<string, unknown>
|
|
74
|
+
try {
|
|
75
|
+
const validated = validateMessages(messages)
|
|
76
|
+
return { ok: true, messages: validated }
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return { ok: false, error: err instanceof Error ? err.message : 'Invalid messages' }
|
|
79
|
+
}
|
|
80
|
+
}
|
package/next.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clawport-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Open-source dashboard for managing, monitoring, and chatting with your OpenClaw AI agents.",
|
|
5
|
+
"homepage": "https://clawport.dev",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/openclaw/clawport.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/openclaw/clawport/issues"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"openclaw",
|
|
15
|
+
"ai-agents",
|
|
16
|
+
"dashboard",
|
|
17
|
+
"clawport",
|
|
18
|
+
"agent-management",
|
|
19
|
+
"next.js"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "next dev",
|
|
24
|
+
"build": "next build",
|
|
25
|
+
"start": "next start",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"setup": "node scripts/setup.mjs"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@xyflow/react": "^12.10.1",
|
|
31
|
+
"class-variance-authority": "^0.7.1",
|
|
32
|
+
"clsx": "^2.1.1",
|
|
33
|
+
"lucide-react": "^0.575.0",
|
|
34
|
+
"next": "16.1.6",
|
|
35
|
+
"openai": "^6.25.0",
|
|
36
|
+
"radix-ui": "^1.4.3",
|
|
37
|
+
"react": "19.2.3",
|
|
38
|
+
"react-dom": "19.2.3",
|
|
39
|
+
"tailwind-merge": "^3.5.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@tailwindcss/postcss": "^4",
|
|
43
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
44
|
+
"@testing-library/react": "^16.3.2",
|
|
45
|
+
"@types/node": "^20",
|
|
46
|
+
"@types/react": "^19",
|
|
47
|
+
"@types/react-dom": "^19",
|
|
48
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
49
|
+
"jsdom": "^28.1.0",
|
|
50
|
+
"shadcn": "^3.8.5",
|
|
51
|
+
"tailwindcss": "^4",
|
|
52
|
+
"tw-animate-css": "^1.4.0",
|
|
53
|
+
"typescript": "^5",
|
|
54
|
+
"vitest": "^4.0.18"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/public/file.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
package/public/globe.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
package/public/next.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|