en_US and en are distinct.",
-
+
"header": "Header",
"headerNone": "None",
"headerText": "Text",
@@ -1356,7 +1357,7 @@
"headerTextPlaceholder": "Header text (max 60 chars, optional {{1}})",
"headerSampleAria": "Sample value for header variable",
"headerSamplePlaceholder": "Sample value for {{1}} (required for Meta review)",
-
+
"uploadImage": "Upload image",
"uploadHint": "JPEG or PNG, ≤5 MB",
"mediaUrlPlaceholder": "https://… (or paste a public {format} link)",
@@ -1364,17 +1365,17 @@
"mediaHint": "Must be a publicly accessible HTTPS link. Meta fetches it once during review, so it needs to stay live for ~24 hrs.",
"videoHint": " Recommended: MP4 / 3GPP, ≤16 MB, ≤60 seconds.",
"documentHint": " Recommended: PDF, ≤100 MB.",
-
+
"bodyText": "Body Text",
"bodyPlaceholder": "Hello {{1}}, your order {{2}} is confirmed.",
"bodyHint": "Use {{1}}, {{2}} for variables (must be contiguous starting at {{1}}).",
"sampleValues": "Sample values (Meta uses these to review your template)",
"sampleAria": "Sample value for body variable {var}",
"samplePlaceholder": "Sample for {var}",
-
+
"footer": "Footer (optional)",
"footerPlaceholder": "Optional footer text (max 60 chars)",
-
+
"buttons": "Buttons (optional)",
"addButton": "Add Button",
"buttonsLimit": "Up to {max} buttons. QUICK_REPLY buttons must come before URL / phone / copy-code buttons.",
@@ -1387,19 +1388,19 @@
"urlSamplePlaceholder": "Example value for {{1}} (required when URL has a variable)",
"phonePlaceholder": "+15551234567",
"codePlaceholder": "Example code (e.g. SUMMER20)",
-
+
"cancel": "Cancel",
"saving": "Saving…",
"submitting": "Submitting…",
"saveResubmit": "Save & Resubmit",
"submitApproval": "Submit for Approval",
-
+
"deleteDialogTitle": "Delete template?",
"deleteMetaDesc": "\"{name}\" will be deleted from Meta and from wacrm. Active broadcasts using this template will start failing on their next send. This can't be undone.",
"deleteLocalDesc": "\"{name}\" will be deleted from wacrm. It was never submitted to Meta, so no remote cleanup is needed.",
"deleting": "Deleting…",
"delete": "Delete",
-
+
"toastLoadFailed": "Failed to load templates",
"toastSubmitFailed": "Failed to submit",
"toastSubmitEditSuccess": "Edit submitted — Meta typically reviews within 24 hours.",
@@ -1539,7 +1540,7 @@
"profileSaved": "Profile saved",
"emailChangeFailed": "Email change failed: {message}",
"profileSavedEmailCheck": "Profile saved — check your email to confirm the address change",
-
+
"passwordTitle": "Password",
"passwordDesc": "Use at least {min} characters. You will stay signed in on this device after changing it.",
"currentPassword": "Current password",
@@ -1553,7 +1554,7 @@
"currentPasswordIncorrect": "Current password is incorrect",
"passwordUpdateFailed": "Password update failed: {message}",
"passwordUpdated": "Password updated",
-
+
"sessionsTitle": "Active sessions",
"sessionsDesc": "Sign out of every device where you're logged in — including this one. Useful if you lost a laptop or shared your password.",
"signOutAll": "Sign out of all devices",
@@ -1600,7 +1601,7 @@
"nameLabel": "Name",
"namePlaceholder": "e.g. Zapier automation",
"scopesLabel": "Scopes",
- "scopesHint": "A key with no scopes can still call GET /api/v1/me to verify it works.",
+ "scopesHint": "A key with no scopes can still call GET /api/v1/me to verify it works.",
"cancel": "Cancel",
"creating": "Creating…",
"createKey": "Create key",
@@ -1629,7 +1630,7 @@
},
"aiConfig": {
"title": "Agent setup",
- "description": "Bring your own OpenAI or Anthropic key. wacrm calls the provider directly with your key — no per-seat AI fees, and your data stays yours. This powers AI-drafted replies in the inbox, the auto-reply bot, and the Playground.",
+ "description": "Bring your own OpenAI, Anthropic, or Groq key. wacrm calls the provider directly with your key — no per-seat AI fees, and your data stays yours. This powers AI-drafted replies in the inbox, the auto-reply bot, and the Playground.",
"adminOnlyConfig": "Only admins and owners can change the AI configuration.",
"loadFailed": "Failed to load AI configuration",
"providerAndKey": "Provider & key",
diff --git a/messages/ko.json b/messages/ko.json
index 7c54fc939e..2e174e3fd5 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -931,6 +931,7 @@
"no": "아니요"
},
"addStep": "단계 추가",
+ "delete": "삭제",
"config": {
"messageText": "메시지 텍스트",
"placeholderMessageText": "안녕하세요! 문의해주셔서 감사합니다…",
@@ -1598,7 +1599,7 @@
"nameLabel": "이름",
"namePlaceholder": "예: Zapier 자동화",
"scopesLabel": "권한 범위",
- "scopesHint": "권한 범위가 없는 키도 GET /api/v1/me를 호출해 동작을 확인할 수 있습니다.",
+ "scopesHint": "권한 범위가 없는 키도 GET /api/v1/me를 호출해 동작을 확인할 수 있습니다.",
"cancel": "취소",
"creating": "만드는 중…",
"createKey": "키 생성",
@@ -1627,7 +1628,7 @@
},
"aiConfig": {
"title": "에이전트 설정",
- "description": "직접 발급받은 OpenAI 또는 Anthropic 키를 사용하세요. wacrm은 회원님의 키로 공급자에 직접 요청하므로 좌석당 AI 비용이 없고 데이터도 온전히 회원님의 것입니다. 인박스의 AI 답장 초안, 자동 응답 봇, 플레이그라운드에 사용됩니다.",
+ "description": "직접 발급받은 OpenAI, Anthropic, 또는 Groq 키를 사용하세요. wacrm은 회원님의 키로 공급자에 직접 요청하므로 좌석당 AI 비용이 없고 데이터도 온전히 회원님의 것입니다. 인박스의 AI 답장 초안, 자동 응답 봇, 플레이그라운드에 사용됩니다.",
"adminOnlyConfig": "관리자와 소유자만 AI 설정을 변경할 수 있습니다.",
"loadFailed": "AI 설정을 불러오지 못했습니다",
"providerAndKey": "공급자 & 키",
diff --git a/package-lock.json b/package-lock.json
index 92ddfffc44..4772361ea7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8660,17 +8660,6 @@
}
}
},
- "node_modules/next-intl/node_modules/@swc/helpers": {
- "version": "0.5.23",
- "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
- "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "tslib": "^2.8.0"
- }
- },
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
diff --git a/src/app/api/ai/config/route.ts b/src/app/api/ai/config/route.ts
index 08025dffbe..5ee74cdd4c 100644
--- a/src/app/api/ai/config/route.ts
+++ b/src/app/api/ai/config/route.ts
@@ -78,8 +78,8 @@ export async function POST(request: Request) {
if (!body || typeof body !== 'object') return bad('Invalid request body')
const provider = body.provider as AiProvider
- if (provider !== 'openai' && provider !== 'anthropic') {
- return bad('provider must be "openai" or "anthropic"')
+ if (provider !== 'openai' && provider !== 'anthropic' && provider !== 'groq') {
+ return bad('provider must be "openai", "anthropic", or "groq"')
}
const model = typeof body.model === 'string' ? body.model.trim() : ''
if (!model) return bad('model is required')
diff --git a/src/app/api/ai/test/route.ts b/src/app/api/ai/test/route.ts
index 8fa1f5f014..c4bf56410f 100644
--- a/src/app/api/ai/test/route.ts
+++ b/src/app/api/ai/test/route.ts
@@ -27,9 +27,9 @@ export async function POST(request: Request) {
}
const provider = body.provider as AiProvider
- if (provider !== 'openai' && provider !== 'anthropic') {
+ if (provider !== 'openai' && provider !== 'anthropic' && provider !== 'groq') {
return NextResponse.json(
- { error: 'provider must be "openai" or "anthropic"' },
+ { error: 'provider must be "openai", "anthropic", or "groq"' },
{ status: 400 },
)
}
diff --git a/src/components/settings/ai-config.tsx b/src/components/settings/ai-config.tsx
index 6028090935..d0690406de 100644
--- a/src/components/settings/ai-config.tsx
+++ b/src/components/settings/ai-config.tsx
@@ -41,11 +41,13 @@ const HANDOFF_QUEUE = '__queue__';
const PROVIDER_LABEL: Record