@nastechai/agent 0.16.0 → 0.17.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/eslint.config.js +23 -0
- package/index.html +24 -0
- package/package.json +54 -26
- package/package.json.bak +89 -0
- package/package.json.pub +88 -0
- package/src/App.tsx +1173 -0
- package/src/components/AuthWidget.tsx +150 -0
- package/src/components/AutoField.tsx +206 -0
- package/src/components/Backdrop.tsx +93 -0
- package/src/components/ChatSidebar.tsx +394 -0
- package/src/components/DeleteConfirmDialog.tsx +40 -0
- package/src/components/LanguageSwitcher.tsx +186 -0
- package/src/components/Markdown.tsx +383 -0
- package/src/components/ModelInfoCard.tsx +112 -0
- package/src/components/ModelPickerDialog.tsx +470 -0
- package/src/components/OAuthLoginModal.tsx +374 -0
- package/src/components/OAuthProvidersCard.tsx +287 -0
- package/src/components/PlatformsCard.tsx +97 -0
- package/src/components/ScheduleBuilder.tsx +273 -0
- package/src/components/SidebarFooter.tsx +42 -0
- package/src/components/SidebarStatusStrip.tsx +72 -0
- package/src/components/SlashPopover.tsx +171 -0
- package/src/components/ThemeSwitcher.tsx +243 -0
- package/src/components/ToolCall.tsx +228 -0
- package/src/components/ToolsetConfigDrawer.tsx +448 -0
- package/src/contexts/PageHeaderProvider.tsx +139 -0
- package/src/contexts/SystemActions.tsx +120 -0
- package/src/contexts/page-header-context.ts +12 -0
- package/src/contexts/system-actions-context.ts +18 -0
- package/src/contexts/usePageHeader.ts +10 -0
- package/src/contexts/useSystemActions.ts +15 -0
- package/src/hooks/useModalBehavior.ts +44 -0
- package/src/hooks/useSidebarStatus.ts +27 -0
- package/src/i18n/af.ts +702 -0
- package/src/i18n/context.tsx +123 -0
- package/src/i18n/de.ts +701 -0
- package/src/i18n/en.ts +708 -0
- package/src/i18n/es.ts +701 -0
- package/src/i18n/fr.ts +701 -0
- package/src/i18n/ga.ts +702 -0
- package/src/i18n/hu.ts +702 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/it.ts +701 -0
- package/src/i18n/ja.ts +702 -0
- package/src/i18n/ko.ts +702 -0
- package/src/i18n/pt.ts +702 -0
- package/src/i18n/ru.ts +702 -0
- package/src/i18n/tr.ts +702 -0
- package/src/i18n/types.ts +710 -0
- package/src/i18n/uk.ts +702 -0
- package/src/i18n/zh-hant.ts +702 -0
- package/src/i18n/zh.ts +698 -0
- package/src/index.css +274 -0
- package/src/lib/api.ts +1585 -0
- package/src/lib/dashboard-flags.ts +15 -0
- package/src/lib/format.ts +9 -0
- package/src/lib/fuzzy.ts +192 -0
- package/src/lib/gatewayClient.ts +253 -0
- package/src/lib/nested.ts +23 -0
- package/src/lib/resolve-page-title.ts +41 -0
- package/src/lib/schedule.ts +382 -0
- package/src/lib/slashExec.ts +163 -0
- package/src/lib/utils.ts +35 -0
- package/src/main.tsx +25 -0
- package/src/pages/AnalyticsPage.tsx +601 -0
- package/src/pages/ChannelsPage.tsx +772 -0
- package/src/pages/ChatPage.tsx +889 -0
- package/src/pages/ConfigPage.tsx +660 -0
- package/src/pages/CronPage.tsx +524 -0
- package/src/pages/DocsPage.tsx +69 -0
- package/src/pages/EnvPage.tsx +918 -0
- package/src/pages/LogsPage.tsx +246 -0
- package/src/pages/McpPage.tsx +757 -0
- package/src/pages/ModelsPage.tsx +994 -0
- package/src/pages/PairingPage.tsx +276 -0
- package/src/pages/PluginsPage.tsx +580 -0
- package/src/pages/ProfilesPage.tsx +559 -0
- package/src/pages/SessionsPage.tsx +936 -0
- package/src/pages/SkillsPage.tsx +557 -0
- package/src/pages/SystemPage.tsx +1259 -0
- package/src/pages/WebhooksPage.tsx +483 -0
- package/src/plugins/PluginPage.tsx +64 -0
- package/src/plugins/index.ts +6 -0
- package/src/plugins/registry.ts +151 -0
- package/src/plugins/sdk.d.ts +160 -0
- package/src/plugins/slots.ts +199 -0
- package/src/plugins/types.ts +37 -0
- package/src/plugins/usePlugins.ts +133 -0
- package/src/themes/context.tsx +443 -0
- package/src/themes/fonts.ts +160 -0
- package/src/themes/index.ts +3 -0
- package/src/themes/presets.ts +477 -0
- package/src/themes/types.ts +187 -0
- package/tsconfig.app.json +34 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +124 -0
- package/vite.config.ts.timestamp-1780999102396-af6b77b30ebd8.mjs +105 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { AlertTriangle, Radio, Wifi, WifiOff } from "lucide-react";
|
|
2
|
+
import type { PlatformStatus } from "@/lib/api";
|
|
3
|
+
import { isoTimeAgo } from "@/lib/utils";
|
|
4
|
+
import { Badge } from "@nastechai/ui/ui/components/badge";
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@nastechai/ui/ui/components/card";
|
|
6
|
+
import { useI18n } from "@/i18n";
|
|
7
|
+
|
|
8
|
+
export function PlatformsCard({ platforms }: PlatformsCardProps) {
|
|
9
|
+
const { t } = useI18n();
|
|
10
|
+
const platformStateBadge: Record<
|
|
11
|
+
string,
|
|
12
|
+
{ tone: "success" | "warning" | "destructive"; label: string }
|
|
13
|
+
> = {
|
|
14
|
+
connected: { tone: "success", label: t.status.connected },
|
|
15
|
+
disconnected: { tone: "warning", label: t.status.disconnected },
|
|
16
|
+
fatal: { tone: "destructive", label: t.status.error },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Card>
|
|
21
|
+
<CardHeader>
|
|
22
|
+
<div className="flex items-center gap-2">
|
|
23
|
+
<Radio className="h-5 w-5 text-muted-foreground" />
|
|
24
|
+
<CardTitle className="text-base">
|
|
25
|
+
{t.status.connectedPlatforms}
|
|
26
|
+
</CardTitle>
|
|
27
|
+
</div>
|
|
28
|
+
</CardHeader>
|
|
29
|
+
|
|
30
|
+
<CardContent className="grid gap-3">
|
|
31
|
+
{platforms.map(([name, info]) => {
|
|
32
|
+
const display = platformStateBadge[info.state] ?? {
|
|
33
|
+
tone: "outline" as const,
|
|
34
|
+
label: info.state,
|
|
35
|
+
};
|
|
36
|
+
const IconComponent =
|
|
37
|
+
info.state === "connected"
|
|
38
|
+
? Wifi
|
|
39
|
+
: info.state === "fatal"
|
|
40
|
+
? AlertTriangle
|
|
41
|
+
: WifiOff;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
key={name}
|
|
46
|
+
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 border border-border p-3 w-full"
|
|
47
|
+
>
|
|
48
|
+
<div className="flex items-center gap-3 min-w-0 w-full">
|
|
49
|
+
<IconComponent
|
|
50
|
+
className={`h-4 w-4 shrink-0 ${
|
|
51
|
+
info.state === "connected"
|
|
52
|
+
? "text-success"
|
|
53
|
+
: info.state === "fatal"
|
|
54
|
+
? "text-destructive"
|
|
55
|
+
: "text-warning"
|
|
56
|
+
}`}
|
|
57
|
+
/>
|
|
58
|
+
|
|
59
|
+
<div className="flex flex-col gap-0.5 min-w-0">
|
|
60
|
+
<span className="font-mondwest normal-case text-sm font-medium capitalize truncate">
|
|
61
|
+
{name}
|
|
62
|
+
</span>
|
|
63
|
+
|
|
64
|
+
{info.error_message && (
|
|
65
|
+
<span className="font-mondwest normal-case text-xs text-destructive">
|
|
66
|
+
{info.error_message}
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
{info.updated_at && (
|
|
71
|
+
<span className="font-mondwest normal-case text-xs text-muted-foreground">
|
|
72
|
+
{t.status.lastUpdate}: {isoTimeAgo(info.updated_at)}
|
|
73
|
+
</span>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<Badge
|
|
79
|
+
tone={display.tone}
|
|
80
|
+
className="shrink-0 self-start sm:self-center"
|
|
81
|
+
>
|
|
82
|
+
{display.tone === "success" && (
|
|
83
|
+
<span className="mr-1 inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-current" />
|
|
84
|
+
)}
|
|
85
|
+
{display.label}
|
|
86
|
+
</Badge>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
</CardContent>
|
|
91
|
+
</Card>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface PlatformsCardProps {
|
|
96
|
+
platforms: [string, PlatformStatus][];
|
|
97
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { Input } from "@nastechai/ui/ui/components/input";
|
|
3
|
+
import { Label } from "@nastechai/ui/ui/components/label";
|
|
4
|
+
import { Select, SelectOption } from "@nastechai/ui/ui/components/select";
|
|
5
|
+
import { Button } from "@nastechai/ui/ui/components/button";
|
|
6
|
+
import { useI18n } from "@/i18n";
|
|
7
|
+
import {
|
|
8
|
+
buildScheduleString,
|
|
9
|
+
DEFAULT_SCHEDULE_STATE,
|
|
10
|
+
type IntervalUnit,
|
|
11
|
+
type ScheduleBuilderState,
|
|
12
|
+
type ScheduleMode,
|
|
13
|
+
type Weekday,
|
|
14
|
+
WEEKDAY_INDEXES,
|
|
15
|
+
} from "@/lib/schedule";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Human-readable schedule picker for cron job create/edit flows.
|
|
19
|
+
*
|
|
20
|
+
* Replaces the raw "type a cron expression" input that lived inline in
|
|
21
|
+
* ``CronPage``. The picker still emits a single backend-compatible
|
|
22
|
+
* schedule string (see ``cron/jobs.py::parse_schedule``), but the user
|
|
23
|
+
* fills out shape-appropriate inputs (time picker, weekday toggles,
|
|
24
|
+
* datetime-local field) per mode.
|
|
25
|
+
*
|
|
26
|
+
* Architecture:
|
|
27
|
+
*
|
|
28
|
+
* - The component is fully controlled. Parent owns the
|
|
29
|
+
* ``ScheduleBuilderState`` and the derived schedule string (built
|
|
30
|
+
* via ``buildScheduleString`` in render).
|
|
31
|
+
* - Mode-specific state slots (``timeOfDay``, ``weekdays``, ...) are
|
|
32
|
+
* preserved across mode switches so flipping back to a previous mode
|
|
33
|
+
* doesn't erase the user's work.
|
|
34
|
+
* - The "Custom" mode is an escape hatch — surfacing it as a normal
|
|
35
|
+
* option (instead of hiding it behind an "advanced" toggle) keeps
|
|
36
|
+
* power-user workflows discoverable without making everyone scroll
|
|
37
|
+
* past it.
|
|
38
|
+
*/
|
|
39
|
+
export function ScheduleBuilder({ onChange, value }: ScheduleBuilderProps) {
|
|
40
|
+
const { t } = useI18n();
|
|
41
|
+
const cronStrings = t.cron;
|
|
42
|
+
const modeStrings = (cronStrings.scheduleModes ?? {}) as Record<string, string>;
|
|
43
|
+
|
|
44
|
+
const update = useCallback(
|
|
45
|
+
(patch: Partial<ScheduleBuilderState>) => {
|
|
46
|
+
onChange({ ...value, ...patch });
|
|
47
|
+
},
|
|
48
|
+
[onChange, value],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const toggleWeekday = useCallback(
|
|
52
|
+
(day: Weekday) => {
|
|
53
|
+
const present = value.weekdays.includes(day);
|
|
54
|
+
update({
|
|
55
|
+
weekdays: present
|
|
56
|
+
? value.weekdays.filter((d) => d !== day)
|
|
57
|
+
: [...value.weekdays, day],
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
[update, value.weekdays],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="grid gap-3">
|
|
65
|
+
<div className="grid gap-2">
|
|
66
|
+
<Label htmlFor="cron-schedule-mode">
|
|
67
|
+
{cronStrings.scheduleMode ?? "Schedule"}
|
|
68
|
+
</Label>
|
|
69
|
+
<Select
|
|
70
|
+
id="cron-schedule-mode"
|
|
71
|
+
value={value.mode}
|
|
72
|
+
onValueChange={(v) => update({ mode: v as ScheduleMode })}
|
|
73
|
+
>
|
|
74
|
+
<SelectOption value="interval">{modeStrings.interval}</SelectOption>
|
|
75
|
+
<SelectOption value="daily">{modeStrings.daily}</SelectOption>
|
|
76
|
+
<SelectOption value="weekly">{modeStrings.weekly}</SelectOption>
|
|
77
|
+
<SelectOption value="monthly">{modeStrings.monthly}</SelectOption>
|
|
78
|
+
<SelectOption value="once">{modeStrings.once}</SelectOption>
|
|
79
|
+
<SelectOption value="custom">{modeStrings.custom}</SelectOption>
|
|
80
|
+
</Select>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{value.mode === "interval" && (
|
|
84
|
+
<div className="grid grid-cols-[1fr_1.4fr] gap-3">
|
|
85
|
+
<div className="grid gap-2">
|
|
86
|
+
<Label htmlFor="cron-interval-value">
|
|
87
|
+
{modeStrings.intervalEvery}
|
|
88
|
+
</Label>
|
|
89
|
+
<Input
|
|
90
|
+
id="cron-interval-value"
|
|
91
|
+
type="number"
|
|
92
|
+
min={1}
|
|
93
|
+
max={9999}
|
|
94
|
+
value={String(value.intervalValue)}
|
|
95
|
+
onChange={(e) => {
|
|
96
|
+
const n = parseInt(e.target.value, 10);
|
|
97
|
+
update({
|
|
98
|
+
intervalValue: Number.isFinite(n) && n > 0 ? n : 1,
|
|
99
|
+
});
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
<div className="grid gap-2">
|
|
104
|
+
<Label htmlFor="cron-interval-unit">{modeStrings.intervalUnit}</Label>
|
|
105
|
+
<Select
|
|
106
|
+
id="cron-interval-unit"
|
|
107
|
+
value={value.intervalUnit}
|
|
108
|
+
onValueChange={(v) => update({ intervalUnit: v as IntervalUnit })}
|
|
109
|
+
>
|
|
110
|
+
<SelectOption value="minutes">
|
|
111
|
+
{modeStrings.unitMinutes}
|
|
112
|
+
</SelectOption>
|
|
113
|
+
<SelectOption value="hours">{modeStrings.unitHours}</SelectOption>
|
|
114
|
+
<SelectOption value="days">{modeStrings.unitDays}</SelectOption>
|
|
115
|
+
</Select>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{value.mode === "daily" && (
|
|
121
|
+
<TimeOfDayField
|
|
122
|
+
id="cron-daily-time"
|
|
123
|
+
label={modeStrings.timeOfDay}
|
|
124
|
+
value={value.timeOfDay}
|
|
125
|
+
onChange={(timeOfDay) => update({ timeOfDay })}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
|
|
129
|
+
{value.mode === "weekly" && (
|
|
130
|
+
<>
|
|
131
|
+
<div className="grid gap-2">
|
|
132
|
+
<Label>{modeStrings.weekdays}</Label>
|
|
133
|
+
<div
|
|
134
|
+
className="flex flex-wrap gap-1.5"
|
|
135
|
+
role="group"
|
|
136
|
+
aria-label={modeStrings.weekdays}
|
|
137
|
+
>
|
|
138
|
+
{WEEKDAY_INDEXES.map((d) => {
|
|
139
|
+
const isOn = value.weekdays.includes(d);
|
|
140
|
+
return (
|
|
141
|
+
<Button
|
|
142
|
+
key={d}
|
|
143
|
+
type="button"
|
|
144
|
+
size="sm"
|
|
145
|
+
outlined={!isOn}
|
|
146
|
+
aria-pressed={isOn}
|
|
147
|
+
onClick={() => toggleWeekday(d)}
|
|
148
|
+
className="min-w-[2.5rem] font-mono-ui text-xs uppercase"
|
|
149
|
+
>
|
|
150
|
+
{modeStrings.weekdaysShort[d]}
|
|
151
|
+
</Button>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<TimeOfDayField
|
|
157
|
+
id="cron-weekly-time"
|
|
158
|
+
label={modeStrings.timeOfDay}
|
|
159
|
+
value={value.timeOfDay}
|
|
160
|
+
onChange={(timeOfDay) => update({ timeOfDay })}
|
|
161
|
+
/>
|
|
162
|
+
</>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
{value.mode === "monthly" && (
|
|
166
|
+
<div className="grid grid-cols-[1fr_1fr] gap-3">
|
|
167
|
+
<div className="grid gap-2">
|
|
168
|
+
<Label htmlFor="cron-month-day">{modeStrings.dayOfMonth}</Label>
|
|
169
|
+
<Input
|
|
170
|
+
id="cron-month-day"
|
|
171
|
+
type="number"
|
|
172
|
+
min={1}
|
|
173
|
+
max={31}
|
|
174
|
+
value={String(value.dayOfMonth)}
|
|
175
|
+
onChange={(e) => {
|
|
176
|
+
const n = parseInt(e.target.value, 10);
|
|
177
|
+
update({
|
|
178
|
+
dayOfMonth:
|
|
179
|
+
Number.isFinite(n) && n >= 1 && n <= 31 ? n : 1,
|
|
180
|
+
});
|
|
181
|
+
}}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
<TimeOfDayField
|
|
185
|
+
id="cron-monthly-time"
|
|
186
|
+
label={modeStrings.timeOfDay}
|
|
187
|
+
value={value.timeOfDay}
|
|
188
|
+
onChange={(timeOfDay) => update({ timeOfDay })}
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{value.mode === "once" && (
|
|
194
|
+
<div className="grid gap-2">
|
|
195
|
+
<Label htmlFor="cron-once-at">{modeStrings.onceAt}</Label>
|
|
196
|
+
{/* Native datetime-local — emits the exact "YYYY-MM-DDTHH:MM"
|
|
197
|
+
shape ``parse_schedule`` accepts on the backend. */}
|
|
198
|
+
<input
|
|
199
|
+
id="cron-once-at"
|
|
200
|
+
type="datetime-local"
|
|
201
|
+
className="flex h-9 w-full border border-border bg-background/40 px-3 py-2 text-sm font-courier shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/30 focus-visible:border-foreground/25"
|
|
202
|
+
value={value.onceAt}
|
|
203
|
+
onChange={(e) => update({ onceAt: e.target.value })}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
{value.mode === "custom" && (
|
|
209
|
+
<div className="grid gap-2">
|
|
210
|
+
<Label htmlFor="cron-custom-expr">{modeStrings.customLabel}</Label>
|
|
211
|
+
<Input
|
|
212
|
+
id="cron-custom-expr"
|
|
213
|
+
placeholder={modeStrings.customPlaceholder}
|
|
214
|
+
value={value.custom}
|
|
215
|
+
onChange={(e) => update({ custom: e.target.value })}
|
|
216
|
+
className="font-mono-ui"
|
|
217
|
+
/>
|
|
218
|
+
<p className="text-xs text-muted-foreground">
|
|
219
|
+
{modeStrings.customHint}
|
|
220
|
+
</p>
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{/* Inline preview of what we'll send to the backend. Helps users
|
|
225
|
+
eyeball the result before hitting Create, and keeps the
|
|
226
|
+
schedule grammar discoverable for the custom mode. */}
|
|
227
|
+
<p className="text-xs text-muted-foreground">
|
|
228
|
+
<span className="opacity-70">{modeStrings.preview}: </span>
|
|
229
|
+
<span className="font-mono-ui text-foreground">
|
|
230
|
+
{buildScheduleString(value) || modeStrings.previewEmpty}
|
|
231
|
+
</span>
|
|
232
|
+
</p>
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function TimeOfDayField({
|
|
238
|
+
id,
|
|
239
|
+
label,
|
|
240
|
+
onChange,
|
|
241
|
+
value,
|
|
242
|
+
}: TimeOfDayFieldProps) {
|
|
243
|
+
return (
|
|
244
|
+
<div className="grid gap-2">
|
|
245
|
+
<Label htmlFor={id}>{label}</Label>
|
|
246
|
+
{/* Native time picker is the right tool for "HH:MM" — saves us
|
|
247
|
+
two separate hour/minute selects, respects user locale's
|
|
248
|
+
AM/PM preference, and round-trips with ``buildScheduleString``
|
|
249
|
+
without parsing. */}
|
|
250
|
+
<input
|
|
251
|
+
id={id}
|
|
252
|
+
type="time"
|
|
253
|
+
className="flex h-9 w-full border border-border bg-background/40 px-3 py-2 text-sm font-courier shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/30 focus-visible:border-foreground/25"
|
|
254
|
+
value={value}
|
|
255
|
+
onChange={(e) => onChange(e.target.value)}
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export { DEFAULT_SCHEDULE_STATE };
|
|
262
|
+
|
|
263
|
+
interface ScheduleBuilderProps {
|
|
264
|
+
onChange: (state: ScheduleBuilderState) => void;
|
|
265
|
+
value: ScheduleBuilderState;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
interface TimeOfDayFieldProps {
|
|
269
|
+
id: string;
|
|
270
|
+
label: string;
|
|
271
|
+
onChange: (value: string) => void;
|
|
272
|
+
value: string;
|
|
273
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Typography } from "@nastechai/ui/ui/components/typography/index";
|
|
2
|
+
import type { StatusResponse } from "@/lib/api";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { useI18n } from "@/i18n";
|
|
5
|
+
|
|
6
|
+
export function SidebarFooter({ status }: SidebarFooterProps) {
|
|
7
|
+
const { t } = useI18n();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={cn(
|
|
12
|
+
"flex shrink-0 items-center justify-between gap-2",
|
|
13
|
+
"px-5 py-2.5",
|
|
14
|
+
"border-t border-current/10",
|
|
15
|
+
)}
|
|
16
|
+
>
|
|
17
|
+
<Typography
|
|
18
|
+
className="font-mono-ui text-xs tabular-nums tracking-[0.08em] text-text-tertiary lowercase"
|
|
19
|
+
>
|
|
20
|
+
{status?.version != null ? `v${status.version}` : "—"}
|
|
21
|
+
</Typography>
|
|
22
|
+
|
|
23
|
+
<a
|
|
24
|
+
href="https://nastech.com"
|
|
25
|
+
target="_blank"
|
|
26
|
+
rel="noopener noreferrer"
|
|
27
|
+
className={cn(
|
|
28
|
+
"font-mondwest text-display text-xs tracking-[0.12em] text-midground",
|
|
29
|
+
"transition-opacity hover:opacity-90",
|
|
30
|
+
"focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground/40",
|
|
31
|
+
)}
|
|
32
|
+
style={{ mixBlendMode: "plus-lighter" }}
|
|
33
|
+
>
|
|
34
|
+
{t.app.footer.org}
|
|
35
|
+
</a>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SidebarFooterProps {
|
|
41
|
+
status: StatusResponse | null;
|
|
42
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Link } from "react-router-dom";
|
|
2
|
+
import type { StatusResponse } from "@/lib/api";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { useI18n } from "@/i18n";
|
|
5
|
+
|
|
6
|
+
/** Gateway + session summary for the System sidebar block (no separate strip chrome). */
|
|
7
|
+
export function SidebarStatusStrip({ status }: SidebarStatusStripProps) {
|
|
8
|
+
const { t } = useI18n();
|
|
9
|
+
|
|
10
|
+
if (status === null) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="px-5 py-1.5" aria-hidden>
|
|
13
|
+
<div className="h-2 w-[80%] max-w-full animate-pulse rounded-sm bg-midground/10" />
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const gw = gatewayLine(status, t);
|
|
19
|
+
const { activeSessionsLabel, gatewayStatusLabel } = t.app;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Link
|
|
23
|
+
to="/sessions"
|
|
24
|
+
title={t.app.statusOverview}
|
|
25
|
+
className={cn(
|
|
26
|
+
"block text-left",
|
|
27
|
+
"px-5 pb-2 pt-0.5",
|
|
28
|
+
"text-text-secondary",
|
|
29
|
+
"transition-colors hover:text-midground",
|
|
30
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground/40",
|
|
31
|
+
"focus-visible:ring-inset",
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
<div className="flex flex-col gap-1 font-mondwest text-xs leading-snug tracking-[0.08em]">
|
|
35
|
+
<p className="break-words">
|
|
36
|
+
<span className="text-text-tertiary">{gatewayStatusLabel}</span>{" "}
|
|
37
|
+
<span className={cn("font-medium", gw.tone)}>{gw.label}</span>
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
<p className="break-words">
|
|
41
|
+
<span className="text-text-tertiary">{activeSessionsLabel}</span>{" "}
|
|
42
|
+
<span className="tabular-nums text-text-secondary">
|
|
43
|
+
{status.active_sessions}
|
|
44
|
+
</span>
|
|
45
|
+
</p>
|
|
46
|
+
</div>
|
|
47
|
+
</Link>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function gatewayLine(
|
|
52
|
+
status: StatusResponse,
|
|
53
|
+
t: ReturnType<typeof useI18n>["t"],
|
|
54
|
+
): { label: string; tone: string } {
|
|
55
|
+
const g = t.app.gatewayStrip;
|
|
56
|
+
const byState: Record<string, { label: string; tone: string }> = {
|
|
57
|
+
running: { label: g.running, tone: "text-success" },
|
|
58
|
+
starting: { label: g.starting, tone: "text-warning" },
|
|
59
|
+
startup_failed: { label: g.failed, tone: "text-destructive" },
|
|
60
|
+
stopped: { label: g.stopped, tone: "text-muted-foreground" },
|
|
61
|
+
};
|
|
62
|
+
if (status.gateway_state && byState[status.gateway_state]) {
|
|
63
|
+
return byState[status.gateway_state];
|
|
64
|
+
}
|
|
65
|
+
return status.gateway_running
|
|
66
|
+
? { label: g.running, tone: "text-success" }
|
|
67
|
+
: { label: g.off, tone: "text-muted-foreground" };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface SidebarStatusStripProps {
|
|
71
|
+
status: StatusResponse | null;
|
|
72
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { GatewayClient } from "@/lib/gatewayClient";
|
|
2
|
+
import { ListItem } from "@nastechai/ui/ui/components/list-item";
|
|
3
|
+
import { ChevronRight } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
forwardRef,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useImperativeHandle,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Slash-command autocomplete popover, rendered above the composer in ChatPage.
|
|
15
|
+
* Mirrors the completion UX of the Ink TUI — type `/`, see matching commands,
|
|
16
|
+
* arrow keys or click to select, Tab to apply, Enter to submit.
|
|
17
|
+
*
|
|
18
|
+
* The parent owns all keyboard handling via `ref.handleKey`, which returns
|
|
19
|
+
* true when the popover consumed the event, so the composer's Enter/arrow
|
|
20
|
+
* logic stays in one place.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface CompletionItem {
|
|
24
|
+
display: string;
|
|
25
|
+
text: string;
|
|
26
|
+
meta?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SlashPopoverHandle {
|
|
30
|
+
/** Returns true if the key was consumed by the popover. */
|
|
31
|
+
handleKey(e: React.KeyboardEvent<HTMLTextAreaElement>): boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Props {
|
|
35
|
+
input: string;
|
|
36
|
+
gw: GatewayClient | null;
|
|
37
|
+
onApply(nextInput: string): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CompletionResponse {
|
|
41
|
+
items?: CompletionItem[];
|
|
42
|
+
replace_from?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const DEBOUNCE_MS = 60;
|
|
46
|
+
|
|
47
|
+
export const SlashPopover = forwardRef<SlashPopoverHandle, Props>(
|
|
48
|
+
function SlashPopover({ input, gw, onApply }, ref) {
|
|
49
|
+
const [items, setItems] = useState<CompletionItem[]>([]);
|
|
50
|
+
const [selected, setSelected] = useState(0);
|
|
51
|
+
const [replaceFrom, setReplaceFrom] = useState(1);
|
|
52
|
+
const lastInputRef = useRef<string>("");
|
|
53
|
+
|
|
54
|
+
// Debounced completion fetch. We never clear `items` in the effect body
|
|
55
|
+
// (doing so would flag react-hooks/set-state-in-effect); instead the
|
|
56
|
+
// render guard below hides stale items once the input stops matching.
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const trimmed = input ?? "";
|
|
59
|
+
|
|
60
|
+
if (!gw || !trimmed.startsWith("/") || trimmed === lastInputRef.current) {
|
|
61
|
+
if (!trimmed.startsWith("/")) lastInputRef.current = "";
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
lastInputRef.current = trimmed;
|
|
65
|
+
|
|
66
|
+
const timer = window.setTimeout(async () => {
|
|
67
|
+
if (lastInputRef.current !== trimmed) return;
|
|
68
|
+
try {
|
|
69
|
+
const r = await gw.request<CompletionResponse>("complete.slash", {
|
|
70
|
+
text: trimmed,
|
|
71
|
+
});
|
|
72
|
+
if (lastInputRef.current !== trimmed) return;
|
|
73
|
+
setItems(r?.items ?? []);
|
|
74
|
+
setReplaceFrom(r?.replace_from ?? 1);
|
|
75
|
+
setSelected(0);
|
|
76
|
+
} catch {
|
|
77
|
+
if (lastInputRef.current === trimmed) setItems([]);
|
|
78
|
+
}
|
|
79
|
+
}, DEBOUNCE_MS);
|
|
80
|
+
|
|
81
|
+
return () => window.clearTimeout(timer);
|
|
82
|
+
}, [input, gw]);
|
|
83
|
+
|
|
84
|
+
const apply = useCallback(
|
|
85
|
+
(item: CompletionItem) => {
|
|
86
|
+
onApply(input.slice(0, replaceFrom) + item.text);
|
|
87
|
+
},
|
|
88
|
+
[input, replaceFrom, onApply],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Only consume keys when the popover is actually visible. Stale items from
|
|
92
|
+
// a previous slash prefix are ignored once the user deletes the "/".
|
|
93
|
+
const visible = items.length > 0 && input.startsWith("/");
|
|
94
|
+
|
|
95
|
+
useImperativeHandle(
|
|
96
|
+
ref,
|
|
97
|
+
() => ({
|
|
98
|
+
handleKey: (e) => {
|
|
99
|
+
if (!visible) return false;
|
|
100
|
+
|
|
101
|
+
switch (e.key) {
|
|
102
|
+
case "ArrowDown":
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
setSelected((s) => (s + 1) % items.length);
|
|
105
|
+
return true;
|
|
106
|
+
|
|
107
|
+
case "ArrowUp":
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
setSelected((s) => (s - 1 + items.length) % items.length);
|
|
110
|
+
return true;
|
|
111
|
+
|
|
112
|
+
case "Tab": {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
const item = items[selected];
|
|
115
|
+
if (item) apply(item);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case "Escape":
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
setItems([]);
|
|
122
|
+
return true;
|
|
123
|
+
|
|
124
|
+
default:
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
[visible, items, selected, apply],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (!visible) return null;
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
className="absolute bottom-full left-0 right-0 mb-2 max-h-64 overflow-y-auto rounded-md border border-border bg-popover shadow-xl text-sm"
|
|
137
|
+
role="listbox"
|
|
138
|
+
>
|
|
139
|
+
{items.map((it, i) => {
|
|
140
|
+
const active = i === selected;
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<ListItem
|
|
144
|
+
key={`${it.text}-${i}`}
|
|
145
|
+
active={active}
|
|
146
|
+
role="option"
|
|
147
|
+
aria-selected={active}
|
|
148
|
+
onMouseEnter={() => setSelected(i)}
|
|
149
|
+
onClick={() => apply(it)}
|
|
150
|
+
className="px-3 py-1.5"
|
|
151
|
+
>
|
|
152
|
+
<ChevronRight
|
|
153
|
+
className={`h-3 w-3 shrink-0 ${active ? "text-primary" : "text-transparent"}`}
|
|
154
|
+
/>
|
|
155
|
+
|
|
156
|
+
<span className="font-mono text-xs shrink-0 truncate">
|
|
157
|
+
{it.display}
|
|
158
|
+
</span>
|
|
159
|
+
|
|
160
|
+
{it.meta && (
|
|
161
|
+
<span className="text-xs text-text-tertiary truncate ml-auto">
|
|
162
|
+
{it.meta}
|
|
163
|
+
</span>
|
|
164
|
+
)}
|
|
165
|
+
</ListItem>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
},
|
|
171
|
+
);
|