@swarmclawai/swarmclaw 0.7.7 → 0.8.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/README.md +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -8,26 +8,41 @@ import { toast } from 'sonner'
|
|
|
8
8
|
import { useWs } from '@/hooks/use-ws'
|
|
9
9
|
import { ExecApprovalCard } from '@/components/chat/exec-approval-card'
|
|
10
10
|
import { getApprovalPayload, getApprovalTitle } from '@/lib/approval-display'
|
|
11
|
-
import type { ApprovalRequest } from '@/types'
|
|
11
|
+
import type { AppSettings, ApprovalCategory, ApprovalRequest } from '@/types'
|
|
12
12
|
|
|
13
13
|
const CATEGORY_LABELS: Record<string, string> = {
|
|
14
14
|
tool_access: 'Plugin Access',
|
|
15
15
|
wallet_transfer: 'Wallet Transfer',
|
|
16
|
+
wallet_action: 'Wallet Action',
|
|
16
17
|
plugin_scaffold: 'Plugin Creation',
|
|
17
18
|
plugin_install: 'Plugin Install',
|
|
19
|
+
connector_sender: 'Connector Sender',
|
|
18
20
|
task_tool: 'Task Plugin Call',
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
const CATEGORY_ICONS: Record<string, string> = {
|
|
22
24
|
tool_access: '🔑',
|
|
23
25
|
wallet_transfer: '💰',
|
|
26
|
+
wallet_action: '✍️',
|
|
24
27
|
plugin_scaffold: '🔌',
|
|
25
28
|
plugin_install: '📦',
|
|
29
|
+
connector_sender: '📲',
|
|
26
30
|
task_tool: '🤖',
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
type ApprovalScope = 'all' | 'execution' | 'workflow' | 'task'
|
|
30
34
|
|
|
35
|
+
const AUTO_APPROVE_OPTIONS: Array<{ id: ApprovalCategory; label: string; description: string; risk?: 'high' | 'very-high' }> = [
|
|
36
|
+
{ id: 'tool_access', label: 'Plugin Access', description: 'Auto-enable requested plugins for a chat.' },
|
|
37
|
+
{ id: 'plugin_scaffold', label: 'Plugin Scaffold', description: 'Auto-create plugin files requested by agents.' },
|
|
38
|
+
{ id: 'plugin_install', label: 'Plugin Install', description: 'Auto-install plugins from approved URLs.' },
|
|
39
|
+
{ id: 'connector_sender', label: 'Connector Senders', description: 'Auto-approve new connector senders and add them to the allowlist.' },
|
|
40
|
+
{ id: 'human_loop', label: 'Human Approval Requests', description: 'Auto-approve ask-human approval prompts.' },
|
|
41
|
+
{ id: 'wallet_transfer', label: 'Wallet Transfers', description: 'Auto-approve wallet send requests.', risk: 'high' },
|
|
42
|
+
{ id: 'wallet_action', label: 'Wallet Actions', description: 'Auto-approve wallet signatures and arbitrary transaction requests.', risk: 'very-high' },
|
|
43
|
+
{ id: 'task_tool', label: 'Task Tool Calls', description: 'Auto-approve task-level tool approvals.' },
|
|
44
|
+
]
|
|
45
|
+
|
|
31
46
|
function relativeTime(ts: number): string {
|
|
32
47
|
const diff = Date.now() - ts
|
|
33
48
|
if (diff < 60_000) return 'just now'
|
|
@@ -39,9 +54,11 @@ function relativeTime(ts: number): string {
|
|
|
39
54
|
export function ApprovalsPanel() {
|
|
40
55
|
const tasks = useAppStore((s) => s.tasks)
|
|
41
56
|
const agents = useAppStore((s) => s.agents)
|
|
57
|
+
const appSettings = useAppStore((s) => s.appSettings)
|
|
42
58
|
const serverApprovals = useAppStore((s) => s.approvals)
|
|
43
59
|
const loadTasks = useAppStore((s) => s.loadTasks)
|
|
44
60
|
const loadServerApprovals = useAppStore((s) => s.loadApprovals)
|
|
61
|
+
const loadAppSettings = useAppStore((s) => s.loadSettings)
|
|
45
62
|
|
|
46
63
|
const execApprovals = useApprovalStore((s) => s.approvals)
|
|
47
64
|
const loadExecApprovals = useApprovalStore((s) => s.loadApprovals)
|
|
@@ -73,12 +90,17 @@ export function ApprovalsPanel() {
|
|
|
73
90
|
const [scope, setScope] = useState<ApprovalScope>('all')
|
|
74
91
|
const [categoryFilter, setCategoryFilter] = useState('all')
|
|
75
92
|
const [now, setNow] = useState(() => Date.now())
|
|
93
|
+
const [savingSetting, setSavingSetting] = useState<string | null>(null)
|
|
76
94
|
|
|
77
95
|
useEffect(() => {
|
|
78
96
|
const intervalId = window.setInterval(() => setNow(Date.now()), 60_000)
|
|
79
97
|
return () => window.clearInterval(intervalId)
|
|
80
98
|
}, [])
|
|
81
99
|
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
void loadAppSettings()
|
|
102
|
+
}, [loadAppSettings])
|
|
103
|
+
|
|
82
104
|
const taskApprovals = useMemo(() => {
|
|
83
105
|
return Object.values(tasks)
|
|
84
106
|
.filter((t) => t.pendingApproval)
|
|
@@ -158,28 +180,51 @@ export function ApprovalsPanel() {
|
|
|
158
180
|
label: 'Execution',
|
|
159
181
|
value: sortedExecApprovals.length,
|
|
160
182
|
tone: 'text-amber-400',
|
|
183
|
+
dotClass: 'bg-amber-400',
|
|
161
184
|
hint: 'Command approvals from OpenClaw',
|
|
162
185
|
},
|
|
163
186
|
{
|
|
164
187
|
label: 'Workflow',
|
|
165
188
|
value: sessionApprovals.length,
|
|
166
189
|
tone: 'text-sky-400',
|
|
190
|
+
dotClass: 'bg-sky-400',
|
|
167
191
|
hint: 'Agent and plugin governance requests',
|
|
168
192
|
},
|
|
169
193
|
{
|
|
170
194
|
label: 'Task Calls',
|
|
171
195
|
value: taskApprovals.length,
|
|
172
196
|
tone: 'text-violet-400',
|
|
197
|
+
dotClass: 'bg-violet-400',
|
|
173
198
|
hint: 'Tasks waiting on tool approval',
|
|
174
199
|
},
|
|
175
200
|
{
|
|
176
201
|
label: 'Recently Active',
|
|
177
202
|
value: workflowApprovals.filter((req) => now - req.updatedAt < 60 * 60 * 1000).length,
|
|
178
203
|
tone: 'text-emerald-400',
|
|
204
|
+
dotClass: 'bg-emerald-400',
|
|
179
205
|
hint: 'Updated in the last hour',
|
|
180
206
|
},
|
|
181
207
|
]
|
|
182
208
|
|
|
209
|
+
const autoApproved = useMemo(() => new Set(appSettings.approvalAutoApproveCategories || []), [appSettings.approvalAutoApproveCategories])
|
|
210
|
+
const approvalsEnabled = appSettings.approvalsEnabled ?? false
|
|
211
|
+
const outboundApprovalEnabled = appSettings.safetyRequireApprovalForOutbound ?? false
|
|
212
|
+
const autoApproveEnabledCount = autoApproved.size
|
|
213
|
+
const autoApproveManualCount = AUTO_APPROVE_OPTIONS.length - autoApproveEnabledCount
|
|
214
|
+
|
|
215
|
+
const saveApprovalSettings = async (patch: Partial<AppSettings>, successMessage: string, key: string) => {
|
|
216
|
+
try {
|
|
217
|
+
setSavingSetting(key)
|
|
218
|
+
const settings = await api<AppSettings>('PUT', '/settings', patch)
|
|
219
|
+
useAppStore.setState({ appSettings: settings })
|
|
220
|
+
toast.success(successMessage)
|
|
221
|
+
} catch (err: unknown) {
|
|
222
|
+
toast.error(err instanceof Error ? err.message : 'Failed to update approval settings')
|
|
223
|
+
} finally {
|
|
224
|
+
setSavingSetting((current) => (current === key ? null : current))
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
183
228
|
const handleDecision = async (req: ApprovalRequest, approved: boolean) => {
|
|
184
229
|
try {
|
|
185
230
|
if (req.category === 'task_tool') {
|
|
@@ -195,51 +240,235 @@ export function ApprovalsPanel() {
|
|
|
195
240
|
}
|
|
196
241
|
}
|
|
197
242
|
|
|
198
|
-
if (pendingCount === 0) {
|
|
199
|
-
return (
|
|
200
|
-
<div className="flex-1 flex flex-col items-center justify-center p-8 text-center">
|
|
201
|
-
<div className="w-16 h-16 rounded-[24px] bg-white/[0.02] border border-white/[0.04] flex items-center justify-center mb-6">
|
|
202
|
-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/40">
|
|
203
|
-
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/>
|
|
204
|
-
<path d="m9 12 2 2 4-4"/>
|
|
205
|
-
</svg>
|
|
206
|
-
</div>
|
|
207
|
-
<h2 className="font-display text-[18px] font-600 text-text-2 mb-2">No pending approvals</h2>
|
|
208
|
-
<p className="text-[13px] text-text-3/60 max-w-[320px]">
|
|
209
|
-
Your swarm is operating autonomously. Actions requiring oversight will appear here.
|
|
210
|
-
</p>
|
|
211
|
-
</div>
|
|
212
|
-
)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
243
|
return (
|
|
216
244
|
<div className="flex-1 overflow-y-auto px-6 py-8">
|
|
217
|
-
<div className="max-w-
|
|
218
|
-
<div className="flex items-
|
|
245
|
+
<div className="max-w-5xl mx-auto space-y-6">
|
|
246
|
+
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
219
247
|
<div>
|
|
220
248
|
<h1 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-1">Approvals</h1>
|
|
221
249
|
<p className="text-[13px] text-text-3">Execution, task, and governance requests queued for review</p>
|
|
222
250
|
</div>
|
|
223
|
-
<div className="
|
|
224
|
-
{
|
|
251
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
252
|
+
<div className={`px-3 py-1.5 rounded-full border text-[11px] font-700 ${
|
|
253
|
+
approvalsEnabled
|
|
254
|
+
? 'bg-amber-500/10 border-amber-500/20 text-amber-300'
|
|
255
|
+
: 'bg-emerald-500/10 border-emerald-500/20 text-emerald-300'
|
|
256
|
+
}`}>
|
|
257
|
+
{approvalsEnabled ? 'Manual approvals enabled' : 'Approvals auto-run'}
|
|
258
|
+
</div>
|
|
259
|
+
<div className="px-3 py-1.5 rounded-full bg-white/[0.04] border border-white/[0.06] text-text-2 text-[11px] font-600">
|
|
260
|
+
{pendingCount} pending
|
|
261
|
+
</div>
|
|
225
262
|
</div>
|
|
226
263
|
</div>
|
|
227
264
|
|
|
228
|
-
<div className="grid grid-cols-2
|
|
265
|
+
<div className="grid grid-cols-2 xl:grid-cols-4 gap-3">
|
|
229
266
|
{summaryCards.map((card) => (
|
|
230
|
-
<div key={card.label} className="rounded-[
|
|
267
|
+
<div key={card.label} className="relative overflow-hidden rounded-[16px] border border-white/[0.06] bg-white/[0.02] px-4 py-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]">
|
|
268
|
+
<div className="flex items-center justify-between gap-3">
|
|
269
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/65">{card.label}</div>
|
|
270
|
+
<div className={`h-2.5 w-2.5 rounded-full ${card.dotClass}`} />
|
|
271
|
+
</div>
|
|
231
272
|
<div className={`text-[22px] font-display font-700 tracking-[-0.03em] ${card.tone}`}>
|
|
232
273
|
{card.value}
|
|
233
274
|
</div>
|
|
234
|
-
<
|
|
235
|
-
<p className="text-[10px] text-text-3/50 mt-1 leading-relaxed">{card.hint}</p>
|
|
275
|
+
<p className="text-[10px] text-text-3/55 mt-1.5 leading-relaxed">{card.hint}</p>
|
|
236
276
|
</div>
|
|
237
277
|
))}
|
|
238
278
|
</div>
|
|
239
279
|
|
|
240
|
-
<div className="
|
|
241
|
-
<div className="
|
|
242
|
-
<div className="flex
|
|
280
|
+
<div className="grid gap-4 xl:grid-cols-[320px_minmax(0,1fr)]">
|
|
281
|
+
<div className="rounded-[18px] border border-white/[0.06] bg-white/[0.02] p-5 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]">
|
|
282
|
+
<div className="flex items-center justify-between gap-3 mb-4">
|
|
283
|
+
<div>
|
|
284
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/65">Approval Mode</div>
|
|
285
|
+
<div className="text-[16px] font-display font-700 tracking-[-0.02em] text-text mt-1">
|
|
286
|
+
{approvalsEnabled ? 'Manual review queue' : 'Auto-run workflow mode'}
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
<div className={`px-2.5 py-1 rounded-full text-[10px] font-700 border ${
|
|
290
|
+
approvalsEnabled
|
|
291
|
+
? 'bg-amber-500/10 border-amber-500/20 text-amber-300'
|
|
292
|
+
: 'bg-emerald-500/10 border-emerald-500/20 text-emerald-300'
|
|
293
|
+
}`}>
|
|
294
|
+
{approvalsEnabled ? 'On' : 'Off'}
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
<p className="text-[12px] text-text-3/70 leading-relaxed">
|
|
298
|
+
{approvalsEnabled
|
|
299
|
+
? 'Requests pause here until someone approves or rejects them. Auto-approve lets you carve out safe request classes without disabling oversight entirely.'
|
|
300
|
+
: 'Workflow approvals will auto-run by default. Use outbound and category-specific controls below to keep higher-risk actions gated.'}
|
|
301
|
+
</p>
|
|
302
|
+
<div className="grid grid-cols-1 gap-2 mt-5">
|
|
303
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-black/20 px-3.5 py-3">
|
|
304
|
+
<div className="text-[10px] font-700 uppercase tracking-[0.08em] text-text-3/55">Queue</div>
|
|
305
|
+
<div className="mt-1 flex items-center justify-between gap-3">
|
|
306
|
+
<span className="text-[13px] font-600 text-text-2">Pending right now</span>
|
|
307
|
+
<span className="text-[13px] font-700 text-text">{pendingCount}</span>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-black/20 px-3.5 py-3">
|
|
311
|
+
<div className="text-[10px] font-700 uppercase tracking-[0.08em] text-text-3/55">Auto-Approve</div>
|
|
312
|
+
<div className="mt-1 flex items-center justify-between gap-3">
|
|
313
|
+
<span className="text-[13px] font-600 text-text-2">Categories enabled</span>
|
|
314
|
+
<span className="text-[13px] font-700 text-text">{autoApproveEnabledCount}</span>
|
|
315
|
+
</div>
|
|
316
|
+
<div className="text-[11px] text-text-3/55 mt-1">{autoApproveManualCount} still require manual review</div>
|
|
317
|
+
</div>
|
|
318
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-black/20 px-3.5 py-3">
|
|
319
|
+
<div className="text-[10px] font-700 uppercase tracking-[0.08em] text-text-3/55">Outbound Sends</div>
|
|
320
|
+
<div className="mt-1 flex items-center justify-between gap-3">
|
|
321
|
+
<span className="text-[13px] font-600 text-text-2">Connector message sends</span>
|
|
322
|
+
<span className={`text-[11px] font-700 ${outboundApprovalEnabled ? 'text-amber-300' : 'text-emerald-300'}`}>
|
|
323
|
+
{outboundApprovalEnabled ? 'Needs approval' : 'Direct send'}
|
|
324
|
+
</span>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div className="rounded-[18px] border border-white/[0.06] bg-white/[0.02] p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]">
|
|
331
|
+
<div className="flex flex-col gap-4">
|
|
332
|
+
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-3">
|
|
333
|
+
<div>
|
|
334
|
+
<h2 className="text-[13px] font-700 text-text">Approval Controls</h2>
|
|
335
|
+
<p className="text-[12px] text-text-3/70 mt-1 max-w-[640px]">
|
|
336
|
+
Control whether actions queue for review, which approval types auto-run, and whether outbound connector sends need explicit confirmation.
|
|
337
|
+
</p>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
342
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-black/20 px-4 py-4">
|
|
343
|
+
<div className="flex items-center justify-between gap-4">
|
|
344
|
+
<div>
|
|
345
|
+
<div className="text-[12px] font-600 text-text-2">Platform Approvals</div>
|
|
346
|
+
<p className="text-[11px] text-text-3/60 mt-1 leading-relaxed">
|
|
347
|
+
Turn this off to auto-approve workflow approvals across the app. Audit records are still kept.
|
|
348
|
+
</p>
|
|
349
|
+
</div>
|
|
350
|
+
<button
|
|
351
|
+
type="button"
|
|
352
|
+
disabled={savingSetting === 'approvalsEnabled'}
|
|
353
|
+
onClick={() => {
|
|
354
|
+
const next = !approvalsEnabled
|
|
355
|
+
void saveApprovalSettings(
|
|
356
|
+
{ approvalsEnabled: next },
|
|
357
|
+
next ? 'Platform approvals enabled' : 'Platform approvals disabled',
|
|
358
|
+
'approvalsEnabled',
|
|
359
|
+
)
|
|
360
|
+
}}
|
|
361
|
+
className={`inline-flex h-[22px] w-10 shrink-0 items-center rounded-full border border-white/[0.08] p-[3px] transition-colors duration-200 cursor-pointer disabled:opacity-50 ${
|
|
362
|
+
approvalsEnabled ? 'justify-end bg-accent' : 'justify-start bg-white/[0.16]'
|
|
363
|
+
}`}
|
|
364
|
+
aria-label="Toggle platform approvals"
|
|
365
|
+
>
|
|
366
|
+
<span className="h-4 w-4 rounded-full bg-white shadow-[0_1px_4px_rgba(0,0,0,0.35)]" />
|
|
367
|
+
</button>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-black/20 px-4 py-4">
|
|
372
|
+
<div className="flex items-center justify-between gap-4">
|
|
373
|
+
<div>
|
|
374
|
+
<div className="text-[12px] font-600 text-text-2">Outbound Send Approvals</div>
|
|
375
|
+
<p className="text-[11px] text-text-3/60 mt-1 leading-relaxed">
|
|
376
|
+
Require explicit approval before agents send messages or media over connectors.
|
|
377
|
+
</p>
|
|
378
|
+
</div>
|
|
379
|
+
<button
|
|
380
|
+
type="button"
|
|
381
|
+
disabled={savingSetting === 'safetyRequireApprovalForOutbound'}
|
|
382
|
+
onClick={() => {
|
|
383
|
+
const next = !outboundApprovalEnabled
|
|
384
|
+
void saveApprovalSettings(
|
|
385
|
+
{ safetyRequireApprovalForOutbound: next },
|
|
386
|
+
next ? 'Outbound send approvals enabled' : 'Outbound send approvals disabled',
|
|
387
|
+
'safetyRequireApprovalForOutbound',
|
|
388
|
+
)
|
|
389
|
+
}}
|
|
390
|
+
className={`inline-flex h-[22px] w-10 shrink-0 items-center rounded-full border border-white/[0.08] p-[3px] transition-colors duration-200 cursor-pointer disabled:opacity-50 ${
|
|
391
|
+
outboundApprovalEnabled ? 'justify-end bg-accent' : 'justify-start bg-white/[0.16]'
|
|
392
|
+
}`}
|
|
393
|
+
aria-label="Toggle outbound send approvals"
|
|
394
|
+
>
|
|
395
|
+
<span className="h-4 w-4 rounded-full bg-white shadow-[0_1px_4px_rgba(0,0,0,0.35)]" />
|
|
396
|
+
</button>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
<div>
|
|
402
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mb-3">
|
|
403
|
+
<div>
|
|
404
|
+
<div className="text-[12px] font-600 text-text-2">Auto-Approve Categories</div>
|
|
405
|
+
<p className="text-[11px] text-text-3/60 mt-1">
|
|
406
|
+
Keep the approval system on, but let low-friction request types flow through automatically.
|
|
407
|
+
</p>
|
|
408
|
+
</div>
|
|
409
|
+
<div className="text-[11px] text-text-3/60">
|
|
410
|
+
{autoApproveEnabledCount} enabled
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-2">
|
|
414
|
+
{AUTO_APPROVE_OPTIONS.map((option) => {
|
|
415
|
+
const checked = autoApproved.has(option.id)
|
|
416
|
+
return (
|
|
417
|
+
<label
|
|
418
|
+
key={option.id}
|
|
419
|
+
className={`rounded-[14px] border px-3 py-3 cursor-pointer transition-all ${
|
|
420
|
+
checked
|
|
421
|
+
? 'border-accent-bright/30 bg-accent-soft/60'
|
|
422
|
+
: 'border-white/[0.06] bg-black/20 hover:bg-white/[0.04]'
|
|
423
|
+
}`}
|
|
424
|
+
>
|
|
425
|
+
<div className="flex items-start gap-3">
|
|
426
|
+
<input
|
|
427
|
+
type="checkbox"
|
|
428
|
+
checked={checked}
|
|
429
|
+
disabled={savingSetting === `auto:${option.id}`}
|
|
430
|
+
onChange={(e) => {
|
|
431
|
+
const next = new Set(appSettings.approvalAutoApproveCategories || [])
|
|
432
|
+
if (e.target.checked) next.add(option.id)
|
|
433
|
+
else next.delete(option.id)
|
|
434
|
+
void saveApprovalSettings(
|
|
435
|
+
{ approvalAutoApproveCategories: [...next] },
|
|
436
|
+
checked ? `${option.label} now requires approval` : `${option.label} will auto-approve`,
|
|
437
|
+
`auto:${option.id}`,
|
|
438
|
+
)
|
|
439
|
+
}}
|
|
440
|
+
className="mt-0.5"
|
|
441
|
+
/>
|
|
442
|
+
<div>
|
|
443
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
444
|
+
<div className="text-[12px] font-600 text-text-2">{option.label}</div>
|
|
445
|
+
{option.risk && (
|
|
446
|
+
<span className={`px-1.5 py-0.5 rounded-full text-[9px] font-700 uppercase tracking-[0.08em] ${
|
|
447
|
+
option.risk === 'very-high'
|
|
448
|
+
? 'bg-red-500/10 text-red-300 border border-red-500/20'
|
|
449
|
+
: 'bg-amber-500/10 text-amber-300 border border-amber-500/20'
|
|
450
|
+
}`}>
|
|
451
|
+
{option.risk === 'very-high' ? 'Very high risk' : 'High risk'}
|
|
452
|
+
</span>
|
|
453
|
+
)}
|
|
454
|
+
</div>
|
|
455
|
+
<p className="text-[11px] text-text-3/60 mt-1 leading-relaxed">{option.description}</p>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
</label>
|
|
459
|
+
)
|
|
460
|
+
})}
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<div className="rounded-[18px] border border-white/[0.06] bg-white/[0.02] p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]">
|
|
468
|
+
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
|
469
|
+
<div className="flex-1">
|
|
470
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/60 mb-2">Queue Filters</div>
|
|
471
|
+
<div className="flex flex-wrap gap-2">
|
|
243
472
|
{([
|
|
244
473
|
['all', `All (${pendingCount})`],
|
|
245
474
|
['execution', `Execution (${sortedExecApprovals.length})`],
|
|
@@ -259,10 +488,11 @@ export function ApprovalsPanel() {
|
|
|
259
488
|
{label}
|
|
260
489
|
</button>
|
|
261
490
|
))}
|
|
491
|
+
</div>
|
|
262
492
|
</div>
|
|
263
493
|
|
|
264
|
-
<div className="flex items-center gap-2">
|
|
265
|
-
<div className="text-[11px] text-text-3/60 font-600">
|
|
494
|
+
<div className="flex items-center gap-2 justify-between lg:justify-end">
|
|
495
|
+
<div className="text-[11px] text-text-3/60 font-600 whitespace-nowrap">
|
|
266
496
|
Showing {visibleCount} of {pendingCount}
|
|
267
497
|
</div>
|
|
268
498
|
{workflowCategories.length > 1 && scope !== 'execution' && (
|
|
@@ -283,7 +513,7 @@ export function ApprovalsPanel() {
|
|
|
283
513
|
</div>
|
|
284
514
|
</div>
|
|
285
515
|
|
|
286
|
-
<div className="mt-
|
|
516
|
+
<div className="mt-1">
|
|
287
517
|
<input
|
|
288
518
|
value={search}
|
|
289
519
|
onChange={(e) => setSearch(e.target.value)}
|
|
@@ -295,7 +525,7 @@ export function ApprovalsPanel() {
|
|
|
295
525
|
</div>
|
|
296
526
|
|
|
297
527
|
{filteredExecApprovals.length > 0 && (
|
|
298
|
-
<div
|
|
528
|
+
<div>
|
|
299
529
|
<h2 className="text-[12px] font-700 uppercase tracking-[0.1em] text-amber-400/90 mb-2">Execution Approvals</h2>
|
|
300
530
|
<div className="grid grid-cols-1 gap-3">
|
|
301
531
|
{filteredExecApprovals.map((approval) => (
|
|
@@ -406,11 +636,36 @@ export function ApprovalsPanel() {
|
|
|
406
636
|
)}
|
|
407
637
|
|
|
408
638
|
{visibleCount === 0 && pendingCount > 0 && (
|
|
409
|
-
<div className="rounded-[
|
|
639
|
+
<div className="rounded-[18px] border border-dashed border-white/[0.08] bg-white/[0.015] px-6 py-10 text-center">
|
|
410
640
|
<p className="text-[13px] font-600 text-text-2 mb-1">No approvals match the current filters</p>
|
|
411
641
|
<p className="text-[12px] text-text-3/60">Try clearing the search or switching the queue scope.</p>
|
|
412
642
|
</div>
|
|
413
643
|
)}
|
|
644
|
+
|
|
645
|
+
{pendingCount === 0 && (
|
|
646
|
+
<div className="rounded-[20px] border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]">
|
|
647
|
+
<div className="w-16 h-16 rounded-[24px] bg-white/[0.02] border border-white/[0.04] flex items-center justify-center mx-auto mb-6">
|
|
648
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/40">
|
|
649
|
+
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/>
|
|
650
|
+
<path d="m9 12 2 2 4-4"/>
|
|
651
|
+
</svg>
|
|
652
|
+
</div>
|
|
653
|
+
<h2 className="font-display text-[20px] font-600 text-text-2 mb-2">No pending approvals</h2>
|
|
654
|
+
<p className="text-[13px] text-text-3/60 max-w-[420px] mx-auto">
|
|
655
|
+
{approvalsEnabled
|
|
656
|
+
? 'Your swarm is operating autonomously. Actions requiring oversight will appear here.'
|
|
657
|
+
: 'Approvals are currently disabled, so eligible requests will auto-run instead of queuing here.'}
|
|
658
|
+
</p>
|
|
659
|
+
<div className={`inline-flex items-center gap-2 mt-5 px-3 py-1.5 rounded-full border text-[11px] font-700 ${
|
|
660
|
+
approvalsEnabled
|
|
661
|
+
? 'bg-amber-500/10 border-amber-500/20 text-amber-300'
|
|
662
|
+
: 'bg-emerald-500/10 border-emerald-500/20 text-emerald-300'
|
|
663
|
+
}`}>
|
|
664
|
+
<span className={`w-2 h-2 rounded-full ${approvalsEnabled ? 'bg-amber-300' : 'bg-emerald-300'}`} />
|
|
665
|
+
{approvalsEnabled ? 'Manual approvals active' : 'Workflow approvals auto-run'}
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
)}
|
|
414
669
|
</div>
|
|
415
670
|
</div>
|
|
416
671
|
)
|