@anchrd/intel-ui 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/agent/agent-calendar/agent-calendar.tsx +32 -14
- package/src/agent/agent-chat/agent-chat.tsx +3 -1
- package/src/agent/agent-cron/agent-cron.ts +49 -113
- package/src/agent/agent-definition/agent-definition.ts +18 -9
- package/src/agent/agent-entry-title/agent-entry-title.ts +3 -1
- package/src/agent/agent-log/agent-log.tsx +6 -3
- package/src/agent/agent-profile/agent-profile.tsx +46 -12
- package/src/agent/agent.tsx +17 -9
- package/src/app/app-sidebar/app-sidebar.tsx +2 -2
- package/src/app/app-tree/app-tree.tsx +5 -3
- package/src/app/app.tsx +2 -2
- package/src/app/header-search/header-search.tsx +3 -1
- package/src/app/settings-dialog/settings-dialog.tsx +133 -0
- package/src/app/tree-move/tree-move.tsx +3 -1
- package/src/app/user-footer/user-footer.tsx +25 -2
- package/src/app/view-toggle/view-toggle.tsx +2 -2
- package/src/archive/archive.tsx +3 -1
- package/src/entry-picker/entry-picker.tsx +3 -1
- package/src/flow-runs/flow-runs.tsx +6 -3
- package/src/flows/flows.tsx +13 -9
- package/src/flows/node-palette/node-palette.tsx +2 -2
- package/src/folder-contents/folder-contents.tsx +3 -1
- package/src/graph-pane/graph-pane.tsx +2 -2
- package/src/i18n/de.json +402 -0
- package/src/i18n/en.json +16 -5
- package/src/i18n/es.json +402 -0
- package/src/i18n/i18n-context.tsx +65 -0
- package/src/i18n/i18n-languages/i18n-languages.ts +83 -0
- package/src/i18n/i18n.ts +15 -11
- package/src/i18n/i18n.types.ts +43 -0
- package/src/main.tsx +33 -12
- package/src/modal/modal.tsx +2 -2
- package/src/node-table/node-table.tsx +3 -1
- package/src/nodes/nodes.tsx +7 -3
- package/src/resource-menu/resource-menu.tsx +8 -4
- package/src/router/router.types.ts +3 -2
- package/src/save-button/save-button.tsx +3 -3
- package/src/sign-in-refused/sign-in-refused.tsx +3 -2
- package/src/theme/theme-context.tsx +89 -0
- package/src/theme/theme.ts +51 -21
- package/src/timezone/timezone-context.tsx +45 -0
- package/src/timezone/timezone.ts +60 -0
- package/src/tools/tools.tsx +3 -1
package/src/agent/agent.tsx
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "@/components/ui/dropdown-menu";
|
|
25
25
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
26
26
|
import { useCapabilities } from "@/hooks/use-capabilities.ts";
|
|
27
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
27
28
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
28
29
|
|
|
29
30
|
const tabs = ["profile", "chat", "calendar", "log"] as const;
|
|
@@ -43,7 +44,7 @@ type Tab = (typeof tabs)[number];
|
|
|
43
44
|
* answer — a sentence in the two tabs that would have needed it — rather than as a red failure.
|
|
44
45
|
*/
|
|
45
46
|
export function AgentPanel({ node }: { node: Node }) {
|
|
46
|
-
const
|
|
47
|
+
const i18n = useI18n();
|
|
47
48
|
const [tab, setTab] = useState<Tab>("profile");
|
|
48
49
|
const agent = useAgentDefinition(node.id);
|
|
49
50
|
const state = useAgentState(node.id);
|
|
@@ -115,7 +116,7 @@ export function AgentPanel({ node }: { node: Node }) {
|
|
|
115
116
|
* whoever runs the installation over a working system.
|
|
116
117
|
*/
|
|
117
118
|
function NotForYou() {
|
|
118
|
-
const
|
|
119
|
+
const i18n = useI18n();
|
|
119
120
|
return (
|
|
120
121
|
<p className="m-auto max-w-md px-6 py-5 text-center text-sm text-muted-foreground">
|
|
121
122
|
{i18n.t("agent.notPermitted")}
|
|
@@ -131,7 +132,7 @@ function NotForYou() {
|
|
|
131
132
|
* agents, which is the operator's choice, not the reader's problem to debug.
|
|
132
133
|
*/
|
|
133
134
|
function NoRuntime() {
|
|
134
|
-
const
|
|
135
|
+
const i18n = useI18n();
|
|
135
136
|
return (
|
|
136
137
|
<p className="m-auto max-w-md px-6 py-5 text-center text-sm text-muted-foreground">
|
|
137
138
|
{i18n.t("agent.noRuntime")}
|
|
@@ -157,7 +158,8 @@ export function AgentStatus({
|
|
|
157
158
|
agentId: string;
|
|
158
159
|
definition: AgentDefinition | null;
|
|
159
160
|
}) {
|
|
160
|
-
const { data
|
|
161
|
+
const { data } = useIntelRouterContext();
|
|
162
|
+
const i18n = useI18n();
|
|
161
163
|
const state = useAgentState(agentId);
|
|
162
164
|
const runs = useQuery({
|
|
163
165
|
queryKey: ["agent-runs", agentId],
|
|
@@ -180,12 +182,17 @@ export function AgentStatus({
|
|
|
180
182
|
paused
|
|
181
183
|
? i18n.t("agent.pausedNoRuns")
|
|
182
184
|
: next
|
|
183
|
-
?
|
|
185
|
+
? // ⚠️ Formatted in the SCHEDULE's zone and named with it (#228), not in the reader's.
|
|
186
|
+
// The agent fires at the hour its schedule says; showing that instant in the zone of
|
|
187
|
+
// whoever happens to be looking would put a different hour on screen from the one the
|
|
188
|
+
// agent keeps, and the difference only surfaces as "it ran at the wrong time".
|
|
189
|
+
i18n.t("agent.nextRun", {
|
|
184
190
|
when: new Intl.DateTimeFormat(i18n.locale, {
|
|
185
191
|
dateStyle: "medium",
|
|
186
192
|
timeStyle: "short",
|
|
187
|
-
timeZone:
|
|
188
|
-
}).format(next),
|
|
193
|
+
timeZone: next.timezone,
|
|
194
|
+
}).format(next.at),
|
|
195
|
+
zone: next.timezone,
|
|
189
196
|
})
|
|
190
197
|
: i18n.t("agent.noNextRun"),
|
|
191
198
|
];
|
|
@@ -213,7 +220,8 @@ export function AgentActions({
|
|
|
213
220
|
agentId: string;
|
|
214
221
|
definition: AgentDefinition | null;
|
|
215
222
|
}) {
|
|
216
|
-
const { data
|
|
223
|
+
const { data } = useIntelRouterContext();
|
|
224
|
+
const i18n = useI18n();
|
|
217
225
|
const queryClient = useQueryClient();
|
|
218
226
|
const state = useAgentState(agentId);
|
|
219
227
|
const denied = agentAccessDenied(state.error);
|
|
@@ -313,7 +321,7 @@ function RunNow({
|
|
|
313
321
|
describedBy: string | undefined;
|
|
314
322
|
start(target: AgentScheduleTarget): void;
|
|
315
323
|
}) {
|
|
316
|
-
const
|
|
324
|
+
const i18n = useI18n();
|
|
317
325
|
const only = targets.length === 1 ? targets[0] : undefined;
|
|
318
326
|
const className =
|
|
319
327
|
"inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50";
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
SidebarMenuItem,
|
|
14
14
|
useSidebar,
|
|
15
15
|
} from "@/components/ui/sidebar";
|
|
16
|
-
import {
|
|
16
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
17
17
|
|
|
18
18
|
// One tree is the whole navigation: documents and flows side by side in the folder they share, with
|
|
19
19
|
// no Intelligence or Flows entry above them (ADR-0004). Tools stay below, next to the user, because
|
|
@@ -25,7 +25,7 @@ export function AppSidebar({
|
|
|
25
25
|
width: number;
|
|
26
26
|
onWidthChange(width: number): void;
|
|
27
27
|
}) {
|
|
28
|
-
const
|
|
28
|
+
const i18n = useI18n();
|
|
29
29
|
// `offcanvas` rather than `icon`: the icon rail is the mode for a list of equal entries with one
|
|
30
30
|
// icon each, and a tree says what it says through indentation, disclosure arrows and nesting —
|
|
31
31
|
// none of which survives 48px. Collapsing exists to free the canvas, so the sidebar goes.
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
import { flowEntry } from "@/data/intel-data-provider/intel-data-provider.ts";
|
|
41
41
|
import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
42
42
|
import { useCapabilities } from "@/hooks/use-capabilities.ts";
|
|
43
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
43
44
|
import { kindIcons } from "@/kind-icon.ts";
|
|
44
45
|
import { Modal } from "@/modal/modal.tsx";
|
|
45
46
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
@@ -106,7 +107,8 @@ function levelKey(level: Level): readonly unknown[] {
|
|
|
106
107
|
export const TreeEntryMediaType = "application/x-intel-tree-entry";
|
|
107
108
|
|
|
108
109
|
export function AppTree() {
|
|
109
|
-
const { data
|
|
110
|
+
const { data } = useIntelRouterContext();
|
|
111
|
+
const i18n = useI18n();
|
|
110
112
|
const queryClient = useQueryClient();
|
|
111
113
|
const navigate = useNavigate();
|
|
112
114
|
const [expanded, setExpanded] = useState<readonly Level[]>([]);
|
|
@@ -720,7 +722,7 @@ function AddMenu({
|
|
|
720
722
|
variant?: "icon" | "row";
|
|
721
723
|
onSelect(kind: NewKind | "upload" | ImportKind): void;
|
|
722
724
|
}) {
|
|
723
|
-
const
|
|
725
|
+
const i18n = useI18n();
|
|
724
726
|
// Whether this deployment runs an agent runtime (#190). Until the answer arrives the item is
|
|
725
727
|
// absent rather than provisional: a menu entry that vanished after being seen was a lie, one that
|
|
726
728
|
// appears a beat late was merely loading.
|
|
@@ -789,7 +791,7 @@ function CreateForm({
|
|
|
789
791
|
pending: boolean;
|
|
790
792
|
submit(title: string, columns: string[]): void;
|
|
791
793
|
}) {
|
|
792
|
-
const
|
|
794
|
+
const i18n = useI18n();
|
|
793
795
|
const [title, setTitle] = useState("");
|
|
794
796
|
const [columns, setColumns] = useState("");
|
|
795
797
|
const parsedColumns = columnsOf(columns);
|
package/src/app/app.tsx
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "@/components/ui/breadcrumb";
|
|
14
14
|
import { Separator } from "@/components/ui/separator";
|
|
15
15
|
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
|
16
|
-
import {
|
|
16
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
17
17
|
|
|
18
18
|
const sections = [
|
|
19
19
|
{ path: "/nodes", labelKey: "nav.nodes" },
|
|
@@ -22,7 +22,7 @@ const sections = [
|
|
|
22
22
|
] as const;
|
|
23
23
|
|
|
24
24
|
export function App() {
|
|
25
|
-
const
|
|
25
|
+
const i18n = useI18n();
|
|
26
26
|
const pathname = useRouterState({ select: (state) => state.location.pathname });
|
|
27
27
|
const current = sections.find(
|
|
28
28
|
(section) => pathname === section.path || pathname.startsWith(`${section.path}/`),
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
DialogHeader,
|
|
19
19
|
DialogTitle,
|
|
20
20
|
} from "@/components/ui/dialog";
|
|
21
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
21
22
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
22
23
|
|
|
23
24
|
const Kinds = ["node", "flow", "tool"] as const;
|
|
@@ -50,7 +51,8 @@ function matches(query: string, ...fields: (string | null)[]): boolean {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export function HeaderSearch() {
|
|
53
|
-
const { data
|
|
54
|
+
const { data } = useIntelRouterContext();
|
|
55
|
+
const i18n = useI18n();
|
|
54
56
|
const navigate = useNavigate();
|
|
55
57
|
const [open, setOpen] = useState(false);
|
|
56
58
|
const [text, setText] = useState("");
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Dialog,
|
|
3
|
+
DialogContent,
|
|
4
|
+
DialogDescription,
|
|
5
|
+
DialogHeader,
|
|
6
|
+
DialogTitle,
|
|
7
|
+
} from "@/components/ui/dialog";
|
|
8
|
+
import {
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
} from "@/components/ui/select";
|
|
15
|
+
import { useI18n, useLanguage } from "@/i18n/i18n-context.tsx";
|
|
16
|
+
import { languageName } from "@/i18n/i18n-languages/i18n-languages.ts";
|
|
17
|
+
import { type ThemeChoice, ThemeChoices } from "@/theme/theme.ts";
|
|
18
|
+
import { useTheme } from "@/theme/theme-context.tsx";
|
|
19
|
+
import { availableTimezones, browserTimezone } from "@/timezone/timezone.ts";
|
|
20
|
+
import { useTimezoneSelection } from "@/timezone/timezone-context.tsx";
|
|
21
|
+
|
|
22
|
+
// The settings that belong to the person rather than to the installation: language, appearance and
|
|
23
|
+
// timezone. All three are client state — they live in localStorage, not in the Intel HTTP surface —
|
|
24
|
+
// so they do not belong on a screen with a save button. A choice applies at once; a dialog that
|
|
25
|
+
// previews and only commits on "Save" would carry three states too many for three dropdowns.
|
|
26
|
+
//
|
|
27
|
+
// The dialog is opened from outside (the user footer holds the state), because its trigger is a
|
|
28
|
+
// DropdownMenuItem: Radix unmounts the menu content when an item is chosen and would take a dialog
|
|
29
|
+
// nested inside it along in the same frame.
|
|
30
|
+
interface SettingsDialogProps {
|
|
31
|
+
open: boolean;
|
|
32
|
+
onOpenChange(open: boolean): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
|
36
|
+
const i18n = useI18n();
|
|
37
|
+
const language = useLanguage();
|
|
38
|
+
const theme = useTheme();
|
|
39
|
+
const timezone = useTimezoneSelection();
|
|
40
|
+
|
|
41
|
+
// ⚠️ The active zone is put in front of the list unless it is already there, and that is not
|
|
42
|
+
// belt-and-braces: `Intl.supportedValuesOf("timeZone")` returns only CANONICAL IANA names, and
|
|
43
|
+
// "UTC" is not among them (it is an alias of Etc/UTC). A reader whose browser reports "UTC" —
|
|
44
|
+
// every CI machine, plenty of containers and servers — would otherwise get a Select whose value
|
|
45
|
+
// matches no item, and Radix renders that as an EMPTY field. Found by CI, not by the test.
|
|
46
|
+
//
|
|
47
|
+
// The same line covers the engine without `supportedValuesOf` at all: the list is empty and the
|
|
48
|
+
// reader still has their own zone to look at.
|
|
49
|
+
const zones = availableTimezones();
|
|
50
|
+
const current = timezone?.current ?? browserTimezone();
|
|
51
|
+
const options = zones.includes(current) ? zones : [current, ...zones];
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
55
|
+
<DialogContent className="sm:max-w-md">
|
|
56
|
+
<DialogHeader>
|
|
57
|
+
<DialogTitle>{i18n.t("settings.title")}</DialogTitle>
|
|
58
|
+
<DialogDescription>{i18n.t("settings.description")}</DialogDescription>
|
|
59
|
+
</DialogHeader>
|
|
60
|
+
<div className="grid gap-6">
|
|
61
|
+
{/* Without a provider there is nothing to choose — then the block is absent rather than a
|
|
62
|
+
dead dropdown. The number of languages is deliberately NOT checked: there are always
|
|
63
|
+
at least the three built-in ones, so a condition on it would be dead code. */}
|
|
64
|
+
{language ? (
|
|
65
|
+
<div className="grid gap-2">
|
|
66
|
+
<label className="text-sm font-medium" htmlFor="settings-language">
|
|
67
|
+
{i18n.t("settings.language")}
|
|
68
|
+
</label>
|
|
69
|
+
<Select value={language.current} onValueChange={language.select}>
|
|
70
|
+
<SelectTrigger id="settings-language" className="w-full">
|
|
71
|
+
<SelectValue />
|
|
72
|
+
</SelectTrigger>
|
|
73
|
+
<SelectContent>
|
|
74
|
+
{language.codes.map((code) => (
|
|
75
|
+
<SelectItem key={code} value={code}>
|
|
76
|
+
{languageName(code)}
|
|
77
|
+
</SelectItem>
|
|
78
|
+
))}
|
|
79
|
+
</SelectContent>
|
|
80
|
+
</Select>
|
|
81
|
+
</div>
|
|
82
|
+
) : null}
|
|
83
|
+
{theme ? (
|
|
84
|
+
<div className="grid gap-2">
|
|
85
|
+
<label className="text-sm font-medium" htmlFor="settings-appearance">
|
|
86
|
+
{i18n.t("settings.appearance")}
|
|
87
|
+
</label>
|
|
88
|
+
{/* Radix hands out a raw string; the values come from the same list that fills the
|
|
89
|
+
menu, so the assertion here is the whole check. */}
|
|
90
|
+
<Select
|
|
91
|
+
value={theme.current}
|
|
92
|
+
onValueChange={(value) => theme.select(value as ThemeChoice)}
|
|
93
|
+
>
|
|
94
|
+
<SelectTrigger id="settings-appearance" className="w-full">
|
|
95
|
+
<SelectValue />
|
|
96
|
+
</SelectTrigger>
|
|
97
|
+
<SelectContent>
|
|
98
|
+
{ThemeChoices.map((choice) => (
|
|
99
|
+
<SelectItem key={choice} value={choice}>
|
|
100
|
+
{i18n.t(`settings.appearance.${choice}`)}
|
|
101
|
+
</SelectItem>
|
|
102
|
+
))}
|
|
103
|
+
</SelectContent>
|
|
104
|
+
</Select>
|
|
105
|
+
</div>
|
|
106
|
+
) : null}
|
|
107
|
+
{timezone ? (
|
|
108
|
+
<div className="grid gap-2">
|
|
109
|
+
<label className="text-sm font-medium" htmlFor="settings-timezone">
|
|
110
|
+
{i18n.t("settings.timezone")}
|
|
111
|
+
</label>
|
|
112
|
+
<Select value={timezone.current} onValueChange={timezone.select}>
|
|
113
|
+
<SelectTrigger id="settings-timezone" className="w-full">
|
|
114
|
+
<SelectValue />
|
|
115
|
+
</SelectTrigger>
|
|
116
|
+
{/* Several hundred zones: the list scrolls, and Radix keeps the selected one in
|
|
117
|
+
view when it opens. */}
|
|
118
|
+
<SelectContent className="max-h-72">
|
|
119
|
+
{options.map((zone) => (
|
|
120
|
+
<SelectItem key={zone} value={zone}>
|
|
121
|
+
{zone.replace(/_/g, " ")}
|
|
122
|
+
</SelectItem>
|
|
123
|
+
))}
|
|
124
|
+
</SelectContent>
|
|
125
|
+
</Select>
|
|
126
|
+
<p className="text-xs text-muted-foreground">{i18n.t("settings.timezoneHint")}</p>
|
|
127
|
+
</div>
|
|
128
|
+
) : null}
|
|
129
|
+
</div>
|
|
130
|
+
</DialogContent>
|
|
131
|
+
</Dialog>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -3,6 +3,7 @@ import { ChevronLeft, Folder } from "lucide-react";
|
|
|
3
3
|
import type * as React from "react";
|
|
4
4
|
import { useState } from "react";
|
|
5
5
|
import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
6
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
6
7
|
import { Modal } from "@/modal/modal.tsx";
|
|
7
8
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
8
9
|
|
|
@@ -106,7 +107,8 @@ export function MoveDialog({
|
|
|
106
107
|
close(): void;
|
|
107
108
|
submit(destination: MoveDestination): void;
|
|
108
109
|
}) {
|
|
109
|
-
const { data
|
|
110
|
+
const { data } = useIntelRouterContext();
|
|
111
|
+
const i18n = useI18n();
|
|
110
112
|
const [destination, setDestination] = useState<MoveDestination | null>(initial);
|
|
111
113
|
// The browsed path, root first. It is also the descendant guard for the keyboard route: the row
|
|
112
114
|
// being moved is never offered, so its subtree can never be entered in the first place.
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
2
2
|
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Archive as ArchiveIcon,
|
|
5
|
+
ChevronsUpDown,
|
|
6
|
+
LogOut,
|
|
7
|
+
Settings,
|
|
8
|
+
User,
|
|
9
|
+
Wrench,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import { useState } from "react";
|
|
4
12
|
import {
|
|
5
13
|
DropdownMenu,
|
|
6
14
|
DropdownMenuContent,
|
|
@@ -10,7 +18,9 @@ import {
|
|
|
10
18
|
} from "@/components/ui/dropdown-menu";
|
|
11
19
|
import { SidebarMenu, SidebarMenuItem } from "@/components/ui/sidebar";
|
|
12
20
|
import { loginPath } from "@/data/intel-data-provider/intel-data-provider.ts";
|
|
21
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
13
22
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
23
|
+
import { SettingsDialog } from "../settings-dialog/settings-dialog.tsx";
|
|
14
24
|
|
|
15
25
|
/**
|
|
16
26
|
* Everything that belongs to the person rather than to the content, behind the person (#24).
|
|
@@ -24,9 +34,13 @@ import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
|
24
34
|
* what it shows depends on who is asking.
|
|
25
35
|
*/
|
|
26
36
|
export function UserFooter() {
|
|
27
|
-
const { data
|
|
37
|
+
const { data } = useIntelRouterContext();
|
|
38
|
+
const i18n = useI18n();
|
|
28
39
|
const navigate = useNavigate();
|
|
29
40
|
const pathname = useRouterState({ select: (state) => state.location.pathname });
|
|
41
|
+
// The state lives here, not in the dialog: its trigger is a DropdownMenuItem, and Radix unmounts
|
|
42
|
+
// the menu content when an item is chosen — a dialog nested in there would go with it.
|
|
43
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
30
44
|
const session = useQuery({ queryKey: ["session"], queryFn: () => data.getSession() });
|
|
31
45
|
const logout = useMutation({
|
|
32
46
|
mutationFn: () => data.logout(),
|
|
@@ -83,6 +97,13 @@ export function UserFooter() {
|
|
|
83
97
|
{i18n.t("archive.title")}
|
|
84
98
|
</DropdownMenuItem>
|
|
85
99
|
<DropdownMenuSeparator />
|
|
100
|
+
{/* Language, appearance and timezone belong to the person, like everything else behind
|
|
101
|
+
this menu — and unlike the two rows above, they change nothing anybody else sees. */}
|
|
102
|
+
<DropdownMenuItem onSelect={() => setSettingsOpen(true)}>
|
|
103
|
+
<Settings aria-hidden="true" />
|
|
104
|
+
{i18n.t("settings.title")}
|
|
105
|
+
</DropdownMenuItem>
|
|
106
|
+
<DropdownMenuSeparator />
|
|
86
107
|
<DropdownMenuItem
|
|
87
108
|
disabled={logout.isPending}
|
|
88
109
|
onSelect={() => logout.mutate()}
|
|
@@ -93,6 +114,8 @@ export function UserFooter() {
|
|
|
93
114
|
</DropdownMenuItem>
|
|
94
115
|
</DropdownMenuContent>
|
|
95
116
|
</DropdownMenu>
|
|
117
|
+
{/* ⚠️ Outside the DropdownMenu on purpose — see the state above. */}
|
|
118
|
+
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
|
|
96
119
|
</SidebarMenuItem>
|
|
97
120
|
{/* ⚠️ Outside the menu on purpose. Choosing sign-out closes the popup, so a refusal rendered
|
|
98
121
|
inside it would be gone in the same frame it was written — the one message that must
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
2
2
|
import { History, Network, PencilRuler } from "lucide-react";
|
|
3
3
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
4
|
-
import {
|
|
4
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
5
5
|
import { type IntelView, viewFrom } from "@/router/selection-search.ts";
|
|
6
6
|
|
|
7
7
|
const icons = { editor: PencilRuler, graph: Network, runs: History } as const;
|
|
@@ -34,7 +34,7 @@ const labels = {
|
|
|
34
34
|
* are — every destination stays one click, and every button keeps one name.
|
|
35
35
|
*/
|
|
36
36
|
export function ViewToggle({ views = ["editor", "graph"] }: { views?: readonly IntelView[] }) {
|
|
37
|
-
const
|
|
37
|
+
const i18n = useI18n();
|
|
38
38
|
const navigate = useNavigate();
|
|
39
39
|
const state = useRouterState({
|
|
40
40
|
select: (current) => ({
|
package/src/archive/archive.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Flow, Node } from "@anchrd/intel-contract";
|
|
2
2
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
3
|
import { ArchiveRestore } from "lucide-react";
|
|
4
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
4
5
|
import { kindIcons } from "@/kind-icon.ts";
|
|
5
6
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
6
7
|
|
|
@@ -17,7 +18,8 @@ interface ArchivedEntry {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function Archive() {
|
|
20
|
-
const { data
|
|
21
|
+
const { data } = useIntelRouterContext();
|
|
22
|
+
const i18n = useI18n();
|
|
21
23
|
const queryClient = useQueryClient();
|
|
22
24
|
|
|
23
25
|
const archived = useQuery({
|
|
@@ -2,6 +2,7 @@ import type { NodeKind } from "@anchrd/intel-contract";
|
|
|
2
2
|
import { useQuery } from "@tanstack/react-query";
|
|
3
3
|
import { ChevronRight, Folder, Search } from "lucide-react";
|
|
4
4
|
import { useMemo, useState } from "react";
|
|
5
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
5
6
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
6
7
|
|
|
7
8
|
// What this picker offers, whichever side of the shared tree it came from. Deliberately the four
|
|
@@ -40,7 +41,8 @@ export function EntryPicker({
|
|
|
40
41
|
onSelect(entryId: string, entryKind: PickerKind): void;
|
|
41
42
|
label: string;
|
|
42
43
|
}) {
|
|
43
|
-
const { data
|
|
44
|
+
const { data } = useIntelRouterContext();
|
|
45
|
+
const i18n = useI18n();
|
|
44
46
|
const [query, setQuery] = useState("");
|
|
45
47
|
const [openFolder, setOpenFolder] = useState<string | null>(null);
|
|
46
48
|
const graph = useQuery({
|
|
@@ -3,6 +3,7 @@ import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
|
|
3
3
|
import { ChevronDown, ChevronRight, CornerDownRight } from "lucide-react";
|
|
4
4
|
import { useState } from "react";
|
|
5
5
|
import type { I18n } from "@/i18n/i18n.types.ts";
|
|
6
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
6
7
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
7
8
|
|
|
8
9
|
// One page is what a person reads before deciding, not what a database can return. The server caps
|
|
@@ -28,7 +29,8 @@ function formatDuration(ms: number, i18n: I18n): string {
|
|
|
28
29
|
* whoever started it — and the person reading this list is not necessarily that person.
|
|
29
30
|
*/
|
|
30
31
|
export function FlowRuns({ flowId }: { flowId: string }) {
|
|
31
|
-
const { data
|
|
32
|
+
const { data } = useIntelRouterContext();
|
|
33
|
+
const i18n = useI18n();
|
|
32
34
|
const [failedOnly, setFailedOnly] = useState(false);
|
|
33
35
|
const [openRunId, setOpenRunId] = useState<string | null>(null);
|
|
34
36
|
// The filter belongs in the key: "only the failed ones" is a different question with a different
|
|
@@ -112,7 +114,7 @@ export function FlowRuns({ flowId }: { flowId: string }) {
|
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
function RunRow({ run, open, toggle }: { run: FlowRunSummary; open: boolean; toggle(): void }) {
|
|
115
|
-
const
|
|
117
|
+
const i18n = useI18n();
|
|
116
118
|
const failed = run.status === "failed";
|
|
117
119
|
const Chevron = open ? ChevronDown : ChevronRight;
|
|
118
120
|
return (
|
|
@@ -160,7 +162,8 @@ function RunRow({ run, open, toggle }: { run: FlowRunSummary; open: boolean; tog
|
|
|
160
162
|
// onto the canvas because a run took an immutable version and the editor shows the current one —
|
|
161
163
|
// marking a node in the wrong graph would point confidently at the wrong step.
|
|
162
164
|
function RunSteps({ runId }: { runId: string }) {
|
|
163
|
-
const { data
|
|
165
|
+
const { data } = useIntelRouterContext();
|
|
166
|
+
const i18n = useI18n();
|
|
164
167
|
const history = useQuery({
|
|
165
168
|
queryKey: ["flow-run-steps", runId],
|
|
166
169
|
queryFn: () => data.getFlowRunSteps(runId),
|
package/src/flows/flows.tsx
CHANGED
|
@@ -32,11 +32,12 @@ import { FlowRuns } from "@/flow-runs/flow-runs.tsx";
|
|
|
32
32
|
import { nodeIcon } from "@/flows/node-icon/node-icon.ts";
|
|
33
33
|
import { NodePalette, usePaletteOpen } from "@/flows/node-palette/node-palette.tsx";
|
|
34
34
|
import { GraphPane } from "@/graph-pane/graph-pane.tsx";
|
|
35
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
35
36
|
import { Modal } from "@/modal/modal.tsx";
|
|
36
37
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
37
38
|
import { selectedFrom, viewFrom } from "@/router/selection-search.ts";
|
|
38
39
|
import { SaveButton, UnsavedChangesGuard } from "@/save-button/save-button.tsx";
|
|
39
|
-
import {
|
|
40
|
+
import { useResolvedTheme } from "@/theme/theme-context.tsx";
|
|
40
41
|
import { TitleRow } from "@/title-row/title-row.tsx";
|
|
41
42
|
|
|
42
43
|
type CanvasNode = ReactFlowNode<{ node: FlowNode }, "intel">;
|
|
@@ -89,7 +90,7 @@ const flowKindOfEntry: Record<string, FlowNode["kind"] | undefined> = {
|
|
|
89
90
|
};
|
|
90
91
|
|
|
91
92
|
function FlowCard({ data, selected }: NodeProps<CanvasNode>) {
|
|
92
|
-
const
|
|
93
|
+
const i18n = useI18n();
|
|
93
94
|
const contract = data.node;
|
|
94
95
|
const Icon = nodeIcon[contract.kind];
|
|
95
96
|
const branching = contract.kind === "condition";
|
|
@@ -354,9 +355,10 @@ export function Flows() {
|
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
function FlowsEditor() {
|
|
357
|
-
const { data
|
|
358
|
+
const { data } = useIntelRouterContext();
|
|
359
|
+
const i18n = useI18n();
|
|
358
360
|
const flow = useReactFlow();
|
|
359
|
-
const theme =
|
|
361
|
+
const theme = useResolvedTheme();
|
|
360
362
|
const queryClient = useQueryClient();
|
|
361
363
|
const navigate = useNavigate();
|
|
362
364
|
const tools = useQuery({
|
|
@@ -714,7 +716,7 @@ function FlowTitle({
|
|
|
714
716
|
onSave(): void;
|
|
715
717
|
onPublish(): void;
|
|
716
718
|
}) {
|
|
717
|
-
const
|
|
719
|
+
const i18n = useI18n();
|
|
718
720
|
// A draft that is already the published one freezes nothing, so publishing it would be a no-op
|
|
719
721
|
// with a dialog in front of it.
|
|
720
722
|
const publishable =
|
|
@@ -775,7 +777,8 @@ function PublishPreview({
|
|
|
775
777
|
close(): void;
|
|
776
778
|
onPublished(): Promise<void>;
|
|
777
779
|
}) {
|
|
778
|
-
const { data
|
|
780
|
+
const { data } = useIntelRouterContext();
|
|
781
|
+
const i18n = useI18n();
|
|
779
782
|
const preview = useQuery({
|
|
780
783
|
queryKey: ["flow-publish-preview", flowId, versionId],
|
|
781
784
|
queryFn: () => data.previewFlowPublish({ flowId, versionId }),
|
|
@@ -858,7 +861,7 @@ function NodeInspector({
|
|
|
858
861
|
tools: Array<{ name: string; title: string | null; fingerprint: string }>;
|
|
859
862
|
flows: Flow[];
|
|
860
863
|
}) {
|
|
861
|
-
const
|
|
864
|
+
const i18n = useI18n();
|
|
862
865
|
if (!node) return <p className="p-5 text-sm text-muted-foreground">{i18n.t("flows.inspect")}</p>;
|
|
863
866
|
const contract = node.data.node;
|
|
864
867
|
return (
|
|
@@ -1001,7 +1004,8 @@ function NodeInspector({
|
|
|
1001
1004
|
// them. A standing "this flow has conflicts" badge is not offered on purpose — for tools it could
|
|
1002
1005
|
// never be true, because the catalog is a live query with each user's own portal token (ADR-0003).
|
|
1003
1006
|
function FlowNeeds({ flowId }: { flowId: string }) {
|
|
1004
|
-
const { data
|
|
1007
|
+
const { data } = useIntelRouterContext();
|
|
1008
|
+
const i18n = useI18n();
|
|
1005
1009
|
const needs = useQuery({
|
|
1006
1010
|
queryKey: ["flow-requirements", flowId],
|
|
1007
1011
|
queryFn: () => data.getFlowRequirements(flowId),
|
|
@@ -1082,7 +1086,7 @@ function SubflowVersionField({
|
|
|
1082
1086
|
configuration: Extract<FlowNode, { kind: "subflow" }>["configuration"];
|
|
1083
1087
|
update(fn: (node: FlowNode) => FlowNode): void;
|
|
1084
1088
|
}) {
|
|
1085
|
-
const
|
|
1089
|
+
const i18n = useI18n();
|
|
1086
1090
|
const mode = configuration.version.mode;
|
|
1087
1091
|
return (
|
|
1088
1092
|
<div className="mt-4">
|
|
@@ -2,8 +2,8 @@ import { flowNodeLayer } from "@anchrd/intel-contract";
|
|
|
2
2
|
import { Plus, X } from "lucide-react";
|
|
3
3
|
import { Fragment, useId, useRef, useState } from "react";
|
|
4
4
|
import { type NodeIcon, nodeIcon } from "@/flows/node-icon/node-icon.ts";
|
|
5
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
5
6
|
import { cn } from "@/lib/utils.ts";
|
|
6
|
-
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
7
7
|
import type { NodePaletteProps, PaletteKind } from "./node-palette.types.ts";
|
|
8
8
|
|
|
9
9
|
const storageKey = "intel.flow-palette";
|
|
@@ -48,7 +48,7 @@ export function NodePalette<K extends PaletteKind>({
|
|
|
48
48
|
add,
|
|
49
49
|
disabledReason,
|
|
50
50
|
}: NodePaletteProps<K>) {
|
|
51
|
-
const
|
|
51
|
+
const i18n = useI18n();
|
|
52
52
|
const listId = useId();
|
|
53
53
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
54
54
|
const listRef = useRef<HTMLDivElement>(null);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
TableRow,
|
|
11
11
|
} from "@/components/ui/table.tsx";
|
|
12
12
|
import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
13
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
13
14
|
import { kindIcons } from "@/kind-icon.ts";
|
|
14
15
|
import { ResourceMenu, type ResourceTarget } from "@/resource-menu/resource-menu.tsx";
|
|
15
16
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
@@ -38,7 +39,8 @@ function targetOf(entry: TreeEntry): ResourceTarget {
|
|
|
38
39
|
* not read pictures. A column repeating the icon in prose costs the width the title needs.
|
|
39
40
|
*/
|
|
40
41
|
export function FolderContents({ folderId }: { folderId: string }) {
|
|
41
|
-
const { data
|
|
42
|
+
const { data } = useIntelRouterContext();
|
|
43
|
+
const i18n = useI18n();
|
|
42
44
|
const navigate = useNavigate();
|
|
43
45
|
const level = useQuery({
|
|
44
46
|
queryKey: treeLevelKey(folderId),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RelationGraph } from "@anchrd/intel-contract";
|
|
2
2
|
import type { UseQueryResult } from "@tanstack/react-query";
|
|
3
3
|
import { lazy, Suspense } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
5
5
|
|
|
6
6
|
const RelationGraphView = lazy(async () => ({
|
|
7
7
|
default: (await import("@/node-graph/node-graph.tsx")).RelationGraphView,
|
|
@@ -17,7 +17,7 @@ export function GraphPane({
|
|
|
17
17
|
query: UseQueryResult<RelationGraph>;
|
|
18
18
|
select(node: RelationGraph["nodes"][number]): void;
|
|
19
19
|
}) {
|
|
20
|
-
const
|
|
20
|
+
const i18n = useI18n();
|
|
21
21
|
const loading = <p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>;
|
|
22
22
|
if (query.isPending) return loading;
|
|
23
23
|
if (query.isError || !query.data) {
|