@adens/openwa 0.1.4 → 0.1.6
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/adens-openwa-0.1.5.tgz +0 -0
- package/package.json +3 -1
- package/server/index.js +30 -12
- package/web/.next/BUILD_ID +1 -0
- package/web/.next/build-manifest.json +43 -0
- package/web/.next/cache/.previewinfo +1 -0
- package/web/.next/cache/.rscinfo +1 -0
- package/web/.next/cache/webpack/client-production/0.pack +0 -0
- package/web/.next/cache/webpack/client-production/1.pack +0 -0
- package/web/.next/cache/webpack/client-production/10.pack +0 -0
- package/web/.next/cache/webpack/client-production/11.pack +0 -0
- package/web/.next/cache/webpack/client-production/12.pack +0 -0
- package/web/.next/cache/webpack/client-production/13.pack +0 -0
- package/web/.next/cache/webpack/client-production/14.pack +0 -0
- package/web/.next/cache/webpack/client-production/15.pack +0 -0
- package/web/.next/cache/webpack/client-production/16.pack +0 -0
- package/web/.next/cache/webpack/client-production/17.pack +0 -0
- package/web/.next/cache/webpack/client-production/18.pack +0 -0
- package/web/.next/cache/webpack/client-production/19.pack +0 -0
- package/web/.next/cache/webpack/client-production/2.pack +0 -0
- package/web/.next/cache/webpack/client-production/20.pack +0 -0
- package/web/.next/cache/webpack/client-production/21.pack +0 -0
- package/web/.next/cache/webpack/client-production/22.pack +0 -0
- package/web/.next/cache/webpack/client-production/23.pack +0 -0
- package/web/.next/cache/webpack/client-production/24.pack +0 -0
- package/web/.next/cache/webpack/client-production/25.pack +0 -0
- package/web/.next/cache/webpack/client-production/26.pack +0 -0
- package/web/.next/cache/webpack/client-production/27.pack +0 -0
- package/web/.next/cache/webpack/client-production/3.pack +0 -0
- package/web/.next/cache/webpack/client-production/4.pack +0 -0
- package/web/.next/cache/webpack/client-production/5.pack +0 -0
- package/web/.next/cache/webpack/client-production/6.pack +0 -0
- package/web/.next/cache/webpack/client-production/7.pack +0 -0
- package/web/.next/cache/webpack/client-production/8.pack +0 -0
- package/web/.next/cache/webpack/client-production/9.pack +0 -0
- package/web/.next/cache/webpack/client-production/index.pack +0 -0
- package/web/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/web/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/web/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/web/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
- package/web/.next/cache/webpack/server-production/0.pack +0 -0
- package/web/.next/cache/webpack/server-production/1.pack +0 -0
- package/web/.next/cache/webpack/server-production/10.pack +0 -0
- package/web/.next/cache/webpack/server-production/11.pack +0 -0
- package/web/.next/cache/webpack/server-production/12.pack +0 -0
- package/web/.next/cache/webpack/server-production/13.pack +0 -0
- package/web/.next/cache/webpack/server-production/14.pack +0 -0
- package/web/.next/cache/webpack/server-production/15.pack +0 -0
- package/web/.next/cache/webpack/server-production/16.pack +0 -0
- package/web/.next/cache/webpack/server-production/17.pack +0 -0
- package/web/.next/cache/webpack/server-production/18.pack +0 -0
- package/web/.next/cache/webpack/server-production/19.pack +0 -0
- package/web/.next/cache/webpack/server-production/2.pack +0 -0
- package/web/.next/cache/webpack/server-production/20.pack +0 -0
- package/web/.next/cache/webpack/server-production/3.pack +0 -0
- package/web/.next/cache/webpack/server-production/4.pack +0 -0
- package/web/.next/cache/webpack/server-production/5.pack +0 -0
- package/web/.next/cache/webpack/server-production/6.pack +0 -0
- package/web/.next/cache/webpack/server-production/7.pack +0 -0
- package/web/.next/cache/webpack/server-production/8.pack +0 -0
- package/web/.next/cache/webpack/server-production/9.pack +0 -0
- package/web/.next/cache/webpack/server-production/index.pack +0 -0
- package/web/.next/cache/webpack/server-production/index.pack.old +0 -0
- package/web/.next/diagnostics/build-diagnostics.json +6 -0
- package/web/.next/diagnostics/framework.json +1 -0
- package/web/.next/dynamic-css-manifest.json +1 -0
- package/web/.next/export-marker.json +6 -0
- package/web/.next/images-manifest.json +58 -0
- package/web/.next/next-minimal-server.js.nft.json +1 -0
- package/web/.next/next-server.js.nft.json +1 -0
- package/web/.next/package.json +1 -0
- package/web/.next/prerender-manifest.json +11 -0
- package/web/.next/react-loadable-manifest.json +14 -0
- package/web/.next/required-server-files.json +343 -0
- package/web/.next/routes-manifest.json +79 -0
- package/web/.next/server/chunks/276.js +19 -0
- package/web/.next/server/chunks/508.js +1 -0
- package/web/.next/server/chunks/891.js +6 -0
- package/web/.next/server/dynamic-css-manifest.js +1 -0
- package/web/.next/server/functions-config-manifest.json +4 -0
- package/web/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/web/.next/server/middleware-build-manifest.js +1 -0
- package/web/.next/server/middleware-manifest.json +6 -0
- package/web/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/web/.next/server/next-font-manifest.js +1 -0
- package/web/.next/server/next-font-manifest.json +1 -0
- package/web/.next/server/pages/404.html +1 -0
- package/web/.next/server/pages/500.html +1 -0
- package/web/.next/server/pages/_app.js +1 -0
- package/web/.next/server/pages/_app.js.nft.json +1 -0
- package/web/.next/server/pages/_document.js +1 -0
- package/web/.next/server/pages/_document.js.nft.json +1 -0
- package/web/.next/server/pages/_error.js +1 -0
- package/web/.next/server/pages/_error.js.nft.json +1 -0
- package/web/.next/server/pages/dashboard.html +1 -0
- package/web/.next/server/pages/dashboard.js.nft.json +1 -0
- package/web/.next/server/pages/index.html +1 -0
- package/web/.next/server/pages/index.js.nft.json +1 -0
- package/web/.next/server/pages-manifest.json +8 -0
- package/web/.next/server/webpack-runtime.js +1 -0
- package/web/.next/static/FqMH7OdKFqTxzPemOh9_6/_buildManifest.js +1 -0
- package/web/.next/static/FqMH7OdKFqTxzPemOh9_6/_ssgManifest.js +1 -0
- package/web/.next/static/chunks/112-1bb8aeaa7c3fd57f.js +1 -0
- package/web/.next/static/chunks/131.2b0db2ec21a47401.js +1 -0
- package/web/.next/static/chunks/1d0474cf-ced9eb4c00bbcd99.js +1 -0
- package/web/.next/static/chunks/750.caaf5a9ddd46b267.js +1 -0
- package/web/.next/static/chunks/framework-9ad035430eed8b2b.js +1 -0
- package/web/.next/static/chunks/main-a5ca0156a62da1bd.js +1 -0
- package/web/.next/static/chunks/pages/_app-37894446f6ae5afe.js +1 -0
- package/web/.next/static/chunks/pages/_error-8e5b843ec9d413fc.js +1 -0
- package/web/.next/static/chunks/pages/dashboard-ce0d55d3fea9e308.js +1 -0
- package/web/.next/static/chunks/pages/index-8d94234d8de13682.js +1 -0
- package/web/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/web/.next/static/chunks/webpack-ea849f90a4c65253.js +1 -0
- package/web/.next/static/css/de4e0852dfebd52f.css +3 -0
- package/web/.next/trace +2 -0
- package/web/.next/types/routes.d.ts +62 -0
- package/web/.next/types/validator.ts +16 -0
- package/web/components/AppHead.js +0 -14
- package/web/components/AuthCard.js +0 -170
- package/web/components/BrandLogo.js +0 -11
- package/web/components/ChatWindow.js +0 -875
- package/web/components/ChatWindow.js.tmp +0 -0
- package/web/components/ContactList.js +0 -97
- package/web/components/ContactsPanel.js +0 -90
- package/web/components/EmojiPicker.js +0 -108
- package/web/components/MediaPreviewModal.js +0 -146
- package/web/components/MessageActionMenu.js +0 -155
- package/web/components/SessionSidebar.js +0 -167
- package/web/components/SettingsModal.js +0 -266
- package/web/components/Skeletons.js +0 -73
- package/web/jsconfig.json +0 -10
- package/web/lib/api.js +0 -33
- package/web/lib/socket.js +0 -9
- package/web/pages/_app.js +0 -5
- package/web/pages/dashboard.js +0 -541
- package/web/pages/index.js +0 -62
- package/web/postcss.config.js +0 -10
- package/web/store/useAppStore.js +0 -209
- package/web/styles/globals.css +0 -52
- package/web/tailwind.config.js +0 -36
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { BrandLogo } from "@/components/BrandLogo";
|
|
2
|
-
|
|
3
|
-
function SessionStatusBadge({ status }) {
|
|
4
|
-
const colors = {
|
|
5
|
-
ready: "bg-brand-500/15 text-brand-100 ring-1 ring-brand-400/20",
|
|
6
|
-
connecting: "bg-amber-500/15 text-amber-100 ring-1 ring-amber-400/20",
|
|
7
|
-
disconnected: "bg-white/8 text-white/60 ring-1 ring-white/10",
|
|
8
|
-
error: "bg-red-500/15 text-red-100 ring-1 ring-red-400/20"
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
return (
|
|
12
|
-
<span className={`rounded-full px-2.5 py-1 text-[11px] font-medium capitalize tracking-[0.08em] ${colors[status] || colors.disconnected}`}>
|
|
13
|
-
{status}
|
|
14
|
-
</span>
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function initials(label) {
|
|
19
|
-
return String(label || "?")
|
|
20
|
-
.split(" ")
|
|
21
|
-
.map((part) => part[0])
|
|
22
|
-
.join("")
|
|
23
|
-
.slice(0, 2)
|
|
24
|
-
.toUpperCase();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function SessionAvatar({ label }) {
|
|
28
|
-
return <div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-[#202c33] text-sm font-semibold text-white">{initials(label)}</div>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function SessionSidebar({
|
|
32
|
-
sessions,
|
|
33
|
-
activeSessionId,
|
|
34
|
-
onSelect,
|
|
35
|
-
onConnect,
|
|
36
|
-
onDisconnect,
|
|
37
|
-
sessionName,
|
|
38
|
-
sessionPhone,
|
|
39
|
-
onSessionNameChange,
|
|
40
|
-
onSessionPhoneChange,
|
|
41
|
-
onCreateSession
|
|
42
|
-
}) {
|
|
43
|
-
const activeSession = sessions.find((session) => session.id === activeSessionId) || sessions[0] || null;
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<aside className="flex w-[330px] shrink-0 flex-col border-r border-white/6 bg-[#0b141a]">
|
|
47
|
-
<div className="border-b border-white/6 px-5 py-5">
|
|
48
|
-
<p className="text-[11px] uppercase tracking-[0.28em] text-brand-100/60">Workspace</p>
|
|
49
|
-
<div className="mt-3 flex items-center gap-3">
|
|
50
|
-
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-white p-2 shadow-[0_16px_40px_rgba(0,0,0,0.18)]">
|
|
51
|
-
<BrandLogo variant="square" alt="OpenWA" className="h-full w-full rounded-xl" />
|
|
52
|
-
</div>
|
|
53
|
-
<div>
|
|
54
|
-
<h2 className="text-lg font-semibold text-white">OpenWA Devices</h2>
|
|
55
|
-
<p className="text-sm text-white/45">Manage multiple numbers in one dashboard.</p>
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
<div className="flex-1 overflow-y-auto px-4 py-4">
|
|
61
|
-
<div className="space-y-2.5">
|
|
62
|
-
{sessions.map((session) => (
|
|
63
|
-
<button
|
|
64
|
-
key={session.id}
|
|
65
|
-
type="button"
|
|
66
|
-
className={`w-full rounded-[26px] border px-4 py-3.5 text-left transition ${
|
|
67
|
-
session.id === activeSessionId
|
|
68
|
-
? "border-brand-500/40 bg-brand-500/10 shadow-panel"
|
|
69
|
-
: "border-white/6 bg-white/[0.03] hover:border-white/12 hover:bg-white/[0.05]"
|
|
70
|
-
}`}
|
|
71
|
-
onClick={() => onSelect(session.id)}
|
|
72
|
-
>
|
|
73
|
-
<div className="flex items-start gap-3">
|
|
74
|
-
<SessionAvatar label={session.name} />
|
|
75
|
-
<div className="min-w-0 flex-1">
|
|
76
|
-
<div className="flex items-center justify-between gap-3">
|
|
77
|
-
<h3 className="truncate font-medium text-white">{session.name}</h3>
|
|
78
|
-
<SessionStatusBadge status={session.status} />
|
|
79
|
-
</div>
|
|
80
|
-
<p className="mt-1 truncate text-sm text-white/45">{session.phoneNumber || "Number will appear after device connects"}</p>
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
83
|
-
</button>
|
|
84
|
-
))}
|
|
85
|
-
|
|
86
|
-
{sessions.length === 0 ? (
|
|
87
|
-
<div className="rounded-[26px] border border-dashed border-white/10 bg-white/[0.03] px-4 py-7 text-sm leading-6 text-white/45">
|
|
88
|
-
No active sessions. Add a new device to start building your OpenWA workspace.
|
|
89
|
-
</div>
|
|
90
|
-
) : null}
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
|
|
94
|
-
<div className="border-t border-white/6 px-5 py-5">
|
|
95
|
-
{activeSession ? (
|
|
96
|
-
<div className="mb-4 rounded-[28px] border border-white/8 bg-gradient-to-b from-white/[0.06] to-white/[0.03] p-4 shadow-panel">
|
|
97
|
-
<div className="mb-4 flex items-center justify-between gap-3">
|
|
98
|
-
<div>
|
|
99
|
-
<h3 className="font-semibold text-white">{activeSession.name}</h3>
|
|
100
|
-
<p className="text-sm text-white/45">{activeSession.phoneNumber || "Waiting for WhatsApp pairing"}</p>
|
|
101
|
-
<p className="mt-1 text-xs uppercase tracking-[0.18em] text-white/30">
|
|
102
|
-
Transport: {activeSession.transportType === "mock" ? "Mock" : "WhatsApp Web"}
|
|
103
|
-
</p>
|
|
104
|
-
</div>
|
|
105
|
-
<SessionStatusBadge status={activeSession.status} />
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
{activeSession.qrCode ? (
|
|
109
|
-
<div className="rounded-[24px] bg-white p-3">
|
|
110
|
-
<img src={activeSession.qrCode} alt="QR Code" className="mx-auto h-40 w-40 rounded-2xl" />
|
|
111
|
-
</div>
|
|
112
|
-
) : (
|
|
113
|
-
<div className="rounded-[24px] border border-dashed border-white/10 bg-[#111b21] px-4 py-10 text-center text-sm leading-6 text-white/45">
|
|
114
|
-
QR code will appear here when session starts pairing.
|
|
115
|
-
</div>
|
|
116
|
-
)}
|
|
117
|
-
|
|
118
|
-
{activeSession.lastError ? (
|
|
119
|
-
<div className="mt-3 rounded-2xl border border-red-400/20 bg-red-500/10 px-3 py-2.5 text-sm leading-6 text-red-100">
|
|
120
|
-
{activeSession.lastError}
|
|
121
|
-
</div>
|
|
122
|
-
) : null}
|
|
123
|
-
|
|
124
|
-
<div className="mt-4 grid grid-cols-2 gap-2">
|
|
125
|
-
<button
|
|
126
|
-
type="button"
|
|
127
|
-
className="rounded-2xl bg-brand-500 px-4 py-3 text-sm font-semibold text-[#10251a] transition hover:bg-brand-600"
|
|
128
|
-
onClick={() => onConnect(activeSession.id)}
|
|
129
|
-
>
|
|
130
|
-
Connect
|
|
131
|
-
</button>
|
|
132
|
-
<button
|
|
133
|
-
type="button"
|
|
134
|
-
className="rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm font-medium text-white/80 transition hover:bg-white/[0.06]"
|
|
135
|
-
onClick={() => onDisconnect(activeSession.id)}
|
|
136
|
-
>
|
|
137
|
-
Disconnect
|
|
138
|
-
</button>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
) : null}
|
|
142
|
-
|
|
143
|
-
<form className="space-y-3" onSubmit={onCreateSession}>
|
|
144
|
-
<div>
|
|
145
|
-
<p className="mb-2 text-[11px] uppercase tracking-[0.26em] text-white/35">Add device</p>
|
|
146
|
-
<input
|
|
147
|
-
className="w-full rounded-2xl border border-white/10 bg-[#202c33] px-4 py-3 text-sm text-white outline-none transition placeholder:text-white/30 focus:border-brand-500"
|
|
148
|
-
placeholder="Session name, e.g. Sales Team"
|
|
149
|
-
value={sessionName}
|
|
150
|
-
onChange={(event) => onSessionNameChange(event.target.value)}
|
|
151
|
-
required
|
|
152
|
-
/>
|
|
153
|
-
</div>
|
|
154
|
-
<input
|
|
155
|
-
className="w-full rounded-2xl border border-white/10 bg-[#202c33] px-4 py-3 text-sm text-white outline-none transition placeholder:text-white/30 focus:border-brand-500"
|
|
156
|
-
placeholder="Nomor WhatsApp (opsional)"
|
|
157
|
-
value={sessionPhone}
|
|
158
|
-
onChange={(event) => onSessionPhoneChange(event.target.value)}
|
|
159
|
-
/>
|
|
160
|
-
<button type="submit" className="w-full rounded-2xl border border-brand-500/30 bg-brand-500/10 px-4 py-3 text-sm font-semibold text-brand-100 transition hover:bg-brand-500/20">
|
|
161
|
-
Add WhatsApp Session
|
|
162
|
-
</button>
|
|
163
|
-
</form>
|
|
164
|
-
</div>
|
|
165
|
-
</aside>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { BrandLogo } from "@/components/BrandLogo";
|
|
3
|
-
|
|
4
|
-
function SessionStatusBadge({ status }) {
|
|
5
|
-
const colors = {
|
|
6
|
-
ready: "bg-brand-500/15 text-brand-100 ring-1 ring-brand-400/20",
|
|
7
|
-
connecting: "bg-amber-500/15 text-amber-100 ring-1 ring-amber-400/20",
|
|
8
|
-
disconnected: "bg-white/8 text-white/60 ring-1 ring-white/10",
|
|
9
|
-
error: "bg-red-500/15 text-red-100 ring-1 ring-red-400/20"
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<span className={`rounded-full px-2.5 py-1 text-[11px] font-medium capitalize tracking-[0.08em] ${colors[status] || colors.disconnected}`}>
|
|
14
|
-
{status}
|
|
15
|
-
</span>
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function initials(label) {
|
|
20
|
-
return String(label || "?")
|
|
21
|
-
.split(" ")
|
|
22
|
-
.map((part) => part[0])
|
|
23
|
-
.join("")
|
|
24
|
-
.slice(0, 2)
|
|
25
|
-
.toUpperCase();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function SessionAvatar({ label }) {
|
|
29
|
-
return <div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-[#2e2f2f] text-sm font-semibold text-white">{initials(label)}</div>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function formatDateTime(value) {
|
|
33
|
-
if (!value) {
|
|
34
|
-
return "Never";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return new Intl.DateTimeFormat("en-US", {
|
|
38
|
-
day: "2-digit",
|
|
39
|
-
month: "short",
|
|
40
|
-
year: "numeric",
|
|
41
|
-
hour: "2-digit",
|
|
42
|
-
minute: "2-digit"
|
|
43
|
-
}).format(new Date(value));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function SettingsModal({
|
|
47
|
-
open,
|
|
48
|
-
sessions,
|
|
49
|
-
activeSessionId,
|
|
50
|
-
onClose,
|
|
51
|
-
onSelect,
|
|
52
|
-
onConnect,
|
|
53
|
-
onDisconnect,
|
|
54
|
-
sessionName,
|
|
55
|
-
sessionPhone,
|
|
56
|
-
onSessionNameChange,
|
|
57
|
-
onSessionPhoneChange,
|
|
58
|
-
onCreateSession,
|
|
59
|
-
apiKeys,
|
|
60
|
-
apiKeysLoading,
|
|
61
|
-
apiKeyName,
|
|
62
|
-
apiKeySecret,
|
|
63
|
-
onApiKeyNameChange,
|
|
64
|
-
onCreateApiKey,
|
|
65
|
-
onRevokeApiKey
|
|
66
|
-
}) {
|
|
67
|
-
const [copied, setCopied] = useState(false);
|
|
68
|
-
|
|
69
|
-
if (!open) {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 px-6 py-8 backdrop-blur-sm">
|
|
75
|
-
<div className="flex h-full max-h-[840px] w-full max-w-[1080px] flex-col overflow-hidden rounded-[32px] bg-[#161717] shadow-[0_40px_120px_rgba(0,0,0,0.5)]">
|
|
76
|
-
<div className="flex items-center justify-between px-6 py-5">
|
|
77
|
-
<div className="flex items-center gap-4">
|
|
78
|
-
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-white p-2 shadow-[0_16px_40px_rgba(0,0,0,0.18)]">
|
|
79
|
-
<BrandLogo variant="square" alt="OpenWA" className="h-full w-full rounded-xl" />
|
|
80
|
-
</div>
|
|
81
|
-
<div>
|
|
82
|
-
<p className="text-[11px] uppercase tracking-[0.26em] text-white/35">Settings</p>
|
|
83
|
-
<h2 className="mt-2 text-xl font-semibold text-white">OpenWA Devices</h2>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
<button type="button" className="rounded-full bg-[#2e2f2f] px-4 py-2 text-sm text-white/70 transition hover:bg-[#3a3b3b] hover:text-white" onClick={onClose}>
|
|
87
|
-
Close
|
|
88
|
-
</button>
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
<div className="grid min-h-0 flex-1 gap-0 md:grid-cols-[1.2fr_0.8fr]">
|
|
92
|
-
<div className="min-h-0 px-5 py-5">
|
|
93
|
-
<div className="h-full overflow-y-auto pr-1">
|
|
94
|
-
<div className="space-y-3">
|
|
95
|
-
{sessions.map((session) => (
|
|
96
|
-
<button
|
|
97
|
-
key={session.id}
|
|
98
|
-
type="button"
|
|
99
|
-
className={`w-full rounded-[18px] px-4 py-4 text-left transition ${
|
|
100
|
-
session.id === activeSessionId
|
|
101
|
-
? "bg-[#2e2f2f]"
|
|
102
|
-
: "bg-transparent hover:bg-white/[0.04]"
|
|
103
|
-
}`}
|
|
104
|
-
onClick={() => onSelect(session.id)}
|
|
105
|
-
>
|
|
106
|
-
<div className="flex items-start gap-3">
|
|
107
|
-
<SessionAvatar label={session.name} />
|
|
108
|
-
<div className="min-w-0 flex-1">
|
|
109
|
-
<div className="flex items-center justify-between gap-3">
|
|
110
|
-
<h3 className="truncate font-medium text-white">{session.name}</h3>
|
|
111
|
-
<SessionStatusBadge status={session.status} />
|
|
112
|
-
</div>
|
|
113
|
-
<p className="mt-1 text-sm text-white/45">{session.phoneNumber || "Waiting for WhatsApp pairing"}</p>
|
|
114
|
-
<p className="mt-2 text-xs uppercase tracking-[0.16em] text-white/30">
|
|
115
|
-
Transport: {session.transportType === "mock" ? "Mock" : "WhatsApp Web"}
|
|
116
|
-
</p>
|
|
117
|
-
{session.lastError ? <p className="mt-3 rounded-2xl bg-red-500/10 px-3 py-2 text-sm text-red-100">{session.lastError}</p> : null}
|
|
118
|
-
</div>
|
|
119
|
-
</div>
|
|
120
|
-
|
|
121
|
-
<div className="mt-4 flex gap-2">
|
|
122
|
-
<button
|
|
123
|
-
type="button"
|
|
124
|
-
className="rounded-2xl bg-brand-500 px-4 py-2 text-sm font-semibold text-[#10251a]"
|
|
125
|
-
onClick={(event) => {
|
|
126
|
-
event.stopPropagation();
|
|
127
|
-
onConnect(session.id);
|
|
128
|
-
}}
|
|
129
|
-
>
|
|
130
|
-
Connect
|
|
131
|
-
</button>
|
|
132
|
-
<button
|
|
133
|
-
type="button"
|
|
134
|
-
className="rounded-2xl bg-[#2e2f2f] px-4 py-2 text-sm text-white/75"
|
|
135
|
-
onClick={(event) => {
|
|
136
|
-
event.stopPropagation();
|
|
137
|
-
onDisconnect(session.id);
|
|
138
|
-
}}
|
|
139
|
-
>
|
|
140
|
-
Disconnect
|
|
141
|
-
</button>
|
|
142
|
-
</div>
|
|
143
|
-
</button>
|
|
144
|
-
))}
|
|
145
|
-
</div>
|
|
146
|
-
</div>
|
|
147
|
-
</div>
|
|
148
|
-
|
|
149
|
-
<div className="min-h-0 overflow-y-auto px-6 py-5">
|
|
150
|
-
<div className="rounded-[28px] bg-[#161717] p-4">
|
|
151
|
-
<p className="text-[11px] uppercase tracking-[0.24em] text-white/35">Pairing QR</p>
|
|
152
|
-
{sessions.find((session) => session.id === activeSessionId)?.qrCode ? (
|
|
153
|
-
<div className="mt-4 rounded-[24px] bg-white p-4">
|
|
154
|
-
<img
|
|
155
|
-
src={sessions.find((session) => session.id === activeSessionId)?.qrCode}
|
|
156
|
-
alt="QR Code"
|
|
157
|
-
className="mx-auto h-56 w-56 rounded-2xl"
|
|
158
|
-
/>
|
|
159
|
-
</div>
|
|
160
|
-
) : (
|
|
161
|
-
<div className="mt-4 rounded-[24px] bg-[#2e2f2f] px-4 py-16 text-center text-sm leading-6 text-white/40">
|
|
162
|
-
QR code for pairing will appear here when session is connecting.
|
|
163
|
-
</div>
|
|
164
|
-
)}
|
|
165
|
-
</div>
|
|
166
|
-
|
|
167
|
-
<form className="mt-5 space-y-3 rounded-[28px] bg-[#161717] p-4" onSubmit={onCreateSession}>
|
|
168
|
-
<div>
|
|
169
|
-
<p className="mb-2 text-[11px] uppercase tracking-[0.24em] text-white/35">Add device</p>
|
|
170
|
-
<input
|
|
171
|
-
className="w-full rounded-[22px] bg-[#2e2f2f] px-4 py-3 text-sm text-white outline-none placeholder:text-white/30"
|
|
172
|
-
placeholder="Session name, e.g. Sales Team"
|
|
173
|
-
value={sessionName}
|
|
174
|
-
onChange={(event) => onSessionNameChange(event.target.value)}
|
|
175
|
-
required
|
|
176
|
-
/>
|
|
177
|
-
</div>
|
|
178
|
-
<input
|
|
179
|
-
className="w-full rounded-[22px] bg-[#2e2f2f] px-4 py-3 text-sm text-white outline-none placeholder:text-white/30"
|
|
180
|
-
placeholder="WhatsApp number (optional)"
|
|
181
|
-
value={sessionPhone}
|
|
182
|
-
onChange={(event) => onSessionPhoneChange(event.target.value)}
|
|
183
|
-
/>
|
|
184
|
-
<button type="submit" className="w-full rounded-2xl bg-brand-500 px-4 py-3 text-sm font-semibold text-[#10251a]">
|
|
185
|
-
Add WhatsApp Session
|
|
186
|
-
</button>
|
|
187
|
-
</form>
|
|
188
|
-
|
|
189
|
-
<div className="mt-5 rounded-[28px] bg-[#161717] p-4">
|
|
190
|
-
<div className="flex items-start justify-between gap-4">
|
|
191
|
-
<div>
|
|
192
|
-
<p className="text-[11px] uppercase tracking-[0.24em] text-white/35">API Access</p>
|
|
193
|
-
<h3 className="mt-2 text-base font-semibold text-white">Generate API key</h3>
|
|
194
|
-
<p className="mt-2 text-sm leading-6 text-white/45">Use with external agents via `X-API-Key` header or `Authorization: Bearer <api-key>`.</p>
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
|
|
198
|
-
{apiKeySecret ? (
|
|
199
|
-
<div className="mt-4 rounded-[22px] bg-[#2e2f2f] p-4">
|
|
200
|
-
<p className="text-[11px] uppercase tracking-[0.22em] text-brand-200/80">Shown once</p>
|
|
201
|
-
<p className="mt-2 break-all font-mono text-sm text-white">{apiKeySecret}</p>
|
|
202
|
-
<button
|
|
203
|
-
type="button"
|
|
204
|
-
className="mt-3 rounded-full bg-brand-500 px-4 py-2 text-sm font-semibold text-[#10251a]"
|
|
205
|
-
onClick={async () => {
|
|
206
|
-
await navigator.clipboard.writeText(apiKeySecret);
|
|
207
|
-
setCopied(true);
|
|
208
|
-
setTimeout(() => setCopied(false), 1500);
|
|
209
|
-
}}
|
|
210
|
-
>
|
|
211
|
-
{copied ? "Copied" : "Copy API key"}
|
|
212
|
-
</button>
|
|
213
|
-
</div>
|
|
214
|
-
) : null}
|
|
215
|
-
|
|
216
|
-
<form className="mt-4 flex gap-2" onSubmit={onCreateApiKey}>
|
|
217
|
-
<input
|
|
218
|
-
className="w-full rounded-[22px] bg-[#2e2f2f] px-4 py-3 text-sm text-white outline-none placeholder:text-white/30"
|
|
219
|
-
placeholder="Key name, e.g. OpenClaw Agent"
|
|
220
|
-
value={apiKeyName}
|
|
221
|
-
onChange={(event) => onApiKeyNameChange(event.target.value)}
|
|
222
|
-
required
|
|
223
|
-
/>
|
|
224
|
-
<button type="submit" className="shrink-0 rounded-[22px] bg-brand-500 px-4 py-3 text-sm font-semibold text-[#10251a]">
|
|
225
|
-
Generate
|
|
226
|
-
</button>
|
|
227
|
-
</form>
|
|
228
|
-
|
|
229
|
-
<div className="mt-4 max-h-[260px] space-y-3 overflow-y-auto pr-1">
|
|
230
|
-
{apiKeysLoading ? <div className="rounded-[22px] bg-[#2e2f2f] px-4 py-6 text-sm text-white/45">Loading API keys...</div> : null}
|
|
231
|
-
|
|
232
|
-
{!apiKeysLoading && !apiKeys.length ? (
|
|
233
|
-
<div className="rounded-[22px] bg-[#2e2f2f] px-4 py-6 text-sm leading-6 text-white/45">
|
|
234
|
-
No API keys yet. Create one for OpenAPI client, AI agents, or external integrations.
|
|
235
|
-
</div>
|
|
236
|
-
) : null}
|
|
237
|
-
|
|
238
|
-
{apiKeys.map((apiKey) => (
|
|
239
|
-
<div key={apiKey.id} className="rounded-[22px] bg-[#2e2f2f] px-4 py-4">
|
|
240
|
-
<div className="flex items-start justify-between gap-3">
|
|
241
|
-
<div className="min-w-0">
|
|
242
|
-
<h4 className="truncate text-sm font-semibold text-white">{apiKey.name}</h4>
|
|
243
|
-
<p className="mt-1 font-mono text-xs text-white/55">{apiKey.maskedKey}</p>
|
|
244
|
-
</div>
|
|
245
|
-
<button
|
|
246
|
-
type="button"
|
|
247
|
-
className="rounded-full bg-white/5 px-3 py-1.5 text-xs font-medium text-red-200 transition hover:bg-red-500/15"
|
|
248
|
-
onClick={() => onRevokeApiKey(apiKey.id)}
|
|
249
|
-
>
|
|
250
|
-
Revoke
|
|
251
|
-
</button>
|
|
252
|
-
</div>
|
|
253
|
-
<div className="mt-3 grid gap-2 text-xs text-white/40">
|
|
254
|
-
<p>Created: {formatDateTime(apiKey.createdAt)}</p>
|
|
255
|
-
<p>Last used: {formatDateTime(apiKey.lastUsedAt)}</p>
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
))}
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
</div>
|
|
262
|
-
</div>
|
|
263
|
-
</div>
|
|
264
|
-
</div>
|
|
265
|
-
);
|
|
266
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
export function MessageSkeleton() {
|
|
2
|
-
return (
|
|
3
|
-
<div className="flex justify-start">
|
|
4
|
-
<div className="max-w-[72%] space-y-2 rounded-[18px] bg-[#2e2f2f] px-4 py-3">
|
|
5
|
-
<div className="h-4 w-48 animate-pulse rounded bg-white/10" />
|
|
6
|
-
<div className="h-4 w-40 animate-pulse rounded bg-white/10" />
|
|
7
|
-
<div className="mt-3 flex justify-between">
|
|
8
|
-
<div className="h-3 w-12 animate-pulse rounded bg-white/5" />
|
|
9
|
-
<div className="h-3 w-8 animate-pulse rounded bg-white/5" />
|
|
10
|
-
</div>
|
|
11
|
-
</div>
|
|
12
|
-
</div>
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function MessagesSkeletonList() {
|
|
17
|
-
return (
|
|
18
|
-
<div className="space-y-3">
|
|
19
|
-
{Array.from({ length: 5 }).map((_, i) => (
|
|
20
|
-
<MessageSkeleton key={i} />
|
|
21
|
-
))}
|
|
22
|
-
</div>
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function ConversationSkeleton() {
|
|
27
|
-
return (
|
|
28
|
-
<div className="flex items-center gap-3 border-b border-white/5 px-3 py-3 transition hover:bg-white/5 cursor-pointer">
|
|
29
|
-
<div className="h-12 w-12 animate-pulse rounded-2xl bg-white/10" />
|
|
30
|
-
<div className="flex-1 min-w-0">
|
|
31
|
-
<div className="h-4 w-32 animate-pulse rounded bg-white/10 mb-2" />
|
|
32
|
-
<div className="h-3 w-48 animate-pulse rounded bg-white/5" />
|
|
33
|
-
</div>
|
|
34
|
-
<div className="h-3 w-10 animate-pulse rounded bg-white/5" />
|
|
35
|
-
</div>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function ConversationsSkeletonList() {
|
|
40
|
-
return (
|
|
41
|
-
<div>
|
|
42
|
-
{Array.from({ length: 8 }).map((_, i) => (
|
|
43
|
-
<ConversationSkeleton key={i} />
|
|
44
|
-
))}
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function ImageGroupSkeleton() {
|
|
50
|
-
return (
|
|
51
|
-
<div className="flex justify-start">
|
|
52
|
-
<div className="max-w-[72%] rounded-[18px] overflow-hidden bg-[#2e2f2f]">
|
|
53
|
-
<div className="grid grid-cols-2 gap-1 p-1">
|
|
54
|
-
{Array.from({ length: 4 }).map((_, i) => (
|
|
55
|
-
<div key={i} className="h-32 w-32 animate-pulse rounded-lg bg-white/10" />
|
|
56
|
-
))}
|
|
57
|
-
</div>
|
|
58
|
-
<div className="px-4 py-3 border-t border-white/10">
|
|
59
|
-
<div className="h-3 w-20 animate-pulse rounded bg-white/5" />
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function SendButtonSpinner() {
|
|
67
|
-
return (
|
|
68
|
-
<svg className="h-5 w-5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
69
|
-
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
70
|
-
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
71
|
-
</svg>
|
|
72
|
-
);
|
|
73
|
-
}
|
package/web/jsconfig.json
DELETED
package/web/lib/api.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export function getApiBaseUrl() {
|
|
2
|
-
return process.env.NEXT_PUBLIC_API_URL || "http://localhost:55222";
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export async function apiFetch(url, options = {}) {
|
|
6
|
-
const target = `${getApiBaseUrl()}${url}`;
|
|
7
|
-
const authHeaders = {
|
|
8
|
-
...(options.token ? { Authorization: `Bearer ${options.token}` } : {}),
|
|
9
|
-
...(options.apiKey ? { "X-API-Key": options.apiKey } : {})
|
|
10
|
-
};
|
|
11
|
-
const response = await fetch(target, {
|
|
12
|
-
method: options.method || "GET",
|
|
13
|
-
headers: options.formData
|
|
14
|
-
? {
|
|
15
|
-
...authHeaders
|
|
16
|
-
}
|
|
17
|
-
: {
|
|
18
|
-
"Content-Type": "application/json",
|
|
19
|
-
...authHeaders
|
|
20
|
-
},
|
|
21
|
-
body: options.formData || (options.body ? JSON.stringify(options.body) : undefined)
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const payload = await response.json().catch(() => ({}));
|
|
25
|
-
|
|
26
|
-
if (!response.ok) {
|
|
27
|
-
const error = new Error(payload.error || "Request failed.");
|
|
28
|
-
error.status = response.status;
|
|
29
|
-
throw error;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return payload;
|
|
33
|
-
}
|
package/web/lib/socket.js
DELETED