wizards 0.4.1 → 0.4.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/apps/runtime/dist/docs/dev/plugins/reloading.md +3 -3
- package/apps/runtime/dist/docs/dev/plugins/runners.md +9 -12
- package/apps/runtime/dist/docs/user/test-and-share.md +22 -58
- package/apps/web/dist/assets/{AboutDialog-CXEpsI6L.js → AboutDialog-CH1rQdia.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-Cc2APUGI.js → ChangelogDialog-78MQLHfA.js} +1 -1
- package/apps/web/dist/assets/{ChatView-B2r4dW_m.js → ChatView-DLJfy69U.js} +1 -1
- package/apps/web/dist/assets/{EditorPage-CNAYlSms.js → EditorPage-BYrZokVk.js} +1 -1
- package/apps/web/dist/assets/{FlowPreview-DzU1nGDb.js → FlowPreview-xIWPdOCV.js} +1 -1
- package/apps/web/dist/assets/{Harness-ALQ9Csm6.js → Harness-CsDhNLSW.js} +1 -1
- package/apps/web/dist/assets/{NewWizardChannels-DMPqHkCF.js → NewWizardChannels-JB3V6DCL.js} +1 -1
- package/apps/web/dist/assets/{NewWizardPage-pi053e8N.js → NewWizardPage-Bkh-hjSe.js} +1 -1
- package/apps/web/dist/assets/{OnboardingPage-TYNX6Umd.js → OnboardingPage-Diw1ILjC.js} +1 -1
- package/apps/web/dist/assets/{ProjectPage-CvEkx_Ux.js → ProjectPage-FgoZDzPI.js} +1 -1
- package/apps/web/dist/assets/{PublicRunner-BrivKNHo.js → PublicRunner-ClT6B7t5.js} +1 -1
- package/apps/web/dist/assets/{RunDrawer-3osugizk.js → RunDrawer-DAwTEyjM.js} +1 -1
- package/apps/web/dist/assets/{RunnerView-Ci4_WzVv.js → RunnerView-B_tJroFM.js} +1 -1
- package/apps/web/dist/assets/{SettingsPage-DKntf7-9.js → SettingsPage-BsMN-XQs.js} +1 -1
- package/apps/web/dist/assets/{SetupPage-DZQxEQyk.js → SetupPage-C7FuIpSt.js} +1 -1
- package/apps/web/dist/assets/{SharePage-B9l4KthU.js → SharePage-CbFe83L9.js} +1 -1
- package/apps/web/dist/assets/changelog-mYclku73.js +1 -0
- package/apps/web/dist/assets/{esm-CuqXcPS3.js → esm-BhIeKBJY.js} +1 -1
- package/apps/web/dist/assets/{index-j09nLtEs.js → index-fflYi8P1.js} +3 -3
- package/apps/web/dist/assets/{settings-menu-C7VsRpe9.js → settings-menu-C5Y8SOhG.js} +1 -1
- package/apps/web/dist/index.html +1 -1
- package/package.json +1 -1
- package/apps/web/dist/assets/changelog-CxdXZo3V.js +0 -1
- package/modules/calls/dist/client.css +0 -3
- package/modules/calls/dist/client.js +0 -1
- package/modules/calls/engenty.plugin.json +0 -8
- package/modules/calls/migrations/0001_init.sql +0 -26
- package/modules/calls/package.json +0 -25
- package/modules/calls/src/openai.ts +0 -155
- package/modules/calls/src/plugin.ts +0 -407
- package/modules/calls/src/rules.ts +0 -226
- package/modules/calls/src/schema.ts +0 -39
- package/modules/calls/src/session.ts +0 -353
- package/modules/calls/src/store.ts +0 -124
- package/modules/calls/src/twilio.ts +0 -90
- package/modules/calls/tsconfig.json +0 -10
- package/modules/calls/ui/Settings.tsx +0 -137
- package/modules/calls/ui/app.ts +0 -50
- package/modules/calls/ui/messages.ts +0 -81
- package/modules/calls/ui/parts.tsx +0 -36
- package/modules/calls/ui/plugin.tsx +0 -15
- package/modules/sms/dist/client.css +0 -3
- package/modules/sms/dist/client.js +0 -1
- package/modules/sms/engenty.plugin.json +0 -8
- package/modules/sms/migrations/0001_init.sql +0 -26
- package/modules/sms/package.json +0 -24
- package/modules/sms/src/plugin.ts +0 -329
- package/modules/sms/src/schema.ts +0 -26
- package/modules/sms/src/store.ts +0 -140
- package/modules/sms/src/surface.ts +0 -70
- package/modules/sms/src/twilio.ts +0 -130
- package/modules/sms/tsconfig.json +0 -10
- package/modules/sms/ui/Settings.tsx +0 -113
- package/modules/sms/ui/Share.tsx +0 -60
- package/modules/sms/ui/app.ts +0 -51
- package/modules/sms/ui/messages.ts +0 -70
- package/modules/sms/ui/parts.tsx +0 -36
- package/modules/sms/ui/plugin.tsx +0 -20
- package/modules/whatsapp/dist/client.css +0 -3
- package/modules/whatsapp/dist/client.js +0 -1
- package/modules/whatsapp/engenty.plugin.json +0 -8
- package/modules/whatsapp/migrations/0001_init.sql +0 -30
- package/modules/whatsapp/package.json +0 -24
- package/modules/whatsapp/src/graph.ts +0 -271
- package/modules/whatsapp/src/inbound.ts +0 -143
- package/modules/whatsapp/src/plugin.ts +0 -374
- package/modules/whatsapp/src/schema.ts +0 -32
- package/modules/whatsapp/src/store.ts +0 -156
- package/modules/whatsapp/src/surface.ts +0 -127
- package/modules/whatsapp/tsconfig.json +0 -10
- package/modules/whatsapp/ui/Settings.tsx +0 -141
- package/modules/whatsapp/ui/Share.tsx +0 -60
- package/modules/whatsapp/ui/app.ts +0 -54
- package/modules/whatsapp/ui/messages.ts +0 -80
- package/modules/whatsapp/ui/parts.tsx +0 -36
- package/modules/whatsapp/ui/plugin.tsx +0 -20
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
import type { PluginDb } from "@engenty-wizards/plugin-sdk";
|
|
3
|
-
import { eq } from "drizzle-orm";
|
|
4
|
-
import { account, call } from "./schema";
|
|
5
|
-
|
|
6
|
-
/** The plugin's rows: the accounts, the calls. */
|
|
7
|
-
|
|
8
|
-
export interface Account {
|
|
9
|
-
twilioSid: string;
|
|
10
|
-
twilioToken: string;
|
|
11
|
-
openaiProject: string;
|
|
12
|
-
openaiWebhookSecret: string;
|
|
13
|
-
region: "eu" | "us";
|
|
14
|
-
/** The Twilio number the links are texted from; none: no texts. */
|
|
15
|
-
smsFrom: string | null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function getAccount(db: PluginDb): Promise<Account | null> {
|
|
19
|
-
const [row] = await db.select().from(account).where(eq(account.id, 1));
|
|
20
|
-
return row
|
|
21
|
-
? {
|
|
22
|
-
twilioSid: row.twilioSid,
|
|
23
|
-
twilioToken: row.twilioToken,
|
|
24
|
-
openaiProject: row.openaiProject,
|
|
25
|
-
openaiWebhookSecret: row.openaiWebhookSecret,
|
|
26
|
-
region: row.region,
|
|
27
|
-
smsFrom: row.smsFrom,
|
|
28
|
-
}
|
|
29
|
-
: null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface AccountInput {
|
|
33
|
-
twilioSid: string;
|
|
34
|
-
/** Left out: stays as it is. */
|
|
35
|
-
twilioToken?: string;
|
|
36
|
-
openaiProject: string;
|
|
37
|
-
/** Left out: stays as it is. */
|
|
38
|
-
openaiWebhookSecret?: string;
|
|
39
|
-
region: "eu" | "us";
|
|
40
|
-
smsFrom?: string | null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function putAccount(db: PluginDb, input: AccountInput): Promise<Account> {
|
|
44
|
-
const before = await getAccount(db);
|
|
45
|
-
const twilioToken = input.twilioToken || before?.twilioToken;
|
|
46
|
-
const openaiWebhookSecret = input.openaiWebhookSecret || before?.openaiWebhookSecret;
|
|
47
|
-
if (!twilioToken) {
|
|
48
|
-
throw Object.assign(new Error("Twilio's auth token is needed."), {
|
|
49
|
-
status: 400,
|
|
50
|
-
code: "no_token",
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
if (!openaiWebhookSecret) {
|
|
54
|
-
throw Object.assign(new Error("OpenAI's webhook secret is needed."), {
|
|
55
|
-
status: 400,
|
|
56
|
-
code: "no_secret",
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
const next = {
|
|
60
|
-
twilioSid: input.twilioSid,
|
|
61
|
-
twilioToken,
|
|
62
|
-
openaiProject: input.openaiProject,
|
|
63
|
-
openaiWebhookSecret,
|
|
64
|
-
region: input.region,
|
|
65
|
-
smsFrom: input.smsFrom || null,
|
|
66
|
-
updatedAt: new Date(),
|
|
67
|
-
};
|
|
68
|
-
await db
|
|
69
|
-
.insert(account)
|
|
70
|
-
.values({ id: 1, ...next })
|
|
71
|
-
.onConflictDoUpdate({ target: account.id, set: next });
|
|
72
|
-
const { updatedAt: _at, ...saved } = next;
|
|
73
|
-
return saved;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export async function dropAccount(db: PluginDb): Promise<void> {
|
|
77
|
-
await db.delete(account).where(eq(account.id, 1));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ── Calls ───────────────────────────────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
export type CallRow = typeof call.$inferSelect;
|
|
83
|
-
|
|
84
|
-
export async function newCall(
|
|
85
|
-
db: PluginDb,
|
|
86
|
-
input: {
|
|
87
|
-
caller: string;
|
|
88
|
-
channel: "whatsapp" | "phone";
|
|
89
|
-
payload: string | null;
|
|
90
|
-
twilioCallSid: string | null;
|
|
91
|
-
},
|
|
92
|
-
): Promise<CallRow> {
|
|
93
|
-
const now = new Date();
|
|
94
|
-
const row: CallRow = {
|
|
95
|
-
token: randomBytes(18).toString("base64url"),
|
|
96
|
-
caller: input.caller,
|
|
97
|
-
channel: input.channel,
|
|
98
|
-
payload: input.payload,
|
|
99
|
-
twilioCallSid: input.twilioCallSid,
|
|
100
|
-
openaiCallId: null,
|
|
101
|
-
runId: null,
|
|
102
|
-
status: "ringing",
|
|
103
|
-
createdAt: now,
|
|
104
|
-
updatedAt: now,
|
|
105
|
-
};
|
|
106
|
-
await db.insert(call).values(row);
|
|
107
|
-
return row;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export async function callByToken(db: PluginDb, token: string): Promise<CallRow | null> {
|
|
111
|
-
const [row] = await db.select().from(call).where(eq(call.token, token));
|
|
112
|
-
return row ?? null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export async function updateCall(
|
|
116
|
-
db: PluginDb,
|
|
117
|
-
token: string,
|
|
118
|
-
patch: Partial<Pick<CallRow, "openaiCallId" | "runId" | "status">>,
|
|
119
|
-
): Promise<void> {
|
|
120
|
-
await db
|
|
121
|
-
.update(call)
|
|
122
|
-
.set({ ...patch, updatedAt: new Date() })
|
|
123
|
-
.where(eq(call.token, token));
|
|
124
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Twilio carries the call: a SIP domain takes Meta's INVITE (a WhatsApp call), a Twilio number
|
|
5
|
-
* takes a phone call, and either asks this plugin for TwiML. The same account texts the links.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export const API = "https://api.twilio.com/2010-04-01";
|
|
9
|
-
|
|
10
|
-
export interface TwilioAccount {
|
|
11
|
-
sid: string;
|
|
12
|
-
token: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class TwilioError extends Error {
|
|
16
|
-
constructor(
|
|
17
|
-
message: string,
|
|
18
|
-
readonly status: number,
|
|
19
|
-
) {
|
|
20
|
-
super(message);
|
|
21
|
-
this.name = "TwilioError";
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** A number as a phone writes it: +, then digits. `sip:+43…@host` gives the same. */
|
|
26
|
-
export function e164(raw: string): string {
|
|
27
|
-
const user = raw.replace(/^sips?:/, "").split("@")[0];
|
|
28
|
-
const digits = user.replace(/[^\d]/g, "");
|
|
29
|
-
return `+${digits}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Sends one text, from the number the call came in on. */
|
|
33
|
-
export async function sendText(
|
|
34
|
-
account: TwilioAccount,
|
|
35
|
-
from: string,
|
|
36
|
-
to: string,
|
|
37
|
-
body: string,
|
|
38
|
-
fetchImpl: typeof fetch = (...args) => globalThis.fetch(...args),
|
|
39
|
-
): Promise<void> {
|
|
40
|
-
const res = await fetchImpl(`${API}/Accounts/${encodeURIComponent(account.sid)}/Messages.json`, {
|
|
41
|
-
method: "POST",
|
|
42
|
-
headers: {
|
|
43
|
-
authorization: `Basic ${Buffer.from(`${account.sid}:${account.token}`).toString("base64")}`,
|
|
44
|
-
"content-type": "application/x-www-form-urlencoded",
|
|
45
|
-
},
|
|
46
|
-
body: new URLSearchParams({ To: to, From: from, Body: body }).toString(),
|
|
47
|
-
signal: AbortSignal.timeout(20_000),
|
|
48
|
-
});
|
|
49
|
-
if (!res.ok) {
|
|
50
|
-
const text = await res.text();
|
|
51
|
-
let message = `Twilio answered ${res.status}.`;
|
|
52
|
-
try {
|
|
53
|
-
message = (JSON.parse(text) as { message?: string }).message ?? message;
|
|
54
|
-
} catch {
|
|
55
|
-
// Not JSON: the status says enough.
|
|
56
|
-
}
|
|
57
|
-
throw new TwilioError(message, res.status);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** `X-Twilio-Signature`: HMAC-SHA1, base64, of the address with the sorted form fields appended. */
|
|
62
|
-
export function signatureOk(
|
|
63
|
-
token: string,
|
|
64
|
-
urls: string[],
|
|
65
|
-
form: URLSearchParams,
|
|
66
|
-
header: string | null,
|
|
67
|
-
): boolean {
|
|
68
|
-
if (!header) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
const given = Buffer.from(header);
|
|
72
|
-
const names = [...new Set(form.keys())].sort();
|
|
73
|
-
return urls.some((url) => {
|
|
74
|
-
const data = names.reduce((acc, name) => acc + name + form.getAll(name).join(""), url);
|
|
75
|
-
const expected = Buffer.from(createHmac("sha1", token).update(data, "utf8").digest("base64"));
|
|
76
|
-
return expected.length === given.length && timingSafeEqual(expected, given);
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** TwiML that hands the call to a SIP address, with a header that names the call to us. */
|
|
81
|
-
export function dialTwiml(sipUri: string, token: string): string {
|
|
82
|
-
const uri = `${sipUri}?x-engenty-call=${encodeURIComponent(token)}`;
|
|
83
|
-
const safe = uri.replace(/&/g, "&").replace(/</g, "<");
|
|
84
|
-
return `<?xml version="1.0" encoding="UTF-8"?><Response><Dial answerOnBridge="true"><Sip>${safe}</Sip></Dial></Response>`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export const emptyTwiml = () =>
|
|
88
|
-
new Response('<?xml version="1.0" encoding="UTF-8"?><Response></Response>', {
|
|
89
|
-
headers: { "content-type": "text/xml" },
|
|
90
|
-
});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { Button, Card, Input, Label, Select, Spinner } from "@engenty-wizards/web/ui";
|
|
2
|
-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
import { type AccountView, api, errorText, KEY, t } from "./app";
|
|
5
|
-
import { Shown } from "./parts";
|
|
6
|
-
|
|
7
|
-
/** Settings → Calls: Twilio, the OpenAI project, and what to enter where. */
|
|
8
|
-
export function SettingsSection() {
|
|
9
|
-
const cache = useQueryClient();
|
|
10
|
-
const account = useQuery({
|
|
11
|
-
queryKey: [KEY, "account"],
|
|
12
|
-
queryFn: () => api().get<AccountView>("/account"),
|
|
13
|
-
});
|
|
14
|
-
const [form, setForm] = useState({
|
|
15
|
-
twilioSid: "",
|
|
16
|
-
twilioToken: "",
|
|
17
|
-
smsFrom: "",
|
|
18
|
-
openaiProject: "",
|
|
19
|
-
openaiWebhookSecret: "",
|
|
20
|
-
region: "eu" as "eu" | "us",
|
|
21
|
-
});
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
if (account.data) {
|
|
24
|
-
setForm((f) => ({
|
|
25
|
-
...f,
|
|
26
|
-
twilioSid: account.data.twilioSid,
|
|
27
|
-
smsFrom: account.data.smsFrom,
|
|
28
|
-
openaiProject: account.data.openaiProject,
|
|
29
|
-
region: account.data.region,
|
|
30
|
-
}));
|
|
31
|
-
}
|
|
32
|
-
}, [account.data]);
|
|
33
|
-
const refresh = () => cache.invalidateQueries({ queryKey: [KEY] });
|
|
34
|
-
const save = useMutation({
|
|
35
|
-
mutationFn: () =>
|
|
36
|
-
api().put<AccountView>("/account", {
|
|
37
|
-
twilioSid: form.twilioSid,
|
|
38
|
-
twilioToken: form.twilioToken || undefined,
|
|
39
|
-
smsFrom: form.smsFrom || null,
|
|
40
|
-
openaiProject: form.openaiProject,
|
|
41
|
-
openaiWebhookSecret: form.openaiWebhookSecret || undefined,
|
|
42
|
-
region: form.region,
|
|
43
|
-
}),
|
|
44
|
-
onSuccess: async () => {
|
|
45
|
-
setForm((f) => ({ ...f, twilioToken: "", openaiWebhookSecret: "" }));
|
|
46
|
-
await refresh();
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
const remove = useMutation({
|
|
50
|
-
mutationFn: () => api().del<AccountView>("/account"),
|
|
51
|
-
onSuccess: refresh,
|
|
52
|
-
});
|
|
53
|
-
const set = (key: keyof typeof form) => (e: React.ChangeEvent<HTMLInputElement>) =>
|
|
54
|
-
setForm((f) => ({ ...f, [key]: e.target.value }));
|
|
55
|
-
const data = account.data;
|
|
56
|
-
return (
|
|
57
|
-
<section className="flex flex-col gap-2.5">
|
|
58
|
-
<h2 className="px-1 font-display font-semibold text-lg leading-tight">{t("title")}</h2>
|
|
59
|
-
<Card className="flex flex-col gap-4 p-5 text-[0.875rem]">
|
|
60
|
-
<p className="text-[0.8125rem] text-ink-3">{t("intro")}</p>
|
|
61
|
-
{account.isLoading ? <Spinner className="mx-auto" /> : null}
|
|
62
|
-
<div>
|
|
63
|
-
<Label hint={t("twilioSidHint")}>{t("twilioSid")}</Label>
|
|
64
|
-
<Input value={form.twilioSid} onChange={set("twilioSid")} autoComplete="off" />
|
|
65
|
-
</div>
|
|
66
|
-
<div>
|
|
67
|
-
<Label hint={data?.hasToken ? t("kept") : t("twilioTokenHint")}>{t("twilioToken")}</Label>
|
|
68
|
-
<Input
|
|
69
|
-
type="password"
|
|
70
|
-
value={form.twilioToken}
|
|
71
|
-
onChange={set("twilioToken")}
|
|
72
|
-
placeholder={data?.hasToken ? "••••••••" : ""}
|
|
73
|
-
autoComplete="new-password"
|
|
74
|
-
/>
|
|
75
|
-
</div>
|
|
76
|
-
<div>
|
|
77
|
-
<Label hint={t("smsFromHint")}>{t("smsFrom")}</Label>
|
|
78
|
-
<Input value={form.smsFrom} onChange={set("smsFrom")} inputMode="tel" />
|
|
79
|
-
</div>
|
|
80
|
-
<div>
|
|
81
|
-
<Label hint={t("openaiProjectHint")}>{t("openaiProject")}</Label>
|
|
82
|
-
<Input value={form.openaiProject} onChange={set("openaiProject")} autoComplete="off" />
|
|
83
|
-
</div>
|
|
84
|
-
<div>
|
|
85
|
-
<Label hint={data?.hasSecret ? t("kept") : t("openaiSecretHint")}>
|
|
86
|
-
{t("openaiSecret")}
|
|
87
|
-
</Label>
|
|
88
|
-
<Input
|
|
89
|
-
type="password"
|
|
90
|
-
value={form.openaiWebhookSecret}
|
|
91
|
-
onChange={set("openaiWebhookSecret")}
|
|
92
|
-
placeholder={data?.hasSecret ? "••••••••" : ""}
|
|
93
|
-
autoComplete="new-password"
|
|
94
|
-
/>
|
|
95
|
-
</div>
|
|
96
|
-
<div>
|
|
97
|
-
<Label>{t("region")}</Label>
|
|
98
|
-
<Select
|
|
99
|
-
className="w-40"
|
|
100
|
-
value={form.region}
|
|
101
|
-
onChange={(region) => setForm((f) => ({ ...f, region: region as "eu" | "us" }))}
|
|
102
|
-
options={[
|
|
103
|
-
{ value: "eu", label: t("regionEu") },
|
|
104
|
-
{ value: "us", label: t("regionUs") },
|
|
105
|
-
]}
|
|
106
|
-
/>
|
|
107
|
-
</div>
|
|
108
|
-
<div className="flex items-center gap-3">
|
|
109
|
-
<Button onClick={() => save.mutate()} busy={save.isPending}>
|
|
110
|
-
{t("save")}
|
|
111
|
-
</Button>
|
|
112
|
-
{data?.connected ? (
|
|
113
|
-
<Button variant="ghost" onClick={() => remove.mutate()} busy={remove.isPending}>
|
|
114
|
-
{t("disconnect")}
|
|
115
|
-
</Button>
|
|
116
|
-
) : null}
|
|
117
|
-
{save.isSuccess ? (
|
|
118
|
-
<span className="text-[0.8125rem] text-ink-3">{t("saved")}</span>
|
|
119
|
-
) : null}
|
|
120
|
-
</div>
|
|
121
|
-
{save.error ? <p className="text-[0.8125rem] text-rose">{errorText(save.error)}</p> : null}
|
|
122
|
-
</Card>
|
|
123
|
-
{data?.connected ? (
|
|
124
|
-
<Card className="flex flex-col gap-4 p-5 text-[0.875rem]">
|
|
125
|
-
<div className="font-medium">{t("setup")}</div>
|
|
126
|
-
<Shown label={t("voiceWebhook")} value={data.voiceWebhook} />
|
|
127
|
-
<p className="-mt-2 text-[0.8125rem] text-ink-3">{t("voiceWebhookHint")}</p>
|
|
128
|
-
<Shown label={t("openaiWebhook")} value={data.openaiWebhook} />
|
|
129
|
-
<p className="-mt-2 text-[0.8125rem] text-ink-3">{t("openaiWebhookHint")}</p>
|
|
130
|
-
{data.sipUri ? <Shown label={t("sipUri")} value={data.sipUri} /> : null}
|
|
131
|
-
<div className="font-medium">{t("meta")}</div>
|
|
132
|
-
<p className="-mt-2 text-[0.8125rem] text-ink-3">{t("metaHint")}</p>
|
|
133
|
-
</Card>
|
|
134
|
-
) : null}
|
|
135
|
-
</section>
|
|
136
|
-
);
|
|
137
|
-
}
|
package/modules/calls/ui/app.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import type { StudioPluginContext } from "@engenty-wizards/plugin-sdk/studio";
|
|
2
|
-
import { messages } from "./messages";
|
|
3
|
-
|
|
4
|
-
/** The studio this half was handed, and its words. */
|
|
5
|
-
|
|
6
|
-
let current: StudioPluginContext | null = null;
|
|
7
|
-
let translate: ((key: string, vars?: Record<string, string | number>) => string) | null = null;
|
|
8
|
-
|
|
9
|
-
export function setStudio(studio: StudioPluginContext) {
|
|
10
|
-
current = studio;
|
|
11
|
-
translate = studio.i18n.register(messages) as typeof translate;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function studio(): StudioPluginContext {
|
|
15
|
-
if (!current) {
|
|
16
|
-
throw new Error("The calls plugin is not loaded.");
|
|
17
|
-
}
|
|
18
|
-
return current;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type Key = keyof (typeof messages)["de"];
|
|
22
|
-
export const t = (key: Key, vars?: Record<string, string | number>) =>
|
|
23
|
-
translate?.(key, vars) ?? key;
|
|
24
|
-
export const api = () => studio().api;
|
|
25
|
-
|
|
26
|
-
export const KEY = "calls";
|
|
27
|
-
|
|
28
|
-
/** What a failed request says, in the person's language. */
|
|
29
|
-
export function errorText(error: unknown): string {
|
|
30
|
-
const body = (error as { body?: { code?: string; error?: string } } | null)?.body;
|
|
31
|
-
const code = body?.code;
|
|
32
|
-
const message = body?.error ?? (error instanceof Error ? error.message : String(error));
|
|
33
|
-
if (code && `error_${code}` in messages.de) {
|
|
34
|
-
return t(`error_${code}` as Key, { message });
|
|
35
|
-
}
|
|
36
|
-
return t("error_other", { message });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface AccountView {
|
|
40
|
-
connected: boolean;
|
|
41
|
-
twilioSid: string;
|
|
42
|
-
hasToken: boolean;
|
|
43
|
-
smsFrom: string;
|
|
44
|
-
openaiProject: string;
|
|
45
|
-
hasSecret: boolean;
|
|
46
|
-
region: "eu" | "us";
|
|
47
|
-
voiceWebhook: string;
|
|
48
|
-
openaiWebhook: string;
|
|
49
|
-
sipUri: string | null;
|
|
50
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/** The studio half's words. `de` is the source. */
|
|
2
|
-
export const messages = {
|
|
3
|
-
de: {
|
|
4
|
-
title: "Anrufe",
|
|
5
|
-
intro:
|
|
6
|
-
"Wizards laufen als Gespräch am Telefon: ein WhatsApp-Anruf auf die Business-Nummer oder ein Anruf auf eine Twilio-Nummer. Twilio trägt den Anruf, das Sprachmodell des OpenAI-Kontos (Einstellungen → Modelle) spricht; was eine Stimme nicht kann, kommt als Link per SMS.",
|
|
7
|
-
twilioSid: "Twilio Account SID",
|
|
8
|
-
twilioSidHint: "Aus der Twilio-Konsole, beginnt mit AC.",
|
|
9
|
-
twilioToken: "Twilio Auth Token",
|
|
10
|
-
twilioTokenHint: "Prüft auch jede Anfrage von Twilio auf ihre Signatur.",
|
|
11
|
-
kept: "Ist hinterlegt. Leer lassen, um ihn zu behalten.",
|
|
12
|
-
smsFrom: "SMS-Nummer",
|
|
13
|
-
smsFromHint:
|
|
14
|
-
"Eine SMS-fähige Twilio-Nummer, von der die Links kommen, mit Ländervorwahl. Ohne sie gibt es keine Links.",
|
|
15
|
-
openaiProject: "OpenAI Project ID",
|
|
16
|
-
openaiProjectHint: "Das Projekt, dessen API Key unter Modelle hinterlegt ist: proj_…",
|
|
17
|
-
openaiSecret: "OpenAI Webhook Secret",
|
|
18
|
-
openaiSecretHint:
|
|
19
|
-
"Aus dem Webhook im OpenAI-Dashboard für das Ereignis realtime.call.incoming: whsec_…",
|
|
20
|
-
region: "Region",
|
|
21
|
-
regionEu: "EU",
|
|
22
|
-
regionUs: "USA",
|
|
23
|
-
save: "Speichern",
|
|
24
|
-
saved: "Gespeichert.",
|
|
25
|
-
disconnect: "Trennen",
|
|
26
|
-
setup: "Eintragen",
|
|
27
|
-
copy: "Kopieren",
|
|
28
|
-
copied: "Kopiert",
|
|
29
|
-
voiceWebhook: "Twilio: Voice-Webhook",
|
|
30
|
-
voiceWebhookHint:
|
|
31
|
-
"Bei der SIP-Domain (WhatsApp) und bei der Telefonnummer unter „A call comes in“, als Webhook mit HTTP POST.",
|
|
32
|
-
openaiWebhook: "OpenAI: Webhook",
|
|
33
|
-
openaiWebhookHint: "Im OpenAI-Dashboard als Webhook für realtime.call.incoming.",
|
|
34
|
-
sipUri: "Wohin Twilio den Anruf gibt",
|
|
35
|
-
meta: "Meta: SIP auf der WhatsApp-Nummer",
|
|
36
|
-
metaHint:
|
|
37
|
-
"In den Calling-Einstellungen der WhatsApp-Nummer: SIP einschalten, als Server die Twilio-SIP-Domain (…sip.twilio.com) mit Port 5061. Die SIP-Domain in Twilio erlaubt Metas Adressen in ihrer Zugriffsliste.",
|
|
38
|
-
error_no_token: "Es braucht den Twilio Auth Token.",
|
|
39
|
-
error_no_secret: "Es braucht das OpenAI Webhook Secret.",
|
|
40
|
-
error_other: "{message}",
|
|
41
|
-
},
|
|
42
|
-
en: {
|
|
43
|
-
title: "Calls",
|
|
44
|
-
intro:
|
|
45
|
-
"Wizards run as a conversation on the phone: a WhatsApp call to the business number or a call to a Twilio number. Twilio carries the call, the speech model of the OpenAI account (Settings → Models) speaks; what a voice cannot do arrives as a link by text.",
|
|
46
|
-
twilioSid: "Twilio Account SID",
|
|
47
|
-
twilioSidHint: "From the Twilio console, starts with AC.",
|
|
48
|
-
twilioToken: "Twilio auth token",
|
|
49
|
-
twilioTokenHint: "Also checks every request from Twilio for its signature.",
|
|
50
|
-
kept: "Is set. Leave empty to keep it.",
|
|
51
|
-
smsFrom: "Texting number",
|
|
52
|
-
smsFromHint:
|
|
53
|
-
"A Twilio number that can text, the links come from it, with the country code. Without it there are no links.",
|
|
54
|
-
openaiProject: "OpenAI project ID",
|
|
55
|
-
openaiProjectHint: "The project whose API key is set under Models: proj_…",
|
|
56
|
-
openaiSecret: "OpenAI webhook secret",
|
|
57
|
-
openaiSecretHint:
|
|
58
|
-
"From the webhook in the OpenAI dashboard for the event realtime.call.incoming: whsec_…",
|
|
59
|
-
region: "Region",
|
|
60
|
-
regionEu: "EU",
|
|
61
|
-
regionUs: "US",
|
|
62
|
-
save: "Save",
|
|
63
|
-
saved: "Saved.",
|
|
64
|
-
disconnect: "Disconnect",
|
|
65
|
-
setup: "Enter",
|
|
66
|
-
copy: "Copy",
|
|
67
|
-
copied: "Copied",
|
|
68
|
-
voiceWebhook: "Twilio: voice webhook",
|
|
69
|
-
voiceWebhookHint:
|
|
70
|
-
"On the SIP domain (WhatsApp) and on the phone number under “A call comes in”, as a webhook with HTTP POST.",
|
|
71
|
-
openaiWebhook: "OpenAI: webhook",
|
|
72
|
-
openaiWebhookHint: "In the OpenAI dashboard as the webhook for realtime.call.incoming.",
|
|
73
|
-
sipUri: "Where Twilio hands the call",
|
|
74
|
-
meta: "Meta: SIP on the WhatsApp number",
|
|
75
|
-
metaHint:
|
|
76
|
-
"In the WhatsApp number's calling settings: switch SIP on, with the Twilio SIP domain (…sip.twilio.com) as the server on port 5061. The SIP domain in Twilio allows Meta's addresses in its access list.",
|
|
77
|
-
error_no_token: "Twilio's auth token is needed.",
|
|
78
|
-
error_no_secret: "OpenAI's webhook secret is needed.",
|
|
79
|
-
error_other: "{message}",
|
|
80
|
-
},
|
|
81
|
-
} as const;
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { IconButton } from "@engenty-wizards/web/ui";
|
|
2
|
-
import { Check, Copy } from "lucide-react";
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { t } from "./app";
|
|
5
|
-
|
|
6
|
-
/** A value to take along: the webhook address, the verify token, the link. */
|
|
7
|
-
export function CopyButton({ text }: { text: string }) {
|
|
8
|
-
const [done, setDone] = useState(false);
|
|
9
|
-
return (
|
|
10
|
-
<IconButton
|
|
11
|
-
label={t(done ? "copied" : "copy")}
|
|
12
|
-
className="size-8"
|
|
13
|
-
onClick={() => {
|
|
14
|
-
void navigator.clipboard.writeText(text).then(() => {
|
|
15
|
-
setDone(true);
|
|
16
|
-
setTimeout(() => setDone(false), 1500);
|
|
17
|
-
});
|
|
18
|
-
}}
|
|
19
|
-
>
|
|
20
|
-
{done ? <Check className="size-4" /> : <Copy className="size-4" />}
|
|
21
|
-
</IconButton>
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** A value shown as it is, with the button that copies it. */
|
|
26
|
-
export function Shown({ label, value }: { label: string; value: string }) {
|
|
27
|
-
return (
|
|
28
|
-
<div className="flex items-center gap-2">
|
|
29
|
-
<div className="min-w-0 flex-1">
|
|
30
|
-
<div className="text-[0.8125rem] text-ink-3">{label}</div>
|
|
31
|
-
<div className="select-all truncate font-mono text-[0.8125rem]">{value}</div>
|
|
32
|
-
</div>
|
|
33
|
-
<CopyButton text={value} />
|
|
34
|
-
</div>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { defineStudioPlugin } from "@engenty-wizards/plugin-sdk/studio";
|
|
2
|
-
import { PhoneCall } from "lucide-react";
|
|
3
|
-
import { setStudio, t } from "./app";
|
|
4
|
-
import { SettingsSection } from "./Settings";
|
|
5
|
-
|
|
6
|
-
/** The studio half: Twilio, the OpenAI project and the addresses to enter, in the settings. */
|
|
7
|
-
export default defineStudioPlugin((studio) => {
|
|
8
|
-
setStudio(studio);
|
|
9
|
-
studio.registerSettingsSection({
|
|
10
|
-
id: "calls",
|
|
11
|
-
label: () => t("title"),
|
|
12
|
-
icon: PhoneCall,
|
|
13
|
-
component: SettingsSection,
|
|
14
|
-
});
|
|
15
|
-
});
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
2
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-content:"";--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer wizards-plugin{[data-plugin=sms] .pointer-events-none{pointer-events:none}[data-plugin=sms] .absolute{position:absolute}[data-plugin=sms] .fixed{position:fixed}[data-plugin=sms] .relative{position:relative}[data-plugin=sms] .-top-1\.5{top:-.375rem}[data-plugin=sms] .top-0\.5{top:.125rem}[data-plugin=sms] .top-1\/2{top:50%}[data-plugin=sms] .-right-1\.5{right:-.375rem}[data-plugin=sms] .right-2{right:.5rem}[data-plugin=sms] .right-3{right:.75rem}[data-plugin=sms] .bottom-0\.5{bottom:.125rem}[data-plugin=sms] .bottom-1{bottom:.25rem}[data-plugin=sms] .left-0{left:0}[data-plugin=sms] .left-1\/2{left:50%}[data-plugin=sms] .z-50{z-index:50}[data-plugin=sms] .m-auto{margin:auto}[data-plugin=sms] .-mx-1{margin-inline:-.25rem}[data-plugin=sms] .mx-auto{margin-inline:auto}[data-plugin=sms] .my-1\.5{margin-block:.375rem}[data-plugin=sms] .-mt-1\.5{margin-top:-.375rem}[data-plugin=sms] .mt-0\.5{margin-top:.125rem}[data-plugin=sms] .mt-2{margin-top:.5rem}[data-plugin=sms] .-mr-2{margin-right:-.5rem}[data-plugin=sms] .mr-0\.5{margin-right:.125rem}[data-plugin=sms] .mb-2{margin-bottom:.5rem}[data-plugin=sms] .mb-5{margin-bottom:1.25rem}[data-plugin=sms] .-ml-1\.5{margin-left:-.375rem}[data-plugin=sms] .ml-1{margin-left:.25rem}[data-plugin=sms] .flex{display:flex}[data-plugin=sms] .grid{display:grid}[data-plugin=sms] .hidden{display:none}[data-plugin=sms] .inline-block{display:inline-block}[data-plugin=sms] .inline-flex{display:inline-flex}[data-plugin=sms] .size-1{width:.25rem;height:.25rem}[data-plugin=sms] .size-1\.5{width:.375rem;height:.375rem}[data-plugin=sms] .size-3{width:.75rem;height:.75rem}[data-plugin=sms] .size-3\.5{width:.875rem;height:.875rem}[data-plugin=sms] .size-4{width:1rem;height:1rem}[data-plugin=sms] .size-5{width:1.25rem;height:1.25rem}[data-plugin=sms] .size-6{width:1.5rem;height:1.5rem}[data-plugin=sms] .size-7{width:1.75rem;height:1.75rem}[data-plugin=sms] .size-8{width:2rem;height:2rem}[data-plugin=sms] .size-9{width:2.25rem;height:2.25rem}[data-plugin=sms] .size-11{width:2.75rem;height:2.75rem}[data-plugin=sms] .size-16{width:4rem;height:4rem}[data-plugin=sms] .h-6{height:1.5rem}[data-plugin=sms] .h-8{height:2rem}[data-plugin=sms] .h-9{height:2.25rem}[data-plugin=sms] .h-10{height:2.5rem}[data-plugin=sms] .h-11{height:2.75rem}[data-plugin=sms] .h-12{height:3rem}[data-plugin=sms] .h-px{height:1px}[data-plugin=sms] .max-h-56{max-height:14rem}[data-plugin=sms] .max-h-\[200px\]{max-height:200px}[data-plugin=sms] .max-h-\[calc\(100dvh-32px\)\]{max-height:calc(100dvh - 32px)}[data-plugin=sms] .min-h-9{min-height:2.25rem}[data-plugin=sms] .w-5{width:1.25rem}[data-plugin=sms] .w-11{width:2.75rem}[data-plugin=sms] .w-14{width:3.5rem}[data-plugin=sms] .w-72{width:18rem}[data-plugin=sms] .w-\[19rem\]{width:19rem}[data-plugin=sms] .w-\[calc\(100\%-32px\)\]{width:calc(100% - 32px)}[data-plugin=sms] .w-\[calc\(100\%-48px\)\]{width:calc(100% - 48px)}[data-plugin=sms] .w-full{width:100%}[data-plugin=sms] .w-px{width:1px}[data-plugin=sms] .max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}[data-plugin=sms] .max-w-\[calc\(100vw-16px\)\]{max-width:calc(100vw - 16px)}[data-plugin=sms] .max-w-full{max-width:100%}[data-plugin=sms] .max-w-xs{max-width:20rem}[data-plugin=sms] .min-w-0{min-width:0}[data-plugin=sms] .flex-1{flex:1}[data-plugin=sms] .shrink-0{flex-shrink:0}[data-plugin=sms] .-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}[data-plugin=sms] .translate-x-\[2px\]{--tw-translate-x:2px;translate:var(--tw-translate-x) var(--tw-translate-y)}[data-plugin=sms] .translate-x-\[22px\]{--tw-translate-x:22px;translate:var(--tw-translate-x) var(--tw-translate-y)}[data-plugin=sms] .-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}[data-plugin=sms] .rotate-180{rotate:180deg}[data-plugin=sms] .transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}[data-plugin=sms] .animate-spin{animation:1s linear infinite spin}[data-plugin=sms] .cursor-pointer{cursor:pointer}[data-plugin=sms] .touch-none{touch-action:none}[data-plugin=sms] .resize{resize:both}[data-plugin=sms] .resize-none{resize:none}[data-plugin=sms] .\[scrollbar-width\:none\]{scrollbar-width:none}[data-plugin=sms] .appearance-none{appearance:none}[data-plugin=sms] .grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}[data-plugin=sms] .flex-col{flex-direction:column}[data-plugin=sms] .flex-wrap{flex-wrap:wrap}[data-plugin=sms] .place-items-center{place-items:center}[data-plugin=sms] .items-center{align-items:center}[data-plugin=sms] .items-end{align-items:flex-end}[data-plugin=sms] .items-start{align-items:flex-start}[data-plugin=sms] .justify-between{justify-content:space-between}[data-plugin=sms] .justify-center{justify-content:center}[data-plugin=sms] .gap-0\.5{gap:.125rem}[data-plugin=sms] .gap-1{gap:.25rem}[data-plugin=sms] .gap-1\.5{gap:.375rem}[data-plugin=sms] .gap-2{gap:.5rem}[data-plugin=sms] .gap-2\.5{gap:.625rem}[data-plugin=sms] .gap-3{gap:.75rem}[data-plugin=sms] .gap-4{gap:1rem}[data-plugin=sms] .gap-y-0\.5{row-gap:.125rem}[data-plugin=sms] .self-stretch{align-self:stretch}[data-plugin=sms] .truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}[data-plugin=sms] .overflow-visible{overflow:visible}[data-plugin=sms] .overflow-x-auto{overflow-x:auto}[data-plugin=sms] .overflow-y-auto{overflow-y:auto}[data-plugin=sms] .overscroll-contain{overscroll-behavior:contain}[data-plugin=sms] .rounded-2xl{border-radius:calc(var(--radius) * 2)}[data-plugin=sms] .rounded-\[1\.375rem\]{border-radius:1.375rem}[data-plugin=sms] .rounded-full{border-radius:2147483647px}[data-plugin=sms] .rounded-lg{border-radius:var(--radius)}[data-plugin=sms] .rounded-md{border-radius:calc(var(--radius) * .75)}[data-plugin=sms] .rounded-xl{border-radius:calc(var(--radius) * 1.5)}[data-plugin=sms] .border{border-style:var(--tw-border-style);border-width:1px}[data-plugin=sms] .border-t{border-top-style:var(--tw-border-style);border-top-width:1px}[data-plugin=sms] .border-border{border-color:var(--border)}[data-plugin=sms] .border-border-soft{border-color:var(--border-soft)}[data-plugin=sms] .border-ember{border-color:var(--ember-mark)}[data-plugin=sms] .border-focus{border-color:var(--focus-line)}[data-plugin=sms] .border-input{border-color:var(--input)}[data-plugin=sms] .bg-amber-tint{background-color:var(--amber-tint)}[data-plugin=sms] .bg-border-soft{background-color:var(--border-soft)}[data-plugin=sms] .bg-card{background-color:var(--card)}[data-plugin=sms] .bg-ember{background-color:var(--ember-mark)}[data-plugin=sms] .bg-ember-tint{background-color:var(--ember-tint)}[data-plugin=sms] .bg-ink{background-color:var(--ink)}[data-plugin=sms] .bg-moss{background-color:var(--moss)}[data-plugin=sms] .bg-moss-tint{background-color:var(--moss-tint)}[data-plugin=sms] .bg-paper-2{background-color:var(--paper-2)}[data-plugin=sms] .bg-paper-3{background-color:var(--paper-3)}[data-plugin=sms] .bg-primary{background-color:var(--primary)}[data-plugin=sms] .bg-rose-tint{background-color:var(--rose-tint)}[data-plugin=sms] .bg-transparent{background-color:#0000}[data-plugin=sms] .bg-white{background-color:#fff}[data-plugin=sms] .bg-linear-to-l{--tw-gradient-position:to left}@supports (background-image:linear-gradient(in lab, red, red)){[data-plugin=sms] .bg-linear-to-l{--tw-gradient-position:to left in oklab}}[data-plugin=sms] .bg-linear-to-l{background-image:linear-gradient(var(--tw-gradient-stops))}[data-plugin=sms] .bg-linear-to-r{--tw-gradient-position:to right}@supports (background-image:linear-gradient(in lab, red, red)){[data-plugin=sms] .bg-linear-to-r{--tw-gradient-position:to right in oklab}}[data-plugin=sms] .bg-linear-to-r{background-image:linear-gradient(var(--tw-gradient-stops))}[data-plugin=sms] .from-transparent{--tw-gradient-from:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}[data-plugin=sms] .from-50\%{--tw-gradient-from-position:50%}[data-plugin=sms] .to-ember-tint{--tw-gradient-to:var(--ember-tint);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}[data-plugin=sms] .to-50\%{--tw-gradient-to-position:50%}[data-plugin=sms] .fill-current{fill:currentColor}[data-plugin=sms] .object-cover{object-fit:cover}[data-plugin=sms] .p-0{padding:0}[data-plugin=sms] .p-0\.5{padding:.125rem}[data-plugin=sms] .p-1{padding:.25rem}[data-plugin=sms] .p-1\.5{padding:.375rem}[data-plugin=sms] .p-3{padding:.75rem}[data-plugin=sms] .p-5{padding:1.25rem}[data-plugin=sms] .px-1{padding-inline:.25rem}[data-plugin=sms] .px-2{padding-inline:.5rem}[data-plugin=sms] .px-2\.5{padding-inline:.625rem}[data-plugin=sms] .px-3{padding-inline:.75rem}[data-plugin=sms] .px-3\.5{padding-inline:.875rem}[data-plugin=sms] .px-4{padding-inline:1rem}[data-plugin=sms] .px-6{padding-inline:1.5rem}[data-plugin=sms] .py-1{padding-block:.25rem}[data-plugin=sms] .py-1\.5{padding-block:.375rem}[data-plugin=sms] .py-2{padding-block:.5rem}[data-plugin=sms] .py-2\.5{padding-block:.625rem}[data-plugin=sms] .py-16{padding-block:4rem}[data-plugin=sms] .pt-1\.5{padding-top:.375rem}[data-plugin=sms] .pt-2{padding-top:.5rem}[data-plugin=sms] .pr-1{padding-right:.25rem}[data-plugin=sms] .pr-9{padding-right:2.25rem}[data-plugin=sms] .pb-1{padding-bottom:.25rem}[data-plugin=sms] .pl-2{padding-left:.5rem}[data-plugin=sms] .pl-2\.5{padding-left:.625rem}[data-plugin=sms] .text-center{text-align:center}[data-plugin=sms] .text-left{text-align:left}[data-plugin=sms] .font-display{font-family:var(--f-display)}[data-plugin=sms] .font-mono{font-family:var(--f-mono)}[data-plugin=sms] .text-lg{font-size:1.125rem;line-height:var(--tw-leading,calc(1.75 / 1.125))}[data-plugin=sms] .text-sm{font-size:.875rem;line-height:var(--tw-leading,calc(1.25 / .875))}[data-plugin=sms] .text-xl{font-size:1.25rem;line-height:var(--tw-leading,calc(1.75 / 1.25))}[data-plugin=sms] .text-\[0\.75rem\]{font-size:.75rem}[data-plugin=sms] .text-\[0\.875rem\]{font-size:.875rem}[data-plugin=sms] .text-\[0\.6875rem\]{font-size:.6875rem}[data-plugin=sms] .text-\[0\.8125rem\]{font-size:.8125rem}[data-plugin=sms] .text-\[0\.9375rem\]{font-size:.9375rem}[data-plugin=sms] .text-\[14px\]{font-size:14px}[data-plugin=sms] .leading-5{--tw-leading:calc(.25rem * 5);line-height:1.25rem}[data-plugin=sms] .leading-\[1\.5\]{--tw-leading:1.5;line-height:1.5}[data-plugin=sms] .leading-tight{--tw-leading:1.25;line-height:1.25}[data-plugin=sms] .font-medium{--tw-font-weight:500;font-weight:500}[data-plugin=sms] .font-semibold{--tw-font-weight:600;font-weight:600}[data-plugin=sms] .tracking-wide{--tw-tracking:.025em;letter-spacing:.025em}[data-plugin=sms] .whitespace-nowrap{white-space:nowrap}[data-plugin=sms] .text-ember{color:var(--ember-mark)}[data-plugin=sms] .text-ember-strong{color:var(--ember-strong-mark)}[data-plugin=sms] .text-ink{color:var(--ink)}[data-plugin=sms] .text-ink-2{color:var(--ink-2)}[data-plugin=sms] .text-ink-3{color:var(--ink-3)}[data-plugin=sms] .text-ink-4{color:var(--ink-4)}[data-plugin=sms] .text-moss{color:var(--moss)}[data-plugin=sms] .text-paper{color:var(--paper)}[data-plugin=sms] .text-primary-foreground{color:var(--primary-foreground)}[data-plugin=sms] .text-rose{color:var(--rose)}[data-plugin=sms] .capitalize{text-transform:capitalize}[data-plugin=sms] .uppercase{text-transform:uppercase}[data-plugin=sms] .tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}[data-plugin=sms] .line-through{text-decoration-line:line-through}[data-plugin=sms] .underline{text-decoration-line:underline}[data-plugin=sms] .underline-offset-2{text-underline-offset:2px}[data-plugin=sms] .opacity-0{opacity:0}[data-plugin=sms] .opacity-35{opacity:.35}[data-plugin=sms] .shadow-\[0_1px_0_oklch\(0\%_0_0\/0\.08\)\]{--tw-shadow:0 1px 0 var(--tw-shadow-color,oklch(0% 0 0/.08));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}[data-plugin=sms] .shadow-\[inset_0_1px_0_oklch\(0\%_0_0\/0\.02\)\]{--tw-shadow:inset 0 1px 0 var(--tw-shadow-color,oklch(0% 0 0/.02));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}[data-plugin=sms] .shadow-overlay{--tw-shadow:var(--shadow-ember-overlay);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}[data-plugin=sms] .shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}[data-plugin=sms] .shadow-soft{--tw-shadow:var(--shadow-ember-soft);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}[data-plugin=sms] .ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}[data-plugin=sms] .ring-4{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}[data-plugin=sms] .ring-border{--tw-ring-color:var(--border)}[data-plugin=sms] .ring-border-soft{--tw-ring-color:var(--border-soft)}[data-plugin=sms] .ring-focus-glow{--tw-ring-color:var(--focus-glow)}[data-plugin=sms] .transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,cubic-bezier(.4, 0, .2, 1));transition-duration:var(--tw-duration,.15s)}[data-plugin=sms] .transition-\[background\,filter\,color\,transform\]{transition-property:background,filter,color,transform;transition-timing-function:var(--tw-ease,cubic-bezier(.4, 0, .2, 1));transition-duration:var(--tw-duration,.15s)}[data-plugin=sms] .transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,cubic-bezier(.4, 0, .2, 1));transition-duration:var(--tw-duration,.15s)}[data-plugin=sms] .transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,cubic-bezier(.4, 0, .2, 1));transition-duration:var(--tw-duration,.15s)}[data-plugin=sms] .duration-150{--tw-duration:.15s;transition-duration:.15s}[data-plugin=sms] .duration-200{--tw-duration:.2s;transition-duration:.2s}[data-plugin=sms] .ease-out{--tw-ease:cubic-bezier(0, 0, .2, 1);transition-timing-function:cubic-bezier(0,0,.2,1)}[data-plugin=sms] .outline-none{--tw-outline-style:none;outline-style:none}[data-plugin=sms] .select-all{-webkit-user-select:all;user-select:all}[data-plugin=sms] .select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){[data-plugin=sms] .group-hover\:scale-110:is(:where(.group):hover *){--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}[data-plugin=sms] .group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}[data-plugin=sms] .placeholder\:truncate::placeholder{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}[data-plugin=sms] .placeholder\:text-ink-4::placeholder{color:var(--ink-4)}[data-plugin=sms] .backdrop\:bg-\[oklch\(20\%_0\.01_60\/0\.28\)\]::backdrop{background-color:oklch(20% .01 60/.28)}[data-plugin=sms] .backdrop\:backdrop-blur-\[2px\]::backdrop{--tw-backdrop-blur:blur(2px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}[data-plugin=sms] .before\:absolute:before{content:var(--tw-content);position:absolute}[data-plugin=sms] .before\:-inset-x-1:before{content:var(--tw-content);inset-inline:-.25rem}[data-plugin=sms] .before\:-inset-y-2\.5:before{content:var(--tw-content);inset-block:-.625rem}[data-plugin=sms] .before\:content-\[\'\'\]:before{--tw-content:"";content:var(--tw-content)}[data-plugin=sms] .focus-within\:ring-focus:focus-within{--tw-ring-color:var(--focus-line)}@media (hover:hover){[data-plugin=sms] .hover\:border-ink-4:hover{border-color:var(--ink-4)}[data-plugin=sms] .hover\:bg-accent:hover{background-color:var(--accent)}[data-plugin=sms] .hover\:bg-card:hover{background-color:var(--card)}[data-plugin=sms] .hover\:bg-paper-2:hover{background-color:var(--paper-2)}[data-plugin=sms] .hover\:bg-paper-3:hover{background-color:var(--paper-3)}[data-plugin=sms] .hover\:bg-rose-tint:hover{background-color:var(--rose-tint)}[data-plugin=sms] .hover\:text-ink:hover{color:var(--ink)}[data-plugin=sms] .hover\:text-ink-2:hover{color:var(--ink-2)}[data-plugin=sms] .hover\:text-rose:hover{color:var(--rose)}[data-plugin=sms] .hover\:brightness-\[1\.06\]:hover{--tw-brightness:brightness(1.06);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}[data-plugin=sms] .focus\:border-focus:focus{border-color:var(--focus-line)}[data-plugin=sms] .focus\:ring-4:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}[data-plugin=sms] .focus\:ring-focus-glow:focus{--tw-ring-color:var(--focus-glow)}[data-plugin=sms] .focus-visible\:border-focus:focus-visible{border-color:var(--focus-line)}[data-plugin=sms] .focus-visible\:opacity-100:focus-visible{opacity:1}[data-plugin=sms] .focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}[data-plugin=sms] .focus-visible\:ring-4:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}[data-plugin=sms] .focus-visible\:ring-focus:focus-visible{--tw-ring-color:var(--focus-line)}[data-plugin=sms] .focus-visible\:ring-focus-glow:focus-visible{--tw-ring-color:var(--focus-glow)}[data-plugin=sms] .active\:scale-\[0\.98\]:active{scale:.98}[data-plugin=sms] .active\:brightness-95:active{--tw-brightness:brightness(95%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}[data-plugin=sms] .disabled\:pointer-events-none:disabled{pointer-events:none}[data-plugin=sms] .disabled\:line-through:disabled{text-decoration-line:line-through}[data-plugin=sms] .disabled\:opacity-40:disabled{opacity:.4}[data-plugin=sms] .disabled\:opacity-50:disabled{opacity:.5}[data-plugin=sms] .disabled\:opacity-60:disabled{opacity:.6}@media (prefers-reduced-motion:reduce){[data-plugin=sms] .motion-reduce\:transition-none{transition-property:none}}@media not all and (width>=48rem){[data-plugin=sms] .max-md\:hidden{display:none}}@media not all and (width>=40rem){[data-plugin=sms] .max-sm\:m-0{margin:0}[data-plugin=sms] .max-sm\:h-dvh{height:100dvh}[data-plugin=sms] .max-sm\:max-h-none{max-height:none}[data-plugin=sms] .max-sm\:w-full{width:100%}[data-plugin=sms] .max-sm\:max-w-none{max-width:none}[data-plugin=sms] .max-sm\:rounded-none{border-radius:0}[data-plugin=sms] .max-sm\:pt-\[env\(safe-area-inset-top\)\]{padding-top:env(safe-area-inset-top)}[data-plugin=sms] .max-sm\:pb-\[env\(safe-area-inset-bottom\)\]{padding-bottom:env(safe-area-inset-bottom)}}@media (width>=40rem){[data-plugin=sms] .sm\:h-\[min\(calc\(100dvh-32px\)\,56rem\)\]{height:min(100dvh - 32px,56rem)}[data-plugin=sms] .sm\:max-w-2xl{max-width:42rem}[data-plugin=sms] .sm\:max-w-4xl{max-width:56rem}[data-plugin=sms] .sm\:max-w-md{max-width:28rem}[data-plugin=sms] .sm\:p-7{padding:1.75rem}}@media (width>=48rem){[data-plugin=sms] .md\:hidden{display:none}}@media (pointer:coarse){[data-plugin=sms] .coarse\:size-10{width:2.5rem;height:2.5rem}[data-plugin=sms] .coarse\:size-11{width:2.75rem;height:2.75rem}[data-plugin=sms] .coarse\:h-9{height:2.25rem}[data-plugin=sms] .coarse\:h-11{height:2.75rem}[data-plugin=sms] .coarse\:min-h-11{min-height:2.75rem}[data-plugin=sms] .coarse\:px-4{padding-inline:1rem}[data-plugin=sms] .coarse\:py-3{padding-block:.75rem}}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}
|
|
3
|
-
/*$vite$:1*/
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var __wizardsPlugin_sms=(function(e,t,n,r,i){var a=e=>e?.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase();function o(e,t,n=[]){if(t==null)throw Error(`[lucide]: iconNode is required when icon name is used`);return{name:a(e),size:24,node:t,...n.length>0?{aliases:n}:{}}}var s=e=>{let t=``,n=!1;for(let r of e){if(r===`-`||r===`_`||r<=` `){n=t.length>0;continue}t.length===0?t+=r.toLowerCase():t+=n?r.toUpperCase():r,n=!1}return t},c=e=>{let t=s(e);return t.charAt(0).toUpperCase()+t.slice(1)},l=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),u={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":2,"stroke-linecap":`round`,"stroke-linejoin":`round`};function d(e){return e!=null}function f(e,t={}){let n=t.attributeNames??{},r=e=>n[e]??e,i=e.size??e.width??u.width,a=e.size??e.height??u.height,o=e.aliases?.filter(e=>typeof e==`string`&&e.trim()!==``).map(e=>`lucide-${e}`)??[],s=[...e.name?[`lucide-${e.name}`]:[],...o],c=t.className?.split(` `).filter(Boolean)??[],f=t.includeDefaultClasses===!1?l(...c):l(`lucide`,...s,...c),p=t.absoluteStrokeWidth?Number(t.strokeWidth??u[`stroke-width`])*Number(e.size??e.width??u.width)/Number(t.size??t.width??u.width):t.strokeWidth??u[`stroke-width`];return[`svg`,{...Object.entries(u).reduce((e,[t,n])=>(e[r(t)]=n,e),{}),...`color`in t&&t.color&&{[r(`stroke`)]:t.color},...`size`in t&&d(t.size)&&{[r(`width`)]:t.size,[r(`height`)]:t.size},...`width`in t&&d(t.width)&&{[r(`width`)]:t.width},...`height`in t&&d(t.height)&&{[r(`height`)]:t.height},[r(`stroke-width`)]:p,...f&&{[r(`class`)]:f},[r(`viewBox`)]:`0 0 ${i} ${a}`,...t.hasA11yProp===!1?{[r(`aria-hidden`)]:`true`}:{},...`attributes`in t&&t.attributes},e.node.map(e=>{let[n,i,a]=e,o=t.nonScalingStroke?{[r(`vector-effect`)]:`non-scaling-stroke`,...i}:i;return a?[n,o,a]:[n,o]})]}function p(e,t={}){return f(e,{...t,attributeNames:{...t.attributeNames,class:`className`,"stroke-width":`strokeWidth`,"stroke-linecap":`strokeLinecap`,"stroke-linejoin":`strokeLinejoin`,"vector-effect":`vectorEffect`}})}var m=e=>{for(let t in e)if(t.startsWith(`aria-`)||t===`role`||t===`title`)return!0;return!1},h=(0,t.createContext)({}),g=()=>(0,t.useContext)(h),_=(0,t.forwardRef)(({color:e,size:n,width:r,height:i,strokeWidth:a,absoluteStrokeWidth:o,nonScalingStroke:s,className:c=``,children:u,iconNode:d=[],icon:f={node:d,aliases:[],size:24},...h},_)=>{let{size:v=24,strokeWidth:y=2,absoluteStrokeWidth:b=!1,nonScalingStroke:x=!1,color:S=`currentColor`,className:C=``}=g()??{},w=!!u||m(h),[T,E,D=[]]=p(f,{color:e??S,width:r??n??v,height:i??n??v,strokeWidth:a??y,absoluteStrokeWidth:o??b,nonScalingStroke:s??x,className:l(C,c),hasA11yProp:w,attributes:h});return(0,t.createElement)(T,{ref:_,...E},[...D.map(([e,n])=>(0,t.createElement)(e,n)),...Array.isArray(u)?u:[u]])});function v(e,n=[],r=[]){let i=typeof e==`string`?o(e,n,r):e,a=(0,t.forwardRef)(({className:e,...n},r)=>(0,t.createElement)(_,{ref:r,icon:i,className:e,...n}));return i.name&&(a.displayName=c(i.name)),a}var y={name:`check`,size:24,node:[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]};y.node;var b=v(y),x={name:`copy`,size:24,node:[[`rect`,{width:`14`,height:`14`,x:`8`,y:`8`,rx:`2`,ry:`2`,key:`17jyea`}],[`path`,{d:`M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2`,key:`zix9uf`}]]};x.node;var S=v(x),C={name:`message-square-text`,size:24,node:[[`path`,{d:`M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z`,key:`18887p`}],[`path`,{d:`M7 11h10`,key:`1twpyw`}],[`path`,{d:`M7 15h6`,key:`d9of3u`}],[`path`,{d:`M7 7h8`,key:`af5zfr`}]]};C.node;var w=v(C),T={de:{title:`SMS`,intro:`Wizards laufen per SMS auf der eigenen Nummer: eine Frage pro Nachricht, Auswahl als Nummern, Bilder und Dateien als Link. Dafür braucht es ein Twilio-Konto mit einer SMS-fähigen Nummer.`,accountSid:`Account SID`,accountSidHint:`Aus der Twilio-Konsole, beginnt mit AC.`,token:`Auth Token`,tokenHint:`Der Auth Token des Kontos. Damit wird auch jede Nachricht von Twilio auf ihre Signatur geprüft.`,tokenKept:`Ist hinterlegt. Leer lassen, um ihn zu behalten.`,number:`Nummer`,numberHint:`Die Twilio-Nummer, von der die Texte kommen, mit Ländervorwahl: +43660…`,save:`Speichern`,saved:`Gespeichert.`,disconnect:`Trennen`,twilio:`In Twilio eintragen`,twilioHint:`Bei der Nummer unter Messaging → „A message comes in“: diese Adresse, als Webhook mit HTTP POST.`,webhook:`Webhook-Adresse`,copy:`Kopieren`,copied:`Kopiert`,testTo:`Test-SMS an`,sendTest:`Senden`,testSent:`Angekommen? Dann passt alles.`,keyword:`Stichwort`,keywordHint:`Wer dieses Wort an die Nummer schreibt, startet den Wizard. Der Link und der QR-Code tippen es vor.`,error_bad_keyword:`Ein Stichwort besteht aus Buchstaben, Ziffern, Leerzeichen und Bindestrichen, bis zu 40.`,error_keyword_taken:`Ein anderer Wizard hat dieses Stichwort schon.`,error_no_token:`Es braucht einen Auth Token.`,error_provider:`Twilio hat die Nachricht nicht angenommen: {message}`,error_other:`{message}`},en:{title:`SMS`,intro:`Wizards run by text message on your own number: one question per text, choices as numbers, pictures and files as links. It takes a Twilio account with a number that can text.`,accountSid:`Account SID`,accountSidHint:`From the Twilio console, starts with AC.`,token:`Auth token`,tokenHint:`The account's auth token. It also checks every message from Twilio for its signature.`,tokenKept:`Is set. Leave empty to keep it.`,number:`Number`,numberHint:`The Twilio number the texts come from, with the country code: +43660…`,save:`Save`,saved:`Saved.`,disconnect:`Disconnect`,twilio:`Enter in Twilio`,twilioHint:`On the number under Messaging → “A message comes in”: this address, as a webhook with HTTP POST.`,webhook:`Webhook address`,copy:`Copy`,copied:`Copied`,testTo:`Test text to`,sendTest:`Send`,testSent:`Arrived? Then everything is set.`,keyword:`Keyword`,keywordHint:`Whoever texts this word to the number starts the wizard. The link and the QR code type it for them.`,error_bad_keyword:`A keyword is letters, digits, spaces and dashes, up to 40.`,error_keyword_taken:`Another wizard has this keyword already.`,error_no_token:`An auth token is needed.`,error_provider:`Twilio did not take the message: {message}`,error_other:`{message}`}},E=null,D=null;function O(e){E=e,D=e.i18n.register(T)}function k(){if(!E)throw Error(`The SMS plugin is not loaded.`);return E}var A=(e,t)=>D?.(e,t)??e,j=()=>k().api;function M(e){let t=e?.body,n=t?.code,r=t?.error??(e instanceof Error?e.message:String(e));return n&&`error_${n}`in T.de?A(`error_${n}`,{message:r}):A(`error_other`,{message:r})}function N({text:e}){let[r,a]=(0,t.useState)(!1);return(0,i.jsx)(n.IconButton,{label:A(r?`copied`:`copy`),className:`size-8`,onClick:()=>{navigator.clipboard.writeText(e).then(()=>{a(!0),setTimeout(()=>a(!1),1500)})},children:r?(0,i.jsx)(b,{className:`size-4`}):(0,i.jsx)(S,{className:`size-4`})})}function P({label:e,value:t}){return(0,i.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,i.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,i.jsx)(`div`,{className:`text-[0.8125rem] text-ink-3`,children:e}),(0,i.jsx)(`div`,{className:`select-all truncate font-mono text-[0.8125rem]`,children:t})]}),(0,i.jsx)(N,{text:t})]})}function F(){let e=(0,r.useQueryClient)(),a=(0,r.useQuery)({queryKey:[`sms`,`account`],queryFn:()=>j().get(`/account`)}),[o,s]=(0,t.useState)({accountSid:``,number:``,authToken:``}),[c,l]=(0,t.useState)(``);(0,t.useEffect)(()=>{a.data&&s(e=>({...e,accountSid:a.data.accountSid,number:a.data.number}))},[a.data]);let u=()=>e.invalidateQueries({queryKey:[`sms`]}),d=(0,r.useMutation)({mutationFn:()=>j().put(`/account`,{accountSid:o.accountSid,number:o.number,authToken:o.authToken||void 0}),onSuccess:async()=>{s(e=>({...e,authToken:``})),await u()}}),f=(0,r.useMutation)({mutationFn:()=>j().del(`/account`),onSuccess:u}),p=(0,r.useMutation)({mutationFn:()=>j().post(`/account/test`,{to:c})}),m=e=>t=>s(n=>({...n,[e]:t.target.value})),h=a.data;return(0,i.jsxs)(`section`,{className:`flex flex-col gap-2.5`,children:[(0,i.jsx)(`h2`,{className:`px-1 font-display font-semibold text-lg leading-tight`,children:A(`title`)}),(0,i.jsxs)(n.Card,{className:`flex flex-col gap-4 p-5 text-[0.875rem]`,children:[(0,i.jsx)(`p`,{className:`text-[0.8125rem] text-ink-3`,children:A(`intro`)}),a.isLoading?(0,i.jsx)(n.Spinner,{className:`mx-auto`}):null,(0,i.jsxs)(`div`,{children:[(0,i.jsx)(n.Label,{hint:A(`accountSidHint`),children:A(`accountSid`)}),(0,i.jsx)(n.Input,{value:o.accountSid,onChange:m(`accountSid`),autoComplete:`off`})]}),(0,i.jsxs)(`div`,{children:[(0,i.jsx)(n.Label,{hint:h?.hasToken?A(`tokenKept`):A(`tokenHint`),children:A(`token`)}),(0,i.jsx)(n.Input,{type:`password`,value:o.authToken,onChange:m(`authToken`),placeholder:h?.hasToken?`••••••••`:``,autoComplete:`new-password`})]}),(0,i.jsxs)(`div`,{children:[(0,i.jsx)(n.Label,{hint:A(`numberHint`),children:A(`number`)}),(0,i.jsx)(n.Input,{value:o.number,onChange:m(`number`),inputMode:`tel`})]}),(0,i.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,i.jsx)(n.Button,{onClick:()=>d.mutate(),busy:d.isPending,children:A(`save`)}),h?.connected?(0,i.jsx)(n.Button,{variant:`ghost`,onClick:()=>f.mutate(),busy:f.isPending,children:A(`disconnect`)}):null,d.isSuccess?(0,i.jsx)(`span`,{className:`text-[0.8125rem] text-ink-3`,children:A(`saved`)}):null]}),d.error?(0,i.jsx)(`p`,{className:`text-[0.8125rem] text-rose`,children:M(d.error)}):null]}),h?.connected?(0,i.jsxs)(n.Card,{className:`flex flex-col gap-4 p-5 text-[0.875rem]`,children:[(0,i.jsx)(`div`,{className:`font-medium`,children:A(`twilio`)}),(0,i.jsx)(`p`,{className:`text-[0.8125rem] text-ink-3`,children:A(`twilioHint`)}),(0,i.jsx)(P,{label:A(`webhook`),value:h.webhook}),(0,i.jsxs)(`div`,{children:[(0,i.jsx)(n.Label,{children:A(`testTo`)}),(0,i.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,i.jsx)(n.Input,{value:c,onChange:e=>l(e.target.value),placeholder:`+43 660 …`,inputMode:`tel`,className:`max-w-xs`}),(0,i.jsx)(n.Button,{variant:`secondary`,onClick:()=>p.mutate(),busy:p.isPending,children:A(`sendTest`)})]}),p.isSuccess?(0,i.jsx)(`p`,{className:`mt-2 text-[0.8125rem] text-ink-3`,children:A(`testSent`)}):null,p.error?(0,i.jsx)(`p`,{className:`mt-2 text-[0.8125rem] text-rose`,children:M(p.error)}):null]})]}):null]})}function I({wizard:e}){let a=(0,r.useQueryClient)(),o=(0,r.useQuery)({queryKey:[`sms`,`binding`,e.id],queryFn:()=>j().get(`/binding/${e.id}`)}),[s,c]=(0,t.useState)(null),l=(0,r.useMutation)({mutationFn:t=>j().put(`/binding/${e.id}`,{keyword:t}),onSuccess:async()=>{c(null),await Promise.all([a.invalidateQueries({queryKey:[`sms`,`binding`,e.id]}),a.invalidateQueries({queryKey:[`wizard-runners`,e.id]})])}});if(!o.data)return null;let u=s??o.data.keyword;return(0,i.jsxs)(`div`,{className:`flex flex-col gap-1.5`,children:[(0,i.jsxs)(`label`,{className:`flex flex-col gap-1.5`,children:[(0,i.jsx)(`span`,{className:`font-medium text-[0.875rem]`,children:A(`keyword`)}),(0,i.jsx)(n.Input,{value:u,onChange:e=>c(e.target.value),onBlur:()=>{s?.trim()&&s.trim()!==o.data?.keyword?l.mutate(s):c(null)},onKeyDown:e=>{e.key===`Enter`&&e.currentTarget.blur()},className:`max-w-xs`})]}),(0,i.jsx)(`p`,{className:`text-[0.8125rem] text-ink-3`,children:A(`keywordHint`)}),l.error?(0,i.jsx)(`p`,{className:`text-[0.8125rem] text-rose`,children:M(l.error)}):null]})}return(0,e.defineStudioPlugin)(e=>{O(e),e.registerSettingsSection({id:`sms`,label:()=>A(`title`),icon:w,component:F}),e.registerShareSection({runner:`sms`,component:I})})})(__WIZARDS_STUDIO__.sdk,__WIZARDS_STUDIO__.react,__WIZARDS_STUDIO__.ui,__WIZARDS_STUDIO__.query,__WIZARDS_STUDIO__.jsx);
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "sms",
|
|
3
|
-
"name": "SMS",
|
|
4
|
-
"version": "0.1.0",
|
|
5
|
-
"description": "Runs wizards by text message: a keyword to the tenant's number starts one, the fields come one message at a time with numbered choices, and what the wizard makes arrives as text and links. Talks to Twilio with the tenant's own account and number; keeps the threads in the tenant's database. Text only: photos, files and signatures go to the wizard's page by link.",
|
|
6
|
-
"kind": "module",
|
|
7
|
-
"provides": ["runner.sms"]
|
|
8
|
-
}
|