@swarmclawai/swarmclaw 0.5.3 → 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 +53 -9
- package/bin/server-cmd.js +1 -0
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +5 -2
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/route.ts +4 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +53 -1
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +12 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +18 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +63 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +40 -1
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +50 -17
- package/src/components/agents/agent-chat-list.tsx +148 -12
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +120 -65
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +173 -0
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +457 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +146 -0
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +356 -315
- package/src/components/chat/message-list.tsx +230 -8
- package/src/components/chat/streaming-bubble.tsx +104 -47
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +111 -73
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +130 -0
- package/src/components/chatrooms/chatroom-message.tsx +432 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +95 -56
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +107 -43
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +194 -97
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +1 -1
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +259 -126
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +44 -6
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +31 -15
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +57 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +182 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +59 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +416 -74
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-view-router.ts +69 -19
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +75 -15
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +229 -7
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +14 -2
- package/src/lib/server/daemon-state.ts +157 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +48 -6
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +105 -0
- package/src/lib/server/memory-db.ts +183 -10
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +56 -10
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +10 -0
- package/src/lib/server/session-tools/memory.ts +7 -1
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +115 -4
- package/src/lib/server/storage.ts +53 -29
- package/src/lib/server/stream-agent-chat.ts +185 -57
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/stores/use-app-store.ts +41 -3
- package/src/stores/use-chat-store.ts +113 -5
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +88 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState } from 'react'
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
4
4
|
import type { PersonalityDraft } from '@/types'
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
6
|
import {
|
|
@@ -21,22 +21,33 @@ const labelClass = 'block text-[11px] font-600 uppercase tracking-wider text-tex
|
|
|
21
21
|
|
|
22
22
|
export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSave }: Props) {
|
|
23
23
|
const [draft, setDraft] = useState<Record<string, string>>({})
|
|
24
|
+
const [initialDraft, setInitialDraft] = useState<Record<string, string>>({})
|
|
25
|
+
const [saveState, setSaveState] = useState<'idle' | 'saved'>('idle')
|
|
24
26
|
|
|
25
27
|
useEffect(() => {
|
|
28
|
+
let parsed: Record<string, string> = {}
|
|
26
29
|
if (fileType === 'IDENTITY.md') {
|
|
27
|
-
const
|
|
28
|
-
|
|
30
|
+
const p = parseIdentityMd(content)
|
|
31
|
+
parsed = { name: p.name || '', creature: p.creature || '', vibe: p.vibe || '', emoji: p.emoji || '' }
|
|
29
32
|
} else if (fileType === 'USER.md') {
|
|
30
|
-
const
|
|
31
|
-
|
|
33
|
+
const p = parseUserMd(content)
|
|
34
|
+
parsed = { name: p.name || '', callThem: p.callThem || '', pronouns: p.pronouns || '', timezone: p.timezone || '', notes: p.notes || '', context: p.context || '' }
|
|
32
35
|
} else if (fileType === 'SOUL.md') {
|
|
33
|
-
const
|
|
34
|
-
|
|
36
|
+
const p = parseSoulMd(content)
|
|
37
|
+
parsed = { coreTruths: p.coreTruths || '', boundaries: p.boundaries || '', vibe: p.vibe || '', continuity: p.continuity || '' }
|
|
35
38
|
}
|
|
39
|
+
setDraft(parsed)
|
|
40
|
+
setInitialDraft(parsed)
|
|
41
|
+
setSaveState('idle')
|
|
36
42
|
}, [content, fileType])
|
|
37
43
|
|
|
44
|
+
const isDirty = useMemo(() => {
|
|
45
|
+
return Object.keys(draft).some((k) => draft[k] !== (initialDraft[k] ?? ''))
|
|
46
|
+
}, [draft, initialDraft])
|
|
47
|
+
|
|
38
48
|
const update = (key: string, value: string) => {
|
|
39
49
|
setDraft((prev) => ({ ...prev, [key]: value }))
|
|
50
|
+
setSaveState('idle')
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
const handleSave = () => {
|
|
@@ -49,6 +60,9 @@ export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSav
|
|
|
49
60
|
serialized = serializeSoulMd(draft as PersonalityDraft['soul'])
|
|
50
61
|
}
|
|
51
62
|
onSave(serialized)
|
|
63
|
+
setInitialDraft({ ...draft })
|
|
64
|
+
setSaveState('saved')
|
|
65
|
+
setTimeout(() => setSaveState('idle'), 1500)
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
const fields = fileType === 'IDENTITY.md'
|
|
@@ -99,13 +113,27 @@ export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSav
|
|
|
99
113
|
)}
|
|
100
114
|
</div>
|
|
101
115
|
))}
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
<div className="flex items-center gap-3">
|
|
117
|
+
<button
|
|
118
|
+
onClick={handleSave}
|
|
119
|
+
className="self-start px-4 py-1.5 rounded-[8px] border-none bg-accent-bright text-white text-[12px] font-600 cursor-pointer transition-all hover:brightness-110 focus-visible:ring-1 focus-visible:ring-accent-bright/50"
|
|
120
|
+
style={{ fontFamily: 'inherit' }}
|
|
121
|
+
>
|
|
122
|
+
Apply to Raw Editor
|
|
123
|
+
</button>
|
|
124
|
+
{isDirty && saveState === 'idle' && (
|
|
125
|
+
<span className="inline-flex items-center gap-1.5 text-[11px] text-amber-400 font-600">
|
|
126
|
+
<span className="w-1.5 h-1.5 rounded-full bg-amber-400" />
|
|
127
|
+
Unsaved
|
|
128
|
+
</span>
|
|
129
|
+
)}
|
|
130
|
+
{saveState === 'saved' && (
|
|
131
|
+
<span className="inline-flex items-center gap-1.5 text-[11px] text-emerald-400 font-600">
|
|
132
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12" /></svg>
|
|
133
|
+
Saved
|
|
134
|
+
</span>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
109
137
|
</div>
|
|
110
138
|
)
|
|
111
139
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react'
|
|
4
|
+
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
5
|
+
import { SOUL_LIBRARY, SOUL_ARCHETYPES, searchSouls, type SoulTemplate } from '@/lib/soul-library'
|
|
6
|
+
|
|
7
|
+
interface SoulLibraryPickerProps {
|
|
8
|
+
open: boolean
|
|
9
|
+
onClose: () => void
|
|
10
|
+
onSelect: (soul: string) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function SoulLibraryPicker({ open, onClose, onSelect }: SoulLibraryPickerProps) {
|
|
14
|
+
const [query, setQuery] = useState('')
|
|
15
|
+
const [archetype, setArchetype] = useState('All')
|
|
16
|
+
|
|
17
|
+
const results = useMemo(() => searchSouls(query, archetype), [query, archetype])
|
|
18
|
+
|
|
19
|
+
const handleSelect = (template: SoulTemplate) => {
|
|
20
|
+
onSelect(template.soul)
|
|
21
|
+
onClose()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<BottomSheet open={open} onClose={onClose}>
|
|
26
|
+
<div className="mb-6">
|
|
27
|
+
<h2 className="font-display text-[24px] font-700 tracking-[-0.03em] mb-1">Soul Library</h2>
|
|
28
|
+
<p className="text-[13px] text-text-3">Browse personality templates for your agent</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
{/* Search */}
|
|
32
|
+
<div className="mb-4">
|
|
33
|
+
<input
|
|
34
|
+
type="text"
|
|
35
|
+
value={query}
|
|
36
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
37
|
+
placeholder="Search personalities..."
|
|
38
|
+
className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none focus-glow"
|
|
39
|
+
style={{ fontFamily: 'inherit' }}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Archetype filter tabs */}
|
|
44
|
+
<div className="flex gap-1 flex-wrap mb-6">
|
|
45
|
+
{SOUL_ARCHETYPES.map((a) => (
|
|
46
|
+
<button
|
|
47
|
+
key={a}
|
|
48
|
+
onClick={() => setArchetype(a)}
|
|
49
|
+
className={`px-3 py-1.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border
|
|
50
|
+
${archetype === a
|
|
51
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
52
|
+
: 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
|
|
53
|
+
style={{ fontFamily: 'inherit' }}
|
|
54
|
+
>
|
|
55
|
+
{a}
|
|
56
|
+
</button>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{/* Results grid */}
|
|
61
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-h-[60vh] overflow-y-auto pb-4">
|
|
62
|
+
{results.map((template) => (
|
|
63
|
+
<button
|
|
64
|
+
key={template.id}
|
|
65
|
+
onClick={() => handleSelect(template)}
|
|
66
|
+
className="text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 hover:border-accent-bright/20 transition-all cursor-pointer group"
|
|
67
|
+
style={{ fontFamily: 'inherit' }}
|
|
68
|
+
>
|
|
69
|
+
<div className="flex items-start gap-2 mb-2">
|
|
70
|
+
<h4 className="text-[14px] font-600 text-text group-hover:text-accent-bright transition-colors">
|
|
71
|
+
{template.name}
|
|
72
|
+
</h4>
|
|
73
|
+
<span className="px-1.5 py-0.5 rounded-[5px] bg-white/[0.06] text-text-3 text-[10px] font-600 shrink-0">
|
|
74
|
+
{template.archetype}
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
<p className="text-[12px] text-text-3 mb-2">{template.description}</p>
|
|
78
|
+
<p className="text-[11px] text-text-3/60 line-clamp-2 italic">{template.soul}</p>
|
|
79
|
+
</button>
|
|
80
|
+
))}
|
|
81
|
+
{results.length === 0 && (
|
|
82
|
+
<p className="text-[13px] text-text-3 col-span-2 text-center py-8">No personalities match your search</p>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<p className="text-[11px] text-text-3/50 mt-4 text-center">{SOUL_LIBRARY.length} personalities available</p>
|
|
87
|
+
</BottomSheet>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -88,7 +88,14 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
if (checking) return
|
|
91
|
+
if (checking) return (
|
|
92
|
+
<div className="h-full flex items-center justify-center bg-bg">
|
|
93
|
+
<div
|
|
94
|
+
className="h-6 w-6 rounded-full border-2 border-white/[0.08] border-t-accent-bright"
|
|
95
|
+
style={{ animation: 'spin 0.8s linear infinite' }}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
92
99
|
|
|
93
100
|
return (
|
|
94
101
|
<div className="h-full flex flex-col items-center justify-center px-8 bg-bg relative overflow-hidden">
|
|
@@ -192,7 +199,7 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
|
|
|
192
199
|
<button
|
|
193
200
|
onClick={handleClaimKey}
|
|
194
201
|
disabled={loading}
|
|
195
|
-
className="px-12 py-4 rounded-[16px] border-none bg-
|
|
202
|
+
className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
|
|
196
203
|
cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
|
|
197
204
|
shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
|
|
198
205
|
>
|
|
@@ -233,7 +240,7 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
|
|
|
233
240
|
<button
|
|
234
241
|
type="submit"
|
|
235
242
|
disabled={!key.trim() || loading}
|
|
236
|
-
className="px-12 py-4 rounded-[16px] border-none bg-
|
|
243
|
+
className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
|
|
237
244
|
cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
|
|
238
245
|
shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
|
|
239
246
|
>
|
|
@@ -819,7 +819,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
819
819
|
<button
|
|
820
820
|
onClick={saveProviderAndContinue}
|
|
821
821
|
disabled={(requiresKey && !apiKey.trim()) || saving}
|
|
822
|
-
className="px-8 py-3.5 rounded-[14px] border-none bg-
|
|
822
|
+
className="px-8 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-display font-600
|
|
823
823
|
cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
|
|
824
824
|
shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
|
|
825
825
|
>
|
|
@@ -923,7 +923,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
923
923
|
<button
|
|
924
924
|
onClick={createStarterAgent}
|
|
925
925
|
disabled={!agentName.trim() || saving}
|
|
926
|
-
className="px-10 py-3.5 rounded-[14px] border-none bg-
|
|
926
|
+
className="px-10 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-display font-600
|
|
927
927
|
cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
|
|
928
928
|
shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
|
|
929
929
|
>
|
|
@@ -2,22 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
5
6
|
import { api } from '@/lib/api-client'
|
|
6
7
|
|
|
7
8
|
export function UserPicker() {
|
|
8
9
|
const setUser = useAppStore((s) => s.setUser)
|
|
10
|
+
const loadSettings = useAppStore((s) => s.loadSettings)
|
|
9
11
|
const [name, setName] = useState('')
|
|
12
|
+
const [avatarSeed, setAvatarSeed] = useState(() => Math.random().toString(36).slice(2, 10))
|
|
10
13
|
|
|
11
14
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
12
15
|
e.preventDefault()
|
|
13
16
|
const trimmed = name.trim()
|
|
14
17
|
if (!trimmed) return
|
|
15
18
|
const userName = trimmed.toLowerCase()
|
|
16
|
-
// Save server-side so it persists across devices
|
|
17
19
|
try {
|
|
18
|
-
await api('PUT', '/settings', { userName })
|
|
20
|
+
await api('PUT', '/settings', { userName, userAvatarSeed: avatarSeed.trim() || undefined })
|
|
19
21
|
} catch { /* still set locally */ }
|
|
20
22
|
setUser(userName)
|
|
23
|
+
loadSettings()
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
return (
|
|
@@ -71,10 +74,35 @@ export function UserPicker() {
|
|
|
71
74
|
focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
72
75
|
style={{ fontFamily: 'inherit' }}
|
|
73
76
|
/>
|
|
77
|
+
|
|
78
|
+
{/* Avatar picker */}
|
|
79
|
+
<div className="flex flex-col items-center gap-3">
|
|
80
|
+
<AgentAvatar seed={avatarSeed || null} name={name || '?'} size={64} />
|
|
81
|
+
<div className="flex items-center gap-2">
|
|
82
|
+
<input
|
|
83
|
+
type="text"
|
|
84
|
+
value={avatarSeed}
|
|
85
|
+
onChange={(e) => setAvatarSeed(e.target.value)}
|
|
86
|
+
placeholder="Avatar seed"
|
|
87
|
+
className="w-[160px] px-3 py-2 rounded-[10px] border border-white/[0.08] bg-surface
|
|
88
|
+
text-text text-[13px] text-center outline-none transition-all
|
|
89
|
+
focus:border-accent-bright/30"
|
|
90
|
+
/>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
onClick={() => setAvatarSeed(Math.random().toString(36).slice(2, 10))}
|
|
94
|
+
className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-3 text-[12px] font-600
|
|
95
|
+
cursor-pointer transition-all hover:bg-white/[0.04] shrink-0"
|
|
96
|
+
>
|
|
97
|
+
Randomize
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
74
102
|
<button
|
|
75
103
|
type="submit"
|
|
76
104
|
disabled={!name.trim()}
|
|
77
|
-
className="px-12 py-4 rounded-[16px] border-none bg-
|
|
105
|
+
className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
|
|
78
106
|
cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
|
|
79
107
|
shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
|
|
80
108
|
style={{ fontFamily: 'inherit' }}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from 'react'
|
|
4
|
+
import { useWs } from '@/hooks/use-ws'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
6
|
+
|
|
7
|
+
interface CanvasPanelProps {
|
|
8
|
+
sessionId: string
|
|
9
|
+
agentName?: string
|
|
10
|
+
onClose: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function CanvasPanel({ sessionId, agentName, onClose }: CanvasPanelProps) {
|
|
14
|
+
const [content, setContent] = useState<string | null>(null)
|
|
15
|
+
|
|
16
|
+
const loadCanvas = useCallback(async () => {
|
|
17
|
+
try {
|
|
18
|
+
const res = await api<{ content: string | null }>('GET', `/canvas/${sessionId}`)
|
|
19
|
+
setContent(res.content)
|
|
20
|
+
} catch { /* ignore */ }
|
|
21
|
+
}, [sessionId])
|
|
22
|
+
|
|
23
|
+
useEffect(() => { loadCanvas() }, [loadCanvas]) // eslint-disable-line react-hooks/set-state-in-effect
|
|
24
|
+
useWs(`canvas:${sessionId}`, loadCanvas, 10_000)
|
|
25
|
+
|
|
26
|
+
if (!content) return (
|
|
27
|
+
<div className="flex flex-col h-full border-l border-white/[0.06] bg-bg min-w-[400px]">
|
|
28
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] shrink-0">
|
|
29
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright shrink-0">
|
|
30
|
+
<rect x="2" y="3" width="20" height="14" rx="2" /><path d="M8 21h8" /><path d="M12 17v4" />
|
|
31
|
+
</svg>
|
|
32
|
+
<span className="text-[13px] font-600 text-text flex-1 truncate">
|
|
33
|
+
Canvas{agentName ? ` — ${agentName}` : ''}
|
|
34
|
+
</span>
|
|
35
|
+
<button
|
|
36
|
+
onClick={onClose}
|
|
37
|
+
className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
|
|
38
|
+
title="Close canvas"
|
|
39
|
+
aria-label="Close canvas"
|
|
40
|
+
>
|
|
41
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
42
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
43
|
+
</svg>
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="flex-1 flex items-center justify-center">
|
|
47
|
+
<div className="text-center">
|
|
48
|
+
<div className="w-8 h-8 rounded-full border-2 border-text-3/20 border-t-accent-bright animate-spin mx-auto mb-3" />
|
|
49
|
+
<span className="text-[13px] text-text-3">Loading canvas...</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="flex flex-col h-full border-l border-white/[0.06] bg-bg min-w-[400px]">
|
|
57
|
+
{/* Toolbar */}
|
|
58
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] shrink-0">
|
|
59
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright shrink-0">
|
|
60
|
+
<rect x="2" y="3" width="20" height="14" rx="2" /><path d="M8 21h8" /><path d="M12 17v4" />
|
|
61
|
+
</svg>
|
|
62
|
+
<span className="text-[13px] font-600 text-text flex-1 truncate">
|
|
63
|
+
Canvas{agentName ? ` — ${agentName}` : ''}
|
|
64
|
+
</span>
|
|
65
|
+
<button
|
|
66
|
+
onClick={loadCanvas}
|
|
67
|
+
className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
|
|
68
|
+
title="Refresh"
|
|
69
|
+
>
|
|
70
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
71
|
+
<polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
|
|
72
|
+
</svg>
|
|
73
|
+
</button>
|
|
74
|
+
<button
|
|
75
|
+
onClick={onClose}
|
|
76
|
+
className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
|
|
77
|
+
title="Close canvas"
|
|
78
|
+
>
|
|
79
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
80
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
81
|
+
</svg>
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Sandboxed iframe */}
|
|
86
|
+
<div className="flex-1 overflow-hidden">
|
|
87
|
+
<iframe
|
|
88
|
+
sandbox="allow-scripts allow-same-origin"
|
|
89
|
+
srcDoc={content}
|
|
90
|
+
className="w-full h-full border-none bg-white"
|
|
91
|
+
title="Agent Canvas"
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
const NOTABLE_TOOLS: Record<string, { label: string; color: string; icon: 'brain' | 'clipboard' | 'delegate' | 'search' | 'message' }> = {
|
|
6
|
+
memory: { label: 'Committed to memory', color: '#A855F7', icon: 'brain' },
|
|
7
|
+
memory_tool: { label: 'Committed to memory', color: '#A855F7', icon: 'brain' },
|
|
8
|
+
manage_tasks: { label: 'Created a task', color: '#EC4899', icon: 'clipboard' },
|
|
9
|
+
manage_schedules: { label: 'Scheduled something', color: '#EC4899', icon: 'clipboard' },
|
|
10
|
+
manage_agents: { label: 'Created an agent', color: '#EC4899', icon: 'clipboard' },
|
|
11
|
+
delegate_to_claude_code: { label: 'Delegated to Claude Code', color: '#38BDF8', icon: 'delegate' },
|
|
12
|
+
delegate_to_codex_cli: { label: 'Delegated to Codex', color: '#38BDF8', icon: 'delegate' },
|
|
13
|
+
delegate_to_opencode_cli: { label: 'Delegated to OpenCode', color: '#38BDF8', icon: 'delegate' },
|
|
14
|
+
delegate_to_agent: { label: 'Delegating task', color: '#6366F1', icon: 'delegate' },
|
|
15
|
+
check_delegation_status: { label: 'Checking delegation', color: '#6366F1', icon: 'delegate' },
|
|
16
|
+
web_search: { label: 'Searched the web', color: '#22C55E', icon: 'search' },
|
|
17
|
+
connector_message_tool: { label: 'Sent a message', color: '#F97316', icon: 'message' },
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function extractSnippet(toolName: string, toolInput: string): string | null {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(toolInput)
|
|
23
|
+
if ((toolName === 'memory' || toolName === 'memory_tool') && parsed.title) return parsed.title
|
|
24
|
+
if ((toolName === 'memory' || toolName === 'memory_tool') && parsed.key) return parsed.key
|
|
25
|
+
if (toolName === 'manage_tasks' && parsed.title) return parsed.title
|
|
26
|
+
if (toolName === 'manage_schedules' && parsed.name) return parsed.name
|
|
27
|
+
if (toolName === 'manage_agents' && parsed.name) return parsed.name
|
|
28
|
+
if (toolName === 'delegate_to_agent' && (parsed.agentName || parsed.agentId)) return parsed.agentName || parsed.agentId
|
|
29
|
+
if (toolName === 'check_delegation_status' && parsed.agentName) return parsed.agentName
|
|
30
|
+
if (toolName.startsWith('delegate_to_') && parsed.task) return parsed.task
|
|
31
|
+
if (toolName === 'web_search' && parsed.query) return parsed.query
|
|
32
|
+
if (toolName === 'connector_message_tool' && parsed.to) return parsed.to
|
|
33
|
+
} catch { /* ignore parse errors */ }
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function MomentIcon({ icon, color }: { icon: string; color: string }) {
|
|
38
|
+
switch (icon) {
|
|
39
|
+
case 'brain':
|
|
40
|
+
return (
|
|
41
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
|
|
42
|
+
<path d="M12 2a7 7 0 0 1 7 7c0 2.38-1.19 4.47-3 5.74V17a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 0 1 7-7z" />
|
|
43
|
+
<line x1="10" y1="22" x2="14" y2="22" />
|
|
44
|
+
</svg>
|
|
45
|
+
)
|
|
46
|
+
case 'delegate':
|
|
47
|
+
return (
|
|
48
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
|
|
49
|
+
<path d="M7 17l9.2-9.2M17 17V7H7" />
|
|
50
|
+
</svg>
|
|
51
|
+
)
|
|
52
|
+
case 'search':
|
|
53
|
+
return (
|
|
54
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
|
|
55
|
+
<circle cx="11" cy="11" r="8" />
|
|
56
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
57
|
+
</svg>
|
|
58
|
+
)
|
|
59
|
+
case 'message':
|
|
60
|
+
return (
|
|
61
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
|
|
62
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
63
|
+
</svg>
|
|
64
|
+
)
|
|
65
|
+
default: // clipboard
|
|
66
|
+
return (
|
|
67
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
|
|
68
|
+
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
|
|
69
|
+
<rect x="8" y="2" width="8" height="4" rx="1" ry="1" />
|
|
70
|
+
</svg>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface Props {
|
|
76
|
+
toolName: string
|
|
77
|
+
toolInput: string
|
|
78
|
+
onDismiss: () => void
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function ActivityMoment({ toolName, toolInput, onDismiss }: Props) {
|
|
82
|
+
const config = NOTABLE_TOOLS[toolName]
|
|
83
|
+
const [phase, setPhase] = useState<'in' | 'out'>('in')
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const holdTimer = setTimeout(() => setPhase('out'), 2000)
|
|
87
|
+
const dismissTimer = setTimeout(onDismiss, 2500)
|
|
88
|
+
return () => {
|
|
89
|
+
clearTimeout(holdTimer)
|
|
90
|
+
clearTimeout(dismissTimer)
|
|
91
|
+
}
|
|
92
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
93
|
+
}, [])
|
|
94
|
+
|
|
95
|
+
if (!config) return null
|
|
96
|
+
|
|
97
|
+
const snippet = extractSnippet(toolName, toolInput)
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
className="absolute bottom-full left-0 z-10 pointer-events-none mb-1.5"
|
|
102
|
+
style={{
|
|
103
|
+
animation: phase === 'in'
|
|
104
|
+
? 'activity-moment-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards'
|
|
105
|
+
: 'activity-moment-out 0.4s cubic-bezier(0.4, 0, 1, 1) forwards',
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<div
|
|
109
|
+
className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] shadow-lg whitespace-nowrap"
|
|
110
|
+
style={{
|
|
111
|
+
background: 'var(--card)',
|
|
112
|
+
border: `1px solid ${config.color}40`,
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
<MomentIcon icon={config.icon} color={config.color} />
|
|
116
|
+
<span className="text-[10px] font-600" style={{ color: config.color }}>
|
|
117
|
+
{config.label}
|
|
118
|
+
</span>
|
|
119
|
+
{snippet && (
|
|
120
|
+
<span className="text-[10px] text-text-3/60 max-w-[120px] truncate">
|
|
121
|
+
{snippet}
|
|
122
|
+
</span>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function isNotableTool(name: string): boolean {
|
|
130
|
+
return name in NOTABLE_TOOLS
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const HEART_PATH = 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'
|
|
134
|
+
|
|
135
|
+
export function HeartbeatMoment({ onDismiss }: { onDismiss: () => void }) {
|
|
136
|
+
const [phase, setPhase] = useState<'in' | 'out'>('in')
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
const holdTimer = setTimeout(() => setPhase('out'), 2000)
|
|
140
|
+
const dismissTimer = setTimeout(onDismiss, 2500)
|
|
141
|
+
return () => {
|
|
142
|
+
clearTimeout(holdTimer)
|
|
143
|
+
clearTimeout(dismissTimer)
|
|
144
|
+
}
|
|
145
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
146
|
+
}, [])
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
className="absolute bottom-full left-0 z-10 pointer-events-none mb-1.5"
|
|
151
|
+
style={{
|
|
152
|
+
animation: phase === 'in'
|
|
153
|
+
? 'activity-moment-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards'
|
|
154
|
+
: 'activity-moment-out 0.4s cubic-bezier(0.4, 0, 1, 1) forwards',
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
<div
|
|
158
|
+
className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] shadow-lg whitespace-nowrap"
|
|
159
|
+
style={{
|
|
160
|
+
background: 'var(--card)',
|
|
161
|
+
border: '1px solid rgba(34,197,94,0.3)',
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="#22c55e">
|
|
165
|
+
<path d={HEART_PATH} />
|
|
166
|
+
</svg>
|
|
167
|
+
<span className="text-[10px] font-600" style={{ color: '#22c55e' }}>
|
|
168
|
+
Heartbeat OK
|
|
169
|
+
</span>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
}
|