mirror of
https://github.com/crabwise-ai/crabwalk.git
synced 2026-08-14 00:57:52 +00:00
Refactor ActionGraph and related components for improved type safety and code clarity. Removed unnecessary peer dependency in package-lock.json. Updated session and action node components to use explicit types and fixed minor styling issues. Enhanced local collection options in clawdbot integration and demo-db. Adjusted live query handling in demo and monitor routes for better data management.
This commit is contained in:
Generated
-1
@@ -2154,7 +2154,6 @@
|
||||
"integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
|
||||
@@ -24,9 +24,10 @@ interface ActionGraphProps {
|
||||
onSessionSelect: (key: string | null) => void
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const nodeTypes: NodeTypes = {
|
||||
session: SessionNode,
|
||||
action: ActionNode,
|
||||
session: SessionNode as any,
|
||||
action: ActionNode as any,
|
||||
}
|
||||
|
||||
export function ActionGraph({
|
||||
@@ -55,7 +56,7 @@ export function ActionGraph({
|
||||
id: `session-${session.key}`,
|
||||
type: 'session',
|
||||
position: { x: 0, y: 0 },
|
||||
data: session,
|
||||
data: session as unknown as Record<string, unknown>,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -65,7 +66,7 @@ export function ActionGraph({
|
||||
id: `action-${action.id}`,
|
||||
type: 'action',
|
||||
position: { x: 0, y: 0 },
|
||||
data: action,
|
||||
data: action as unknown as Record<string, unknown>,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -87,8 +88,8 @@ export function ActionGraph({
|
||||
// Connect session to first action of each run
|
||||
for (const [runId, runActs] of runActions) {
|
||||
const sorted = [...runActs].sort((a, b) => a.seq - b.seq)
|
||||
if (sorted.length > 0) {
|
||||
const first = sorted[0]
|
||||
const first = sorted[0]
|
||||
if (first) {
|
||||
const sessionId = `session-${first.sessionKey}`
|
||||
edges.push({
|
||||
id: `e-session-${runId}`,
|
||||
@@ -98,20 +99,20 @@ export function ActionGraph({
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
style: { stroke: '#6b7280' },
|
||||
})
|
||||
}
|
||||
|
||||
// Connect actions in sequence
|
||||
for (let i = 1; i < sorted.length; i++) {
|
||||
const prev = sorted[i - 1]
|
||||
const curr = sorted[i]
|
||||
edges.push({
|
||||
id: `e-${prev.id}-${curr.id}`,
|
||||
source: `action-${prev.id}`,
|
||||
target: `action-${curr.id}`,
|
||||
animated: curr.type === 'delta',
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
style: { stroke: '#6b7280' },
|
||||
})
|
||||
// Connect actions in sequence
|
||||
for (let i = 1; i < sorted.length; i++) {
|
||||
const prev = sorted[i - 1]!
|
||||
const curr = sorted[i]!
|
||||
edges.push({
|
||||
id: `e-${prev.id}-${curr.id}`,
|
||||
source: `action-${prev.id}`,
|
||||
target: `action-${curr.id}`,
|
||||
animated: curr.type === 'delta',
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
style: { stroke: '#6b7280' },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +143,7 @@ export function ActionGraph({
|
||||
const onNodeClick = useCallback(
|
||||
(_: React.MouseEvent, node: Node) => {
|
||||
if (node.type === 'session') {
|
||||
const sessionKey = (node.data as MonitorSession).key
|
||||
const sessionKey = (node.data as unknown as MonitorSession).key
|
||||
onSessionSelect(selectedSession === sessionKey ? null : sessionKey)
|
||||
}
|
||||
},
|
||||
@@ -165,9 +166,9 @@ export function ActionGraph({
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background color="#374151" gap={20} />
|
||||
<Controls className="!bg-gray-800 !border-gray-700 !rounded-lg" />
|
||||
<Controls className="bg-gray-800! !border-gray-700! rounded-lg!" />
|
||||
<MiniMap
|
||||
className="!bg-gray-800 !border-gray-700 !rounded-lg"
|
||||
className="bg-gray-800! border-gray-700! rounded-lg!"
|
||||
nodeColor={(node) => {
|
||||
if (node.type === 'session') return '#06b6d4'
|
||||
return '#6b7280'
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import { memo, useState } from 'react'
|
||||
import { Handle, Position, type NodeProps } from '@xyflow/react'
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Loader2, CheckCircle, XCircle, Wrench, MessageSquare } from 'lucide-react'
|
||||
import type { MonitorAction } from '~/integrations/clawdbot'
|
||||
|
||||
type ActionNodeData = MonitorAction
|
||||
interface ActionNodeProps {
|
||||
data: MonitorAction
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
const typeConfig = {
|
||||
const typeConfig: Record<
|
||||
MonitorAction['type'],
|
||||
{
|
||||
icon: typeof Loader2
|
||||
color: string
|
||||
iconColor: string
|
||||
animate: boolean
|
||||
}
|
||||
> = {
|
||||
delta: {
|
||||
icon: Loader2,
|
||||
color: 'border-blue-500 bg-blue-500/10',
|
||||
@@ -48,7 +59,7 @@ const typeConfig = {
|
||||
export const ActionNode = memo(function ActionNode({
|
||||
data,
|
||||
selected,
|
||||
}: NodeProps<ActionNodeData>) {
|
||||
}: ActionNodeProps) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const config = typeConfig[data.type]
|
||||
const Icon = config.icon
|
||||
@@ -71,7 +82,7 @@ export const ActionNode = memo(function ActionNode({
|
||||
${selected ? 'ring-2 ring-white/50' : ''}
|
||||
`}
|
||||
>
|
||||
<Handle type="target" position={Position.Top} className="!bg-gray-500 !w-2 !h-2" />
|
||||
<Handle type="target" position={Position.Top} className="bg-gray-500! w-2! h-2!" />
|
||||
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Icon
|
||||
@@ -98,13 +109,13 @@ export const ActionNode = memo(function ActionNode({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{expanded && data.toolArgs && (
|
||||
{expanded && data.toolArgs != null && (
|
||||
<pre className="mt-2 text-[10px] text-gray-400 bg-black/30 p-1 rounded overflow-auto max-h-32">
|
||||
{JSON.stringify(data.toolArgs, null, 2)}
|
||||
{JSON.stringify(data.toolArgs, null, 2) as string}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
<Handle type="source" position={Position.Bottom} className="!bg-gray-500 !w-2 !h-2" />
|
||||
<Handle type="source" position={Position.Bottom} className="bg-gray-500! w-2! h-2!" />
|
||||
</motion.div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { Search, Filter } from 'lucide-react'
|
||||
import { Search } from 'lucide-react'
|
||||
import { StatusIndicator } from './StatusIndicator'
|
||||
import type { MonitorSession } from '~/integrations/clawdbot'
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { memo } from 'react'
|
||||
import { Handle, Position, type NodeProps } from '@xyflow/react'
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { MessageSquare, Users, User } from 'lucide-react'
|
||||
import { Users, User } from 'lucide-react'
|
||||
import { StatusIndicator } from './StatusIndicator'
|
||||
import type { MonitorSession } from '~/integrations/clawdbot'
|
||||
|
||||
type SessionNodeData = MonitorSession
|
||||
interface SessionNodeProps {
|
||||
data: MonitorSession
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
const platformIcons: Record<string, string> = {
|
||||
whatsapp: '💬',
|
||||
@@ -17,7 +20,7 @@ const platformIcons: Record<string, string> = {
|
||||
export const SessionNode = memo(function SessionNode({
|
||||
data,
|
||||
selected,
|
||||
}: NodeProps<SessionNodeData>) {
|
||||
}: SessionNodeProps) {
|
||||
const platformIcon = platformIcons[data.platform] ?? '📱'
|
||||
|
||||
return (
|
||||
@@ -31,7 +34,7 @@ export const SessionNode = memo(function SessionNode({
|
||||
${data.status === 'thinking' ? 'border-yellow-500' : ''}
|
||||
`}
|
||||
>
|
||||
<Handle type="target" position={Position.Top} className="!bg-gray-500" />
|
||||
<Handle type="target" position={Position.Top} className="bg-gray-500!" />
|
||||
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">{platformIcon}</span>
|
||||
@@ -52,7 +55,7 @@ export const SessionNode = memo(function SessionNode({
|
||||
{data.agentId}
|
||||
</div>
|
||||
|
||||
<Handle type="source" position={Position.Bottom} className="!bg-gray-500" />
|
||||
<Handle type="source" position={Position.Bottom} className="bg-gray-500!" />
|
||||
</motion.div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -67,7 +67,7 @@ export class ClawdbotClient {
|
||||
private handleMessage(
|
||||
msg: GatewayFrame | HelloOk,
|
||||
connectResolve?: (v: HelloOk) => void,
|
||||
connectReject?: (e: Error) => void
|
||||
_connectReject?: (e: Error) => void
|
||||
) {
|
||||
if ('type' in msg) {
|
||||
switch (msg.type) {
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import { createCollection } from '@tanstack/db'
|
||||
import { createCollection, localOnlyCollectionOptions } from '@tanstack/db'
|
||||
import type { MonitorSession, MonitorAction } from './protocol'
|
||||
|
||||
export const sessionsCollection = createCollection<MonitorSession>({
|
||||
id: 'clawdbot-sessions',
|
||||
primaryKey: 'key',
|
||||
})
|
||||
export const sessionsCollection = createCollection(
|
||||
localOnlyCollectionOptions<MonitorSession>({
|
||||
id: 'clawdbot-sessions',
|
||||
getKey: (item) => item.key,
|
||||
})
|
||||
)
|
||||
|
||||
export const actionsCollection = createCollection<MonitorAction>({
|
||||
id: 'clawdbot-actions',
|
||||
primaryKey: 'id',
|
||||
})
|
||||
export const actionsCollection = createCollection(
|
||||
localOnlyCollectionOptions<MonitorAction>({
|
||||
id: 'clawdbot-actions',
|
||||
getKey: (item) => item.id,
|
||||
})
|
||||
)
|
||||
|
||||
// Helper to update or insert session
|
||||
export function upsertSession(session: MonitorSession) {
|
||||
const existing = sessionsCollection.state.get(session.key)
|
||||
if (existing) {
|
||||
sessionsCollection.update(session.key, session)
|
||||
sessionsCollection.update(session.key, (draft) => {
|
||||
Object.assign(draft, session)
|
||||
})
|
||||
} else {
|
||||
sessionsCollection.insert(session)
|
||||
}
|
||||
@@ -36,9 +42,9 @@ export function updateSessionStatus(
|
||||
) {
|
||||
const session = sessionsCollection.state.get(key)
|
||||
if (session) {
|
||||
sessionsCollection.update(key, {
|
||||
status,
|
||||
lastActivityAt: Date.now(),
|
||||
sessionsCollection.update(key, (draft) => {
|
||||
draft.status = status
|
||||
draft.lastActivityAt = Date.now()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ export const dbDevtoolsPlugin = {
|
||||
}
|
||||
|
||||
function DbInspector() {
|
||||
const todos = useLiveQuery(todosCollection)
|
||||
const todosQuery = useLiveQuery(todosCollection)
|
||||
const todos = todosQuery.data ?? []
|
||||
return (
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-1">todos ({todos.length})</div>
|
||||
|
||||
@@ -4,8 +4,6 @@ import superjson from 'superjson'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
getClawdbotClient,
|
||||
isChatEvent,
|
||||
isAgentEvent,
|
||||
parseEventFrame,
|
||||
sessionInfoToMonitor,
|
||||
type MonitorSession,
|
||||
|
||||
+7
-5
@@ -1,4 +1,4 @@
|
||||
import { createCollection } from '@tanstack/db'
|
||||
import { createCollection, localOnlyCollectionOptions } from '@tanstack/db'
|
||||
|
||||
export interface Todo {
|
||||
id: string
|
||||
@@ -7,7 +7,9 @@ export interface Todo {
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
export const todosCollection = createCollection<Todo>({
|
||||
id: 'todos',
|
||||
primaryKey: 'id',
|
||||
})
|
||||
export const todosCollection = createCollection(
|
||||
localOnlyCollectionOptions<Todo>({
|
||||
id: 'todos',
|
||||
getKey: (item) => item.id,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -12,7 +12,6 @@ async function handler({ request }: { request: Request }) {
|
||||
}
|
||||
|
||||
export const Route = createFileRoute('/api/trpc/$')({
|
||||
// @ts-expect-error server property not in route types yet
|
||||
server: {
|
||||
handlers: {
|
||||
GET: handler,
|
||||
|
||||
@@ -9,7 +9,8 @@ export const Route = createFileRoute('/demo/db')({
|
||||
|
||||
function DbDemo() {
|
||||
const [newTodo, setNewTodo] = useState('')
|
||||
const todos = useLiveQuery(todosCollection)
|
||||
const todosQuery = useLiveQuery(todosCollection)
|
||||
const todos = todosQuery.data ?? []
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -40,7 +41,9 @@ function DbDemo() {
|
||||
})
|
||||
|
||||
tx.mutate(() => {
|
||||
todosCollection.update(todo.id, { completed: !todo.completed })
|
||||
todosCollection.update(todo.id, (draft) => {
|
||||
draft.completed = !draft.completed
|
||||
})
|
||||
})
|
||||
|
||||
await tx.commit()
|
||||
|
||||
@@ -7,7 +7,7 @@ export const Route = createFileRoute('/')({
|
||||
|
||||
function Home() {
|
||||
return (
|
||||
<div className="min-h-[calc(100vh-72px)] bg-gradient-to-br from-gray-900 to-gray-800 flex items-center justify-center">
|
||||
<div className="min-h-[calc(100vh-72px)] bg-linear-to-br from-gray-900 to-gray-800 flex items-center justify-center">
|
||||
<div className="text-center px-4">
|
||||
<Activity size={64} className="mx-auto mb-6 text-cyan-400" />
|
||||
<h1 className="text-5xl font-bold text-white mb-4">Crabwalk</h1>
|
||||
|
||||
@@ -7,10 +7,7 @@ import {
|
||||
sessionsCollection,
|
||||
actionsCollection,
|
||||
upsertSession,
|
||||
addAction,
|
||||
clearCollections,
|
||||
type MonitorSession,
|
||||
type MonitorAction,
|
||||
} from '~/integrations/clawdbot'
|
||||
import {
|
||||
ActionGraph,
|
||||
@@ -31,8 +28,11 @@ function MonitorPage() {
|
||||
const [selectedSession, setSelectedSession] = useState<string | null>(null)
|
||||
|
||||
// Live queries from TanStack DB collections
|
||||
const sessions = useLiveQuery(sessionsCollection)
|
||||
const actions = useLiveQuery(actionsCollection)
|
||||
const sessionsQuery = useLiveQuery(sessionsCollection)
|
||||
const actionsQuery = useLiveQuery(actionsCollection)
|
||||
|
||||
const sessions = sessionsQuery.data ?? []
|
||||
const actions = actionsQuery.data ?? []
|
||||
|
||||
// Check connection status on mount
|
||||
useEffect(() => {
|
||||
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.css?url' {
|
||||
const url: string
|
||||
export default url
|
||||
}
|
||||
Reference in New Issue
Block a user