@agent-native/core 0.191.0 → 0.192.0-nightly-20260926045737
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/corpus/README.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +4 -0
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +41 -12
- package/corpus/templates/analytics/app/lib/chat-handoff.ts +62 -0
- package/corpus/templates/analytics/changelog/2026-09-25-chat-sidebar-stays-closed-in-settings.md +6 -0
- package/corpus/templates/analytics/changelog/2026-09-25-show-one-recent-prompt-per-chat-turn.md +6 -0
- package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +74 -11
- package/corpus/templates/clips/app/i18n/en-US.ts +6 -0
- package/corpus/templates/clips/changelog/2026-09-25-one-click-builder-storage.md +6 -0
- package/corpus/templates/design/actions/get-design.schema.ts +19 -0
- package/corpus/templates/design/actions/get-design.ts +16 -19
- package/corpus/templates/mail/actions/apply-ai-filter.ts +8 -0
- package/corpus/templates/mail/actions/create-automation.ts +7 -1
- package/corpus/templates/mail/actions/manage-ai-filter-rule-undo.ts +34 -0
- package/corpus/templates/mail/actions/manage-automations.ts +21 -44
- package/corpus/templates/mail/app/components/email/EmailList.tsx +35 -0
- package/corpus/templates/mail/app/components/onboarding/AiInboxSetup.tsx +116 -34
- package/corpus/templates/mail/app/components/settings/AiFilterSection.tsx +108 -70
- package/corpus/templates/mail/app/components/settings/AiRulePromptField.tsx +8 -1
- package/corpus/templates/mail/app/components/settings/JevConnectionPrompt.tsx +171 -0
- package/corpus/templates/mail/app/hooks/use-automations.ts +24 -0
- package/corpus/templates/mail/app/i18n/en-US.ts +8 -2
- package/corpus/templates/mail/app/pages/InboxPage.tsx +12 -2
- package/corpus/templates/mail/changelog/2026-09-25-mail-now-guides-users-to-connect-jev-before-setting-up-triag.md +6 -0
- package/corpus/templates/mail/server/db/schema.ts +17 -0
- package/corpus/templates/mail/server/lib/ai-filter-rule-undo.ts +151 -0
- package/corpus/templates/mail/server/lib/automations.ts +34 -0
- package/corpus/templates/mail/server/plugins/db.ts +18 -0
- package/corpus/templates/mail/server/plugins/mail-jobs.ts +6 -0
- package/dist/client/AssistantChat.js +20 -3
- package/dist/client/BuilderReferralInviteRow.d.ts +3 -0
- package/dist/client/BuilderReferralInviteRow.js +16 -0
- package/dist/client/agent-chat-adapter.js +6 -5
- package/dist/client/chat/run-recovery.js +6 -5
- package/dist/client/settings/UsageSection.js +2 -1
- package/dist/client/settings/index.d.ts +1 -1
- package/dist/client/settings/index.js +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +4 -1
- package/dist/client/settings/useBuilderStatus.js +32 -16
- package/dist/client/use-agent-chat-running-threads.d.ts +2 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/framework-tools.d.ts +3 -1
- package/dist/framework-tools.js +2 -0
- package/dist/localization/core-messages/ar-SA.js +4 -0
- package/dist/localization/core-messages/de-DE.js +4 -0
- package/dist/localization/core-messages/en-US.d.ts +4 -0
- package/dist/localization/core-messages/en-US.js +4 -0
- package/dist/localization/core-messages/es-ES.js +4 -0
- package/dist/localization/core-messages/fr-FR.js +4 -0
- package/dist/localization/core-messages/hi-IN.js +4 -0
- package/dist/localization/core-messages/ja-JP.js +4 -0
- package/dist/localization/core-messages/ko-KR.js +4 -0
- package/dist/localization/core-messages/pt-BR.js +4 -0
- package/dist/localization/core-messages/zh-CN.js +4 -0
- package/dist/localization/core-messages/zh-TW.js +4 -0
- package/dist/localization/core-messages.d.ts +4 -0
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +14 -14
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/action-discovery.js +4 -0
- package/dist/server/agent-chat/plugin-options.d.ts +1 -1
- package/dist/server/agent-chat-plugin.js +6 -4
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/dist/server/fusion-app.d.ts +3 -0
- package/dist/server/fusion-app.js +17 -0
- package/dist/shared/builder-referrals.d.ts +10 -0
- package/dist/shared/builder-referrals.js +29 -0
- package/dist/usage/actions/get-builder-referral-info.d.ts +9 -0
- package/dist/usage/actions/get-builder-referral-info.js +26 -0
- package/dist/usage/metrics-store.js +142 -30
- package/dist/vite/action-types-plugin.js +4 -0
- package/docs/content/actions-agent-tools.mdx +2 -1
- package/package.json +3 -2
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
actionErrorMessage,
|
|
3
|
+
useActionQuery,
|
|
4
|
+
} from "@agent-native/core/client/hooks";
|
|
2
5
|
import { useT } from "@agent-native/core/client/i18n";
|
|
3
6
|
import { AI_FILTER_LABEL, AI_FILTER_RULE_NAME } from "@shared/ai-filter";
|
|
4
7
|
import { AI_IMPORTANT_LABEL } from "@shared/ai-priority";
|
|
@@ -18,6 +21,10 @@ import {
|
|
|
18
21
|
TAG_SUGGESTIONS,
|
|
19
22
|
} from "@/components/onboarding/AiInboxSetup";
|
|
20
23
|
import { AiRulePromptField } from "@/components/settings/AiRulePromptField";
|
|
24
|
+
import {
|
|
25
|
+
JevAvailabilityError,
|
|
26
|
+
JevConnectionPrompt,
|
|
27
|
+
} from "@/components/settings/JevConnectionPrompt";
|
|
21
28
|
import { Button } from "@/components/ui/button";
|
|
22
29
|
import { Input } from "@/components/ui/input";
|
|
23
30
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
@@ -30,9 +37,11 @@ import {
|
|
|
30
37
|
import { useManageAiFilter, useAiFilter } from "@/hooks/use-ai-filter";
|
|
31
38
|
import {
|
|
32
39
|
useAutomations,
|
|
40
|
+
useClearAiFilterRules,
|
|
33
41
|
useConsolidateAiFilterRules,
|
|
34
42
|
useCreateAutomation,
|
|
35
43
|
useDeleteAutomation,
|
|
44
|
+
useRestoreAiFilterRules,
|
|
36
45
|
useUpdateAutomation,
|
|
37
46
|
} from "@/hooks/use-automations";
|
|
38
47
|
import { useLabels, useSettings, useUpdateSettings } from "@/hooks/use-emails";
|
|
@@ -41,12 +50,6 @@ import { useGoogleAuthStatus } from "@/hooks/use-google-auth";
|
|
|
41
50
|
type RuleMode = "tag" | "important" | "archive" | "spam";
|
|
42
51
|
type PromptMode = Exclude<RuleMode, "tag">;
|
|
43
52
|
|
|
44
|
-
function makeAggregateError(errors: unknown[], message: string) {
|
|
45
|
-
const error = new Error(message);
|
|
46
|
-
error.name = "AggregateError";
|
|
47
|
-
return Object.assign(error, { errors });
|
|
48
|
-
}
|
|
49
|
-
|
|
50
53
|
const PROMPT_MODES: PromptMode[] = ["important", "archive", "spam"];
|
|
51
54
|
|
|
52
55
|
const labelForRule = (rule: AutomationRule) => {
|
|
@@ -97,6 +100,7 @@ function promptForRules(rules: AutomationRule[]) {
|
|
|
97
100
|
function AiTagRow({
|
|
98
101
|
rule,
|
|
99
102
|
expanded,
|
|
103
|
+
disabled,
|
|
100
104
|
onToggle,
|
|
101
105
|
onSave,
|
|
102
106
|
onDelete,
|
|
@@ -104,6 +108,7 @@ function AiTagRow({
|
|
|
104
108
|
}: {
|
|
105
109
|
rule: AutomationRule;
|
|
106
110
|
expanded: boolean;
|
|
111
|
+
disabled: boolean;
|
|
107
112
|
onToggle: () => void;
|
|
108
113
|
onSave: (rule: AutomationRule, name: string, condition: string) => void;
|
|
109
114
|
onDelete: (rule: AutomationRule) => void;
|
|
@@ -122,8 +127,10 @@ function AiTagRow({
|
|
|
122
127
|
|
|
123
128
|
return (
|
|
124
129
|
<div
|
|
125
|
-
draggable={!expanded}
|
|
126
|
-
onDragStart={(event) =>
|
|
130
|
+
draggable={!expanded && !disabled}
|
|
131
|
+
onDragStart={(event) => {
|
|
132
|
+
if (!disabled) event.dataTransfer.setData("text/plain", rule.id);
|
|
133
|
+
}}
|
|
127
134
|
onDragOver={(event) => event.preventDefault()}
|
|
128
135
|
onDrop={(event) => onDrop(event, rule.id)}
|
|
129
136
|
className="group border-b border-border/40 last:border-0"
|
|
@@ -134,6 +141,7 @@ function AiTagRow({
|
|
|
134
141
|
type="button"
|
|
135
142
|
className="grid min-w-0 flex-1 grid-cols-[110px_minmax(0,1fr)] items-center gap-3 text-left"
|
|
136
143
|
aria-expanded={expanded}
|
|
144
|
+
disabled={disabled}
|
|
137
145
|
onClick={onToggle}
|
|
138
146
|
>
|
|
139
147
|
<span className="truncate text-sm font-medium text-foreground">
|
|
@@ -146,7 +154,7 @@ function AiTagRow({
|
|
|
146
154
|
<Button
|
|
147
155
|
variant="ghost"
|
|
148
156
|
size="icon"
|
|
149
|
-
className=
|
|
157
|
+
className={`size-7 text-muted-foreground hover:text-destructive ${disabled ? "" : "opacity-0 group-hover:opacity-100 focus-visible:opacity-100"}`}
|
|
150
158
|
aria-label={t("mail.aiFilter.deleteInstruction")}
|
|
151
159
|
onClick={() => onDelete(rule)}
|
|
152
160
|
>
|
|
@@ -159,6 +167,7 @@ function AiTagRow({
|
|
|
159
167
|
value={name}
|
|
160
168
|
onChange={(event) => setName(event.target.value)}
|
|
161
169
|
onBlur={save}
|
|
170
|
+
disabled={disabled}
|
|
162
171
|
aria-label={t("mail.aiFilter.tagNamePlaceholder")}
|
|
163
172
|
placeholder={t("mail.aiFilter.tagNamePlaceholder")}
|
|
164
173
|
/>
|
|
@@ -166,6 +175,7 @@ function AiTagRow({
|
|
|
166
175
|
value={condition}
|
|
167
176
|
onChange={setCondition}
|
|
168
177
|
onBlur={save}
|
|
178
|
+
disabled={disabled}
|
|
169
179
|
label={t("mail.aiFilter.tagPlaceholder")}
|
|
170
180
|
placeholder={t("mail.aiFilter.tagPlaceholder")}
|
|
171
181
|
/>
|
|
@@ -182,9 +192,26 @@ export function AiFilterSection() {
|
|
|
182
192
|
const { data: settings } = useSettings();
|
|
183
193
|
const { data: labels = [] } = useLabels();
|
|
184
194
|
const googleStatus = useGoogleAuthStatus();
|
|
195
|
+
const jevAvailability = useActionQuery(
|
|
196
|
+
"get-jev-availability",
|
|
197
|
+
{},
|
|
198
|
+
{
|
|
199
|
+
staleTime: 0,
|
|
200
|
+
// request-storm-allow: the shared status query revalidates API-key setup when its settings tab returns.
|
|
201
|
+
refetchOnWindowFocus: true,
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
const jevConfigured =
|
|
205
|
+
!jevAvailability.isError && jevAvailability.data?.configured === true;
|
|
206
|
+
const jevUnavailable =
|
|
207
|
+
!jevAvailability.isLoading &&
|
|
208
|
+
!jevAvailability.isError &&
|
|
209
|
+
jevAvailability.data?.configured === false;
|
|
185
210
|
const updateSettings = useManageAiFilter();
|
|
186
211
|
const updatePreferences = useUpdateSettings();
|
|
212
|
+
const clearAiFilterRules = useClearAiFilterRules();
|
|
187
213
|
const consolidateAiFilterRules = useConsolidateAiFilterRules();
|
|
214
|
+
const restoreAiFilterRules = useRestoreAiFilterRules();
|
|
188
215
|
const createRule = useCreateAutomation();
|
|
189
216
|
const updateRule = useUpdateAutomation();
|
|
190
217
|
const deleteRule = useDeleteAutomation();
|
|
@@ -256,6 +283,7 @@ export function AiFilterSection() {
|
|
|
256
283
|
}, [promptRules]);
|
|
257
284
|
|
|
258
285
|
const updateAiSettings = (enabled: boolean) => {
|
|
286
|
+
if (enabled && !jevConfigured) return;
|
|
259
287
|
updateSettings.mutate(
|
|
260
288
|
{ mode: "settings", settings: { enabled } },
|
|
261
289
|
{
|
|
@@ -269,64 +297,37 @@ export function AiFilterSection() {
|
|
|
269
297
|
);
|
|
270
298
|
};
|
|
271
299
|
|
|
272
|
-
const savePrompt = async (mode: PromptMode) => {
|
|
273
|
-
const condition = promptDrafts[mode].trim();
|
|
300
|
+
const savePrompt = async (mode: PromptMode, clear = false) => {
|
|
301
|
+
const condition = clear ? "" : promptDrafts[mode].trim();
|
|
274
302
|
const existing = promptRules[mode];
|
|
275
|
-
const actions = actionsForMode(mode);
|
|
276
303
|
if (condition === promptForRules(existing)) return;
|
|
304
|
+
if (!jevConfigured && condition) {
|
|
305
|
+
setPromptDrafts((drafts) => ({
|
|
306
|
+
...drafts,
|
|
307
|
+
[mode]: promptForRules(existing),
|
|
308
|
+
}));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const actions = actionsForMode(mode);
|
|
277
312
|
|
|
278
|
-
const restoreRules = async (rulesToRestore: AutomationRule[]) => {
|
|
279
|
-
const errors: unknown[] = [];
|
|
280
|
-
for (const rule of rulesToRestore) {
|
|
281
|
-
try {
|
|
282
|
-
const restored = await createRule.mutateAsync({
|
|
283
|
-
name: rule.name,
|
|
284
|
-
condition: rule.condition,
|
|
285
|
-
actions: rule.actions,
|
|
286
|
-
kind: rule.kind,
|
|
287
|
-
domain: rule.domain,
|
|
288
|
-
});
|
|
289
|
-
if (!rule.enabled) {
|
|
290
|
-
await updateRule.mutateAsync({ id: restored.id, enabled: false });
|
|
291
|
-
}
|
|
292
|
-
} catch (error) {
|
|
293
|
-
errors.push(error);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
if (errors.length) {
|
|
297
|
-
throw makeAggregateError(
|
|
298
|
-
errors,
|
|
299
|
-
errors[0] instanceof Error
|
|
300
|
-
? errors[0].message
|
|
301
|
-
: t("mail.aiFilter.instructionFailed"),
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
313
|
try {
|
|
306
314
|
if (!condition) {
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
await deleteRule.mutateAsync(rule.id);
|
|
311
|
-
removed.push(rule);
|
|
312
|
-
}
|
|
313
|
-
} catch (error) {
|
|
314
|
-
try {
|
|
315
|
-
await restoreRules(removed);
|
|
316
|
-
} catch (restoreError) {
|
|
317
|
-
throw makeAggregateError(
|
|
318
|
-
[error, restoreError],
|
|
319
|
-
error instanceof Error
|
|
320
|
-
? error.message
|
|
321
|
-
: t("mail.aiFilter.instructionFailed"),
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
throw error;
|
|
325
|
-
}
|
|
315
|
+
const { undoId } = await clearAiFilterRules.mutateAsync(
|
|
316
|
+
existing.map((rule) => rule.id),
|
|
317
|
+
);
|
|
326
318
|
toast(t("mail.aiFilter.promptRulesCleared"), {
|
|
327
319
|
action: {
|
|
328
320
|
label: t("mail.actions.undo"),
|
|
329
|
-
onClick: () =>
|
|
321
|
+
onClick: () => {
|
|
322
|
+
void restoreAiFilterRules
|
|
323
|
+
.mutateAsync(undoId)
|
|
324
|
+
.catch((error) =>
|
|
325
|
+
toast.error(
|
|
326
|
+
actionErrorMessage(error) ??
|
|
327
|
+
t("mail.aiFilter.instructionFailed"),
|
|
328
|
+
),
|
|
329
|
+
);
|
|
330
|
+
},
|
|
330
331
|
},
|
|
331
332
|
});
|
|
332
333
|
return;
|
|
@@ -384,6 +385,7 @@ export function AiFilterSection() {
|
|
|
384
385
|
};
|
|
385
386
|
|
|
386
387
|
const saveNewTag = async () => {
|
|
388
|
+
if (!jevConfigured) return;
|
|
387
389
|
const name = newTagName.trim();
|
|
388
390
|
const condition = newTagPrompt.trim();
|
|
389
391
|
if (!name || !condition || savingNewTag) return;
|
|
@@ -412,7 +414,7 @@ export function AiFilterSection() {
|
|
|
412
414
|
};
|
|
413
415
|
|
|
414
416
|
const saveSuggestedTag = async (nameKey: string, promptKey: string) => {
|
|
415
|
-
if (savingSuggestedTag) return;
|
|
417
|
+
if (!jevConfigured || savingSuggestedTag) return;
|
|
416
418
|
const name = t(nameKey);
|
|
417
419
|
const condition = t(promptKey);
|
|
418
420
|
setSavingSuggestedTag(nameKey);
|
|
@@ -441,6 +443,7 @@ export function AiFilterSection() {
|
|
|
441
443
|
nameDraft: string,
|
|
442
444
|
conditionDraft: string,
|
|
443
445
|
) => {
|
|
446
|
+
if (!jevConfigured) return;
|
|
444
447
|
const name = nameDraft.trim() || labelForRule(rule);
|
|
445
448
|
const condition = conditionDraft.trim() || rule.condition;
|
|
446
449
|
if (!name || !condition) return;
|
|
@@ -499,7 +502,7 @@ export function AiFilterSection() {
|
|
|
499
502
|
};
|
|
500
503
|
|
|
501
504
|
const reorderTags = async (draggedId: string, targetId: string) => {
|
|
502
|
-
if (draggedId === targetId) return;
|
|
505
|
+
if (!jevConfigured || draggedId === targetId) return;
|
|
503
506
|
const orderedNames = tagRules.map((rule) => labelForRule(rule));
|
|
504
507
|
const from = tagRules.findIndex((rule) => rule.id === draggedId);
|
|
505
508
|
const to = tagRules.findIndex((rule) => rule.id === targetId);
|
|
@@ -543,9 +546,23 @@ export function AiFilterSection() {
|
|
|
543
546
|
checked={state.enabled}
|
|
544
547
|
onCheckedChange={updateAiSettings}
|
|
545
548
|
aria-label={t("mail.aiFilter.toggle")}
|
|
549
|
+
disabled={!jevConfigured && !state.enabled}
|
|
546
550
|
/>
|
|
547
551
|
</div>
|
|
548
552
|
|
|
553
|
+
{jevAvailability.isLoading ? (
|
|
554
|
+
<Skeleton className="h-16 w-full" />
|
|
555
|
+
) : jevAvailability.isError ? (
|
|
556
|
+
<JevAvailabilityError
|
|
557
|
+
onRetry={() => void jevAvailability.refetch()}
|
|
558
|
+
retrying={jevAvailability.isFetching}
|
|
559
|
+
/>
|
|
560
|
+
) : !jevConfigured ? (
|
|
561
|
+
<JevConnectionPrompt
|
|
562
|
+
onConnected={() => void jevAvailability.refetch()}
|
|
563
|
+
/>
|
|
564
|
+
) : null}
|
|
565
|
+
|
|
549
566
|
<section id="tags" className="space-y-2">
|
|
550
567
|
<div className="flex items-center justify-between">
|
|
551
568
|
<div className="flex items-center gap-1.5">
|
|
@@ -571,6 +588,7 @@ export function AiFilterSection() {
|
|
|
571
588
|
variant="ghost"
|
|
572
589
|
size="sm"
|
|
573
590
|
className="h-7 px-2 text-xs"
|
|
591
|
+
disabled={!jevConfigured}
|
|
574
592
|
onClick={() => {
|
|
575
593
|
setNewTagOpen(true);
|
|
576
594
|
setExpandedTagId("new");
|
|
@@ -588,6 +606,7 @@ export function AiFilterSection() {
|
|
|
588
606
|
key={rule.id}
|
|
589
607
|
rule={rule}
|
|
590
608
|
expanded={expandedTagId === rule.id}
|
|
609
|
+
disabled={!jevConfigured}
|
|
591
610
|
onToggle={() =>
|
|
592
611
|
setExpandedTagId((current) =>
|
|
593
612
|
current === rule.id ? null : rule.id,
|
|
@@ -599,6 +618,7 @@ export function AiFilterSection() {
|
|
|
599
618
|
onDelete={removeTag}
|
|
600
619
|
onDrop={(event, targetId) => {
|
|
601
620
|
event.preventDefault();
|
|
621
|
+
if (!jevConfigured) return;
|
|
602
622
|
void reorderTags(
|
|
603
623
|
event.dataTransfer.getData("text/plain"),
|
|
604
624
|
targetId,
|
|
@@ -613,6 +633,7 @@ export function AiFilterSection() {
|
|
|
613
633
|
value={newTagName}
|
|
614
634
|
onChange={(event) => setNewTagName(event.target.value)}
|
|
615
635
|
onBlur={() => void saveNewTag()}
|
|
636
|
+
disabled={!jevConfigured}
|
|
616
637
|
aria-label={t("mail.aiFilter.tagNamePlaceholder")}
|
|
617
638
|
placeholder={t("mail.aiFilter.tagNamePlaceholder")}
|
|
618
639
|
/>
|
|
@@ -620,6 +641,7 @@ export function AiFilterSection() {
|
|
|
620
641
|
value={newTagPrompt}
|
|
621
642
|
onChange={setNewTagPrompt}
|
|
622
643
|
onBlur={() => void saveNewTag()}
|
|
644
|
+
disabled={!jevConfigured}
|
|
623
645
|
label={t("mail.aiFilter.tagPlaceholder")}
|
|
624
646
|
placeholder={t("mail.aiFilter.tagPlaceholder")}
|
|
625
647
|
/>
|
|
@@ -633,6 +655,7 @@ export function AiFilterSection() {
|
|
|
633
655
|
value={newTagName}
|
|
634
656
|
onChange={(event) => setNewTagName(event.target.value)}
|
|
635
657
|
onBlur={() => void saveNewTag()}
|
|
658
|
+
disabled={!jevConfigured}
|
|
636
659
|
aria-label={t("mail.aiFilter.tagNamePlaceholder")}
|
|
637
660
|
placeholder={t("mail.aiFilter.tagNamePlaceholder")}
|
|
638
661
|
/>
|
|
@@ -640,6 +663,7 @@ export function AiFilterSection() {
|
|
|
640
663
|
value={newTagPrompt}
|
|
641
664
|
onChange={setNewTagPrompt}
|
|
642
665
|
onBlur={() => void saveNewTag()}
|
|
666
|
+
disabled={!jevConfigured}
|
|
643
667
|
label={t("mail.aiFilter.tagPlaceholder")}
|
|
644
668
|
placeholder={t("mail.aiFilter.tagPlaceholder")}
|
|
645
669
|
/>
|
|
@@ -651,7 +675,7 @@ export function AiFilterSection() {
|
|
|
651
675
|
key={nameKey}
|
|
652
676
|
variant="outline"
|
|
653
677
|
size="sm"
|
|
654
|
-
disabled={savingSuggestedTag !== null}
|
|
678
|
+
disabled={!jevConfigured || savingSuggestedTag !== null}
|
|
655
679
|
onClick={() => void saveSuggestedTag(nameKey, promptKey)}
|
|
656
680
|
>
|
|
657
681
|
<IconPlus className="size-3.5" />
|
|
@@ -668,15 +692,29 @@ export function AiFilterSection() {
|
|
|
668
692
|
id={mode === "important" ? "importance-rules" : `${mode}-rules`}
|
|
669
693
|
className="space-y-2 scroll-mt-6"
|
|
670
694
|
>
|
|
671
|
-
<
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
695
|
+
<div className="flex items-center justify-between">
|
|
696
|
+
<h3 className="text-[13px] font-semibold text-foreground">
|
|
697
|
+
{mode === "important"
|
|
698
|
+
? t("mail.aiFilter.importantMode")
|
|
699
|
+
: mode === "archive"
|
|
700
|
+
? t("mail.aiFilter.skipInboxMode")
|
|
701
|
+
: t("mail.aiFilter.spamMode")}
|
|
702
|
+
</h3>
|
|
703
|
+
{jevUnavailable && promptRules[mode].length > 0 && (
|
|
704
|
+
<Button
|
|
705
|
+
variant="ghost"
|
|
706
|
+
size="icon"
|
|
707
|
+
className="size-7 text-muted-foreground hover:text-destructive"
|
|
708
|
+
aria-label={t("mail.aiFilter.deleteInstruction")}
|
|
709
|
+
onClick={() => void savePrompt(mode, true)}
|
|
710
|
+
>
|
|
711
|
+
<IconTrash className="size-3.5" />
|
|
712
|
+
</Button>
|
|
713
|
+
)}
|
|
714
|
+
</div>
|
|
678
715
|
<AiRulePromptField
|
|
679
716
|
value={promptDrafts[mode]}
|
|
717
|
+
disabled={!jevConfigured}
|
|
680
718
|
onChange={(value) =>
|
|
681
719
|
setPromptDrafts((drafts) => ({ ...drafts, [mode]: value }))
|
|
682
720
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FocusEventHandler, KeyboardEventHandler } from "react";
|
|
2
2
|
|
|
3
3
|
import { Textarea } from "@/components/ui/textarea";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
4
5
|
|
|
5
6
|
export function AiRulePromptField({
|
|
6
7
|
value,
|
|
@@ -8,6 +9,7 @@ export function AiRulePromptField({
|
|
|
8
9
|
label,
|
|
9
10
|
placeholder,
|
|
10
11
|
className,
|
|
12
|
+
disabled,
|
|
11
13
|
onKeyDown,
|
|
12
14
|
onBlur,
|
|
13
15
|
defaultValue,
|
|
@@ -18,6 +20,7 @@ export function AiRulePromptField({
|
|
|
18
20
|
label: string;
|
|
19
21
|
placeholder?: string;
|
|
20
22
|
className?: string;
|
|
23
|
+
disabled?: boolean;
|
|
21
24
|
onKeyDown?: KeyboardEventHandler<HTMLTextAreaElement>;
|
|
22
25
|
onBlur?: FocusEventHandler<HTMLTextAreaElement>;
|
|
23
26
|
}) {
|
|
@@ -27,10 +30,14 @@ export function AiRulePromptField({
|
|
|
27
30
|
defaultValue={defaultValue}
|
|
28
31
|
onChange={(event) => onChange?.(event.target.value)}
|
|
29
32
|
placeholder={placeholder}
|
|
33
|
+
disabled={disabled}
|
|
30
34
|
onKeyDown={onKeyDown}
|
|
31
35
|
onBlur={onBlur}
|
|
32
36
|
aria-label={label}
|
|
33
|
-
className={
|
|
37
|
+
className={cn(
|
|
38
|
+
"placeholder:text-muted-foreground",
|
|
39
|
+
className ?? "min-h-28 resize-none text-sm",
|
|
40
|
+
)}
|
|
34
41
|
maxLength={2_000}
|
|
35
42
|
/>
|
|
36
43
|
);
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import { buildSettingsRoute } from "@agent-native/core/client/navigation";
|
|
3
|
+
import {
|
|
4
|
+
BuilderConnectPopover,
|
|
5
|
+
useBuilderConnectFlow,
|
|
6
|
+
} from "@agent-native/core/client/settings";
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
import { Link } from "react-router";
|
|
9
|
+
|
|
10
|
+
import { Button } from "@/components/ui/button";
|
|
11
|
+
import {
|
|
12
|
+
Dialog,
|
|
13
|
+
DialogContent,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle,
|
|
16
|
+
DialogTrigger,
|
|
17
|
+
} from "@/components/ui/dialog";
|
|
18
|
+
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|
19
|
+
|
|
20
|
+
export function JevConnectionPrompt({
|
|
21
|
+
variant = "row",
|
|
22
|
+
onConnected,
|
|
23
|
+
disabled = false,
|
|
24
|
+
showHeading = true,
|
|
25
|
+
}: {
|
|
26
|
+
variant?: "row" | "trigger" | "menu-item";
|
|
27
|
+
onConnected?: () => void;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
showHeading?: boolean;
|
|
30
|
+
}) {
|
|
31
|
+
const t = useT();
|
|
32
|
+
const [open, setOpen] = useState(false);
|
|
33
|
+
const flow = useBuilderConnectFlow({
|
|
34
|
+
trackingSource: "mail_jev_triage_setup",
|
|
35
|
+
trackingFlow: "connect_jev",
|
|
36
|
+
onConnected,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const actions = (
|
|
40
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
41
|
+
<BuilderConnectPopover flow={flow}>
|
|
42
|
+
<Button
|
|
43
|
+
type="button"
|
|
44
|
+
disabled={flow.connecting}
|
|
45
|
+
aria-busy={flow.connecting}
|
|
46
|
+
className="h-8 px-3 text-xs"
|
|
47
|
+
>
|
|
48
|
+
{flow.connecting
|
|
49
|
+
? t("mail.accounts.connecting")
|
|
50
|
+
: t("mail.aiFilter.connectBuilder")}
|
|
51
|
+
</Button>
|
|
52
|
+
</BuilderConnectPopover>
|
|
53
|
+
<Link
|
|
54
|
+
to={buildSettingsRoute("keys:secrets:JEV_API_KEY")}
|
|
55
|
+
target="_blank"
|
|
56
|
+
rel="noreferrer"
|
|
57
|
+
className="whitespace-nowrap text-xs font-medium text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"
|
|
58
|
+
>
|
|
59
|
+
{t("mail.aiFilter.addJevApiKey")}
|
|
60
|
+
</Link>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const dialogContent = (
|
|
65
|
+
<DialogContent className="sm:max-w-[420px]">
|
|
66
|
+
<DialogHeader>
|
|
67
|
+
<DialogTitle>{t("mail.aiFilter.connectJev")}</DialogTitle>
|
|
68
|
+
</DialogHeader>
|
|
69
|
+
<p className="text-sm text-muted-foreground">
|
|
70
|
+
{t("mail.aiFilter.freeBuilderOrApiKey")}
|
|
71
|
+
</p>
|
|
72
|
+
{actions}
|
|
73
|
+
{flow.error ? (
|
|
74
|
+
<p className="text-xs text-destructive">{flow.error}</p>
|
|
75
|
+
) : null}
|
|
76
|
+
</DialogContent>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (variant === "menu-item") {
|
|
80
|
+
return (
|
|
81
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
82
|
+
<DropdownMenuItem
|
|
83
|
+
onSelect={(event) => {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
setOpen(true);
|
|
86
|
+
}}
|
|
87
|
+
disabled={disabled || flow.connecting}
|
|
88
|
+
className="justify-between"
|
|
89
|
+
>
|
|
90
|
+
{t("mail.sort.priority")}
|
|
91
|
+
<span className="text-xs text-muted-foreground">
|
|
92
|
+
{t("mail.aiFilter.connectJev")}
|
|
93
|
+
</span>
|
|
94
|
+
</DropdownMenuItem>
|
|
95
|
+
{dialogContent}
|
|
96
|
+
</Dialog>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (variant === "trigger") {
|
|
101
|
+
return (
|
|
102
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
103
|
+
<DialogTrigger asChild>
|
|
104
|
+
<Button
|
|
105
|
+
type="button"
|
|
106
|
+
variant="outline"
|
|
107
|
+
size="sm"
|
|
108
|
+
disabled={disabled || flow.connecting}
|
|
109
|
+
className="h-7 px-2 text-[11px]"
|
|
110
|
+
>
|
|
111
|
+
{t("mail.aiFilter.connectJev")}
|
|
112
|
+
</Button>
|
|
113
|
+
</DialogTrigger>
|
|
114
|
+
{dialogContent}
|
|
115
|
+
</Dialog>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="flex flex-col gap-2 rounded-lg border border-border/60 bg-muted/10 px-3 py-2.5 sm:flex-row sm:items-center sm:justify-between">
|
|
121
|
+
<div className="min-w-0">
|
|
122
|
+
{showHeading ? (
|
|
123
|
+
<p className="text-sm font-medium text-foreground">
|
|
124
|
+
{t("mail.aiFilter.connectJevToRunTriage")}
|
|
125
|
+
</p>
|
|
126
|
+
) : null}
|
|
127
|
+
<p className="text-xs text-muted-foreground">
|
|
128
|
+
{t("mail.aiFilter.freeBuilderOrApiKey")}
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
{actions}
|
|
132
|
+
{flow.error ? (
|
|
133
|
+
<p className="basis-full text-xs text-destructive">{flow.error}</p>
|
|
134
|
+
) : null}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function JevAvailabilityError({
|
|
140
|
+
onRetry,
|
|
141
|
+
retrying = false,
|
|
142
|
+
showMessage = true,
|
|
143
|
+
}: {
|
|
144
|
+
onRetry: () => void;
|
|
145
|
+
retrying?: boolean;
|
|
146
|
+
showMessage?: boolean;
|
|
147
|
+
}) {
|
|
148
|
+
const t = useT();
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
role="alert"
|
|
152
|
+
className="flex items-center justify-between gap-3 rounded-lg border border-border/60 px-3 py-2.5"
|
|
153
|
+
>
|
|
154
|
+
{showMessage ? (
|
|
155
|
+
<p className="text-sm text-muted-foreground">
|
|
156
|
+
{t("mail.aiFilter.jevAvailabilityFailed")}
|
|
157
|
+
</p>
|
|
158
|
+
) : null}
|
|
159
|
+
<Button
|
|
160
|
+
type="button"
|
|
161
|
+
variant="ghost"
|
|
162
|
+
size="sm"
|
|
163
|
+
disabled={retrying}
|
|
164
|
+
aria-busy={retrying}
|
|
165
|
+
onClick={onRetry}
|
|
166
|
+
>
|
|
167
|
+
{t("mail.error.tryAgain")}
|
|
168
|
+
</Button>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -112,6 +112,30 @@ export function useConsolidateAiFilterRules() {
|
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
export function useClearAiFilterRules() {
|
|
116
|
+
const qc = useQueryClient();
|
|
117
|
+
return useMutation({
|
|
118
|
+
mutationFn: (ids: string[]) =>
|
|
119
|
+
callAction("manage-ai-filter-rule-undo", {
|
|
120
|
+
operation: "clear",
|
|
121
|
+
ids,
|
|
122
|
+
}) as Promise<{ undoId: string }>,
|
|
123
|
+
onSettled: () => qc.invalidateQueries({ queryKey: ["automations"] }),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function useRestoreAiFilterRules() {
|
|
128
|
+
const qc = useQueryClient();
|
|
129
|
+
return useMutation({
|
|
130
|
+
mutationFn: (undoId: string) =>
|
|
131
|
+
callAction("manage-ai-filter-rule-undo", {
|
|
132
|
+
operation: "undo",
|
|
133
|
+
undoId,
|
|
134
|
+
}) as Promise<{ restored: true }>,
|
|
135
|
+
onSettled: () => qc.invalidateQueries({ queryKey: ["automations"] }),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
115
139
|
export function useTriggerAutomations() {
|
|
116
140
|
return useMutation({
|
|
117
141
|
mutationFn: () =>
|
|
@@ -42,13 +42,13 @@ const messages = {
|
|
|
42
42
|
aiSetupTagLabel: "Create an AI tag",
|
|
43
43
|
aiSetupImportanceLabel: "Important mail",
|
|
44
44
|
aiSetupSpamLabel: "Spam and unwanted mail",
|
|
45
|
-
aiSetupArchiveLabel: "
|
|
45
|
+
aiSetupArchiveLabel: "Skip inbox",
|
|
46
46
|
aiSetupSave: "Save setup",
|
|
47
47
|
aiSetupSkip: "Skip for now",
|
|
48
48
|
aiSetupImportantHeadline: "What’s important",
|
|
49
49
|
aiSetupSkipInboxHeadline: "What can skip your inbox",
|
|
50
50
|
aiSetupTagsHeadline: "Pick your tabs",
|
|
51
|
-
aiSetupArchiveSpamHeadline: "
|
|
51
|
+
aiSetupArchiveSpamHeadline: "Skip inbox and Spam",
|
|
52
52
|
aiSetupTagReceipts: "Receipts",
|
|
53
53
|
aiSetupTagUpdates: "Product updates",
|
|
54
54
|
aiSetupTagGitHub: "GitHub humans",
|
|
@@ -595,6 +595,12 @@ const messages = {
|
|
|
595
595
|
tagTabsHelp: "Each tag becomes an inbox tab",
|
|
596
596
|
addTag: "Add tag",
|
|
597
597
|
triageTitle: "Triage",
|
|
598
|
+
connectJev: "Connect Jev",
|
|
599
|
+
connectJevToRunTriage: "Connect Jev to run triage",
|
|
600
|
+
freeBuilderOrApiKey: "Free with Builder.io, or add an API key.",
|
|
601
|
+
jevAvailabilityFailed: "Could not check whether Jev is available.",
|
|
602
|
+
connectBuilder: "Connect Builder.io",
|
|
603
|
+
addJevApiKey: "Add API key",
|
|
598
604
|
},
|
|
599
605
|
draftQueue: {
|
|
600
606
|
title: "Draft queue",
|
|
@@ -366,11 +366,14 @@ export function InboxPage() {
|
|
|
366
366
|
{},
|
|
367
367
|
{
|
|
368
368
|
enabled: view === "inbox" || navState.command.data?.sort === "priority",
|
|
369
|
-
staleTime:
|
|
369
|
+
staleTime: 0,
|
|
370
|
+
// request-storm-allow: the shared status query revalidates API-key setup when its settings tab returns.
|
|
371
|
+
refetchOnWindowFocus: true,
|
|
370
372
|
retry: 2,
|
|
371
373
|
},
|
|
372
374
|
);
|
|
373
|
-
const jevConfigured =
|
|
375
|
+
const jevConfigured =
|
|
376
|
+
!jevAvailability.isError && jevAvailability.data?.configured === true;
|
|
374
377
|
const showPrioritySort =
|
|
375
378
|
jevConfigured || (jevAvailability.isError && sortMode === "priority");
|
|
376
379
|
const changeSortMode = useCallback((mode: MailSortMode) => {
|
|
@@ -1199,6 +1202,13 @@ export function InboxPage() {
|
|
|
1199
1202
|
isFetchNextPageError={isFetchNextPageError}
|
|
1200
1203
|
sortMode={sortMode}
|
|
1201
1204
|
showPrioritySort={showPrioritySort}
|
|
1205
|
+
jevConfigured={jevConfigured}
|
|
1206
|
+
jevAvailabilityLoading={
|
|
1207
|
+
jevAvailability.isLoading || jevAvailability.isFetching
|
|
1208
|
+
}
|
|
1209
|
+
jevAvailabilityError={jevAvailability.isError}
|
|
1210
|
+
onJevConnected={() => void jevAvailability.refetch()}
|
|
1211
|
+
onJevRetry={() => void jevAvailability.refetch()}
|
|
1202
1212
|
onSortModeChange={changeSortMode}
|
|
1203
1213
|
/>
|
|
1204
1214
|
)}
|