@meshxdata/fops 0.1.52 → 0.1.54
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/CHANGELOG.md +559 -0
- package/package.json +2 -6
- package/src/agent/agent.js +6 -0
- package/src/commands/setup.js +34 -0
- package/src/fleet-registry.js +38 -2
- package/src/plugins/__test-fixtures__/fake-plugin.js +2 -0
- package/src/plugins/__test-fixtures__/no-register-plugin.js +2 -0
- package/src/plugins/__test-fixtures__/with-register/index.js +2 -0
- package/src/plugins/__test-fixtures__/without-register/index.js +2 -0
- package/src/plugins/api.js +4 -0
- package/src/plugins/builtins/docker-compose.js +65 -0
- package/src/plugins/bundled/fops-plugin-azure/index.js +4 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-core.js +44 -53
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-storage.js +2 -2
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-cost.js +52 -22
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-helpers.js +6 -2
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops.js +113 -7
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision-init.js +13 -4
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision.js +91 -14
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-service.js +507 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-sync.js +146 -7
- package/src/plugins/bundled/fops-plugin-azure/lib/azure.js +1 -1
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +61 -0
- package/src/plugins/bundled/fops-plugin-cloud/api.js +712 -0
- package/src/plugins/bundled/fops-plugin-cloud/fops.plugin.json +6 -0
- package/src/plugins/bundled/fops-plugin-cloud/index.js +208 -0
- package/src/plugins/bundled/fops-plugin-cloud/lib/azure-provider.js +81 -0
- package/src/plugins/bundled/fops-plugin-cloud/lib/provider.js +50 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/dist/assets/favicon-C49brna2.svg +15 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/dist/assets/index-CVqQ_kKW.js +65 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/dist/assets/index-DZetahP3.css +1 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/dist/index.html +28 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/index.html +27 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/package-lock.json +2634 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/package.json +29 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/postcss.config.cjs +5 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/App.jsx +32 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/api/client.js +114 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/api/queries.js +111 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/components/LogPanel.jsx +162 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/components/ThemeToggle.jsx +46 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/css/additional-styles/utility-patterns.css +147 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/css/style.css +138 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/favicon.svg +15 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/lib/utils.ts +19 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/main.jsx +25 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Audit.jsx +164 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Costs.jsx +305 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/CreateResource.jsx +285 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Fleet.jsx +307 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Resources.jsx +229 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/Header.jsx +132 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/Sidebar.jsx +174 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/SidebarLinkGroup.jsx +21 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/AuthContext.jsx +170 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Info.jsx +49 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/ThemeContext.jsx +37 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Transition.jsx +116 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Utils.js +63 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/vite.config.js +23 -0
- package/src/plugins/bundled/fops-plugin-foundation/test-helpers.js +65 -0
- package/src/plugins/loader.js +34 -1
- package/src/plugins/registry.js +15 -0
- package/src/plugins/schemas.js +17 -0
- package/src/project.js +1 -1
- package/src/serve.js +196 -2
- package/src/shell.js +21 -1
- package/src/web/admin.html.js +236 -0
- package/src/web/api.js +73 -0
- package/src/web/dist/assets/index-BphVaAUd.css +1 -0
- package/src/web/dist/assets/index-CSckLzuG.js +129 -0
- package/src/web/dist/index.html +2 -2
- package/src/web/frontend/index.html +16 -0
- package/src/web/frontend/src/App.jsx +445 -0
- package/src/web/frontend/src/components/ChatView.jsx +910 -0
- package/src/web/frontend/src/components/InputBox.jsx +523 -0
- package/src/web/frontend/src/components/Sidebar.jsx +410 -0
- package/src/web/frontend/src/components/StatusBar.jsx +37 -0
- package/src/web/frontend/src/components/TabBar.jsx +87 -0
- package/src/web/frontend/src/hooks/useWebSocket.js +412 -0
- package/src/web/frontend/src/index.css +78 -0
- package/src/web/frontend/src/main.jsx +6 -0
- package/src/web/frontend/vite.config.js +21 -0
- package/src/web/server.js +64 -1
- package/src/web/dist/assets/index-NXC8Hvnp.css +0 -1
- package/src/web/dist/assets/index-QH1N4ejK.js +0 -112
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
const SLASH_COMMANDS = [
|
|
4
|
+
{ name: "/agents", desc: "List available agents" },
|
|
5
|
+
{ name: "/agent", desc: "Open agent (use /agent <name>)" },
|
|
6
|
+
{ name: "/mesh", desc: "Generate landscape for a mesh (pick from list)" },
|
|
7
|
+
{ name: "/up", desc: "Start Foundation stack" },
|
|
8
|
+
{ name: "/down", desc: "Stop Foundation stack" },
|
|
9
|
+
{ name: "/bootstrap", desc: "Create demo data mesh (~5 min)" },
|
|
10
|
+
{ name: "/train", desc: "Fine-tune embedding model" },
|
|
11
|
+
{ name: "/plan", desc: "Draft a step-by-step implementation plan" },
|
|
12
|
+
{ name: "/ollama", desc: "Setup Ollama" },
|
|
13
|
+
{ name: "/help", desc: "Show help and shortcuts" },
|
|
14
|
+
{ name: "/clear", desc: "Clear chat view" },
|
|
15
|
+
{ name: "/sessions", desc: "List saved sessions" },
|
|
16
|
+
{ name: "/exit", desc: "Close session" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const MAX_ATTACHMENT_BYTES = 256 * 1024;
|
|
20
|
+
const MAX_ATTACHMENT_CHARS = 24_000;
|
|
21
|
+
|
|
22
|
+
function guessCodeFence(fileName = "", mimeType = "") {
|
|
23
|
+
const lower = String(fileName || "").toLowerCase();
|
|
24
|
+
if (lower.endsWith(".json") || mimeType.includes("json")) return "json";
|
|
25
|
+
if (lower.endsWith(".md")) return "markdown";
|
|
26
|
+
if (lower.endsWith(".yml") || lower.endsWith(".yaml")) return "yaml";
|
|
27
|
+
if (lower.endsWith(".csv")) return "csv";
|
|
28
|
+
if (lower.endsWith(".sql")) return "sql";
|
|
29
|
+
if (lower.endsWith(".py")) return "python";
|
|
30
|
+
if (lower.endsWith(".ts") || lower.endsWith(".tsx")) return "typescript";
|
|
31
|
+
if (lower.endsWith(".js") || lower.endsWith(".jsx")) return "javascript";
|
|
32
|
+
if (lower.endsWith(".xml")) return "xml";
|
|
33
|
+
if (lower.endsWith(".html")) return "html";
|
|
34
|
+
if (lower.endsWith(".css")) return "css";
|
|
35
|
+
if (lower.endsWith(".sh")) return "bash";
|
|
36
|
+
return "text";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatAttachmentPrompt({ name, type, size, text, truncated }) {
|
|
40
|
+
const lang = guessCodeFence(name, type);
|
|
41
|
+
const truncationNote = truncated ? `\n[Truncated to first ${MAX_ATTACHMENT_CHARS} chars]` : "";
|
|
42
|
+
return [
|
|
43
|
+
`[Attached file: ${name} | type: ${type || "unknown"} | size: ${size} bytes]${truncationNote}`,
|
|
44
|
+
"",
|
|
45
|
+
"```" + lang,
|
|
46
|
+
text,
|
|
47
|
+
"```",
|
|
48
|
+
].join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function InputBox({ onSubmit, onCancel, disabled, streaming = false, queuedCount = 0 }) {
|
|
52
|
+
const [input, setInput] = useState("");
|
|
53
|
+
const [pendingAttachments, setPendingAttachments] = useState([]);
|
|
54
|
+
const [history, setHistory] = useState([]);
|
|
55
|
+
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
56
|
+
const [slashSelected, setSlashSelected] = useState(0);
|
|
57
|
+
const [attachStatus, setAttachStatus] = useState("");
|
|
58
|
+
const [sttSupported, setSttSupported] = useState(false);
|
|
59
|
+
const [sttListening, setSttListening] = useState(false);
|
|
60
|
+
const [sttStatus, setSttStatus] = useState("");
|
|
61
|
+
const textareaRef = useRef(null);
|
|
62
|
+
const fileInputRef = useRef(null);
|
|
63
|
+
const speechRef = useRef(null);
|
|
64
|
+
const sttBaseRef = useRef("");
|
|
65
|
+
const sttStoppingRef = useRef(false);
|
|
66
|
+
|
|
67
|
+
const filterNorm = input.trim().toLowerCase();
|
|
68
|
+
const slashOpen = filterNorm.startsWith("/");
|
|
69
|
+
const slashMatches = useMemo(() => {
|
|
70
|
+
if (!slashOpen) return [];
|
|
71
|
+
return SLASH_COMMANDS.filter((c) =>
|
|
72
|
+
c.name.toLowerCase().startsWith(filterNorm)
|
|
73
|
+
);
|
|
74
|
+
}, [slashOpen, filterNorm]);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
setSlashSelected(0);
|
|
78
|
+
}, [filterNorm]);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
textareaRef.current?.focus();
|
|
82
|
+
}, [disabled]);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (typeof window === "undefined") return;
|
|
86
|
+
const Recognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
87
|
+
setSttSupported(typeof Recognition === "function");
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
try {
|
|
91
|
+
speechRef.current?.stop?.();
|
|
92
|
+
} catch {}
|
|
93
|
+
speechRef.current = null;
|
|
94
|
+
};
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const completeSlash = (cmd) => {
|
|
98
|
+
const name = cmd.split(" ")[0];
|
|
99
|
+
setInput(name + " ");
|
|
100
|
+
setSlashSelected(0);
|
|
101
|
+
textareaRef.current?.focus();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const submitCurrent = () => {
|
|
105
|
+
const text = input.trim();
|
|
106
|
+
const hasAttachments = pendingAttachments.length > 0;
|
|
107
|
+
if ((!text && !hasAttachments) || disabled) return;
|
|
108
|
+
|
|
109
|
+
const attachmentBlocks = pendingAttachments.map((file) =>
|
|
110
|
+
formatAttachmentPrompt({
|
|
111
|
+
name: file.name,
|
|
112
|
+
type: file.type,
|
|
113
|
+
size: file.size,
|
|
114
|
+
text: file.text,
|
|
115
|
+
truncated: file.truncated,
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const payload = [text, ...attachmentBlocks].filter(Boolean).join("\n\n");
|
|
120
|
+
const historyEntry = [text, ...pendingAttachments.map((f) => `[Attached file: ${f.name}]`)]
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.join(" ");
|
|
123
|
+
|
|
124
|
+
if (historyEntry.trim()) {
|
|
125
|
+
setHistory((prev) => [...prev, historyEntry.trim()]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
onSubmit(payload);
|
|
129
|
+
setInput("");
|
|
130
|
+
setPendingAttachments([]);
|
|
131
|
+
setAttachStatus("");
|
|
132
|
+
setHistoryIndex(-1);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const handleKeyDown = (e) => {
|
|
136
|
+
if (slashOpen && slashMatches.length > 0) {
|
|
137
|
+
if (e.key === "ArrowDown") {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
setSlashSelected((i) => Math.min(i + 1, slashMatches.length - 1));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (e.key === "ArrowUp") {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
setSlashSelected((i) => Math.max(i - 1, 0));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (e.key === "Tab") {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
completeSlash(slashMatches[slashSelected].name);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
const cmd = slashMatches[slashSelected].name;
|
|
155
|
+
completeSlash(cmd);
|
|
156
|
+
setHistory((prev) => [...prev, cmd.trim()]);
|
|
157
|
+
onSubmit(cmd.trim());
|
|
158
|
+
setInput("");
|
|
159
|
+
setPendingAttachments([]);
|
|
160
|
+
setSlashSelected(0);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (e.key === "Escape") {
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
setInput("");
|
|
166
|
+
setSlashSelected(0);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
submitCurrent();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (e.key === "Escape") {
|
|
178
|
+
if ((disabled || streaming) && onCancel) onCancel();
|
|
179
|
+
if (slashOpen) setInput("");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (e.key === "ArrowUp" && !input.includes("\n") && !slashOpen) {
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
if (history.length > 0 && historyIndex < history.length - 1) {
|
|
186
|
+
const idx = historyIndex + 1;
|
|
187
|
+
setHistoryIndex(idx);
|
|
188
|
+
setInput(history[history.length - 1 - idx] || "");
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (e.key === "ArrowDown" && !input.includes("\n") && !slashOpen) {
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
if (historyIndex > 0) {
|
|
196
|
+
const idx = historyIndex - 1;
|
|
197
|
+
setHistoryIndex(idx);
|
|
198
|
+
setInput(history[history.length - 1 - idx] || "");
|
|
199
|
+
} else if (historyIndex === 0) {
|
|
200
|
+
setHistoryIndex(-1);
|
|
201
|
+
setInput("");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const handleChange = (e) => {
|
|
207
|
+
setInput(e.target.value);
|
|
208
|
+
setHistoryIndex(-1);
|
|
209
|
+
const ta = e.target;
|
|
210
|
+
ta.style.height = "auto";
|
|
211
|
+
ta.style.height = Math.min(ta.scrollHeight, 150) + "px";
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const addAttachment = async (file) => {
|
|
215
|
+
if (!file) return;
|
|
216
|
+
if (file.size > MAX_ATTACHMENT_BYTES) {
|
|
217
|
+
setAttachStatus(`File too large (${file.size} bytes). Max ${MAX_ATTACHMENT_BYTES} bytes.`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
setAttachStatus(`Reading ${file.name}...`);
|
|
222
|
+
const raw = await file.text();
|
|
223
|
+
if (raw.includes("\u0000")) {
|
|
224
|
+
setAttachStatus("This looks like a binary file. Please attach a text-based file.");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const truncated = raw.length > MAX_ATTACHMENT_CHARS;
|
|
228
|
+
const text = truncated ? raw.slice(0, MAX_ATTACHMENT_CHARS) : raw;
|
|
229
|
+
const block = formatAttachmentPrompt({
|
|
230
|
+
name: file.name,
|
|
231
|
+
type: file.type,
|
|
232
|
+
size: file.size,
|
|
233
|
+
text,
|
|
234
|
+
truncated,
|
|
235
|
+
});
|
|
236
|
+
setPendingAttachments((prev) => [
|
|
237
|
+
...prev,
|
|
238
|
+
{
|
|
239
|
+
name: file.name,
|
|
240
|
+
type: file.type,
|
|
241
|
+
size: file.size,
|
|
242
|
+
text,
|
|
243
|
+
truncated,
|
|
244
|
+
},
|
|
245
|
+
]);
|
|
246
|
+
setAttachStatus(`Attached ${file.name}`);
|
|
247
|
+
textareaRef.current?.focus();
|
|
248
|
+
} catch {
|
|
249
|
+
setAttachStatus("Could not read file. Please try again.");
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const handleFileChange = async (e) => {
|
|
254
|
+
const files = Array.from(e.target.files || []);
|
|
255
|
+
for (const file of files) {
|
|
256
|
+
// Keep reads sequential to avoid jarring status flicker.
|
|
257
|
+
// eslint-disable-next-line no-await-in-loop
|
|
258
|
+
await addAttachment(file);
|
|
259
|
+
}
|
|
260
|
+
e.target.value = "";
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const removeAttachment = (name) => {
|
|
264
|
+
setPendingAttachments((prev) => prev.filter((file) => file.name !== name));
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const resizeTextarea = () => {
|
|
268
|
+
const ta = textareaRef.current;
|
|
269
|
+
if (!ta) return;
|
|
270
|
+
ta.style.height = "auto";
|
|
271
|
+
ta.style.height = Math.min(ta.scrollHeight, 150) + "px";
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const applyTranscript = (transcript) => {
|
|
275
|
+
const base = sttBaseRef.current || "";
|
|
276
|
+
const sep = base && !base.endsWith(" ") ? " " : "";
|
|
277
|
+
const merged = `${base}${sep}${String(transcript || "").trim()}`.trim();
|
|
278
|
+
setInput(merged);
|
|
279
|
+
setHistoryIndex(-1);
|
|
280
|
+
requestAnimationFrame(resizeTextarea);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const toggleDictation = () => {
|
|
284
|
+
if (disabled || !sttSupported) return;
|
|
285
|
+
if (sttListening) {
|
|
286
|
+
try {
|
|
287
|
+
sttStoppingRef.current = true;
|
|
288
|
+
speechRef.current?.stop?.();
|
|
289
|
+
} catch {}
|
|
290
|
+
setSttListening(false);
|
|
291
|
+
setSttStatus("Stopped listening.");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const Recognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
296
|
+
if (typeof Recognition !== "function") {
|
|
297
|
+
setSttStatus("Speech recognition is not available in this browser.");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const recognition = new Recognition();
|
|
302
|
+
speechRef.current = recognition;
|
|
303
|
+
recognition.lang = navigator?.language || "en-US";
|
|
304
|
+
recognition.interimResults = true;
|
|
305
|
+
recognition.continuous = true;
|
|
306
|
+
|
|
307
|
+
sttBaseRef.current = input.trim();
|
|
308
|
+
setSttStatus("Listening… speak now.");
|
|
309
|
+
setSttListening(true);
|
|
310
|
+
sttStoppingRef.current = false;
|
|
311
|
+
|
|
312
|
+
recognition.onresult = (event) => {
|
|
313
|
+
const transcript = Array.from(event.results || [])
|
|
314
|
+
.map((r) => r?.[0]?.transcript || "")
|
|
315
|
+
.join(" ")
|
|
316
|
+
.trim();
|
|
317
|
+
applyTranscript(transcript);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
recognition.onerror = (event) => {
|
|
321
|
+
const code = event?.error || "unknown";
|
|
322
|
+
if (code === "aborted" || sttStoppingRef.current) {
|
|
323
|
+
setSttStatus("Stopped listening.");
|
|
324
|
+
setSttListening(false);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const message = code === "not-allowed"
|
|
328
|
+
? "Microphone permission denied."
|
|
329
|
+
: code === "no-speech"
|
|
330
|
+
? "No speech detected."
|
|
331
|
+
: `Speech recognition error: ${code}`;
|
|
332
|
+
setSttStatus(message);
|
|
333
|
+
setSttListening(false);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
recognition.onend = () => {
|
|
337
|
+
setSttListening(false);
|
|
338
|
+
sttStoppingRef.current = false;
|
|
339
|
+
setSttStatus((prev) => prev || "Stopped listening.");
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
recognition.start();
|
|
344
|
+
} catch {
|
|
345
|
+
setSttListening(false);
|
|
346
|
+
setSttStatus("Could not start speech recognition.");
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
return (
|
|
351
|
+
<div className="border-t border-[#1a1a28] bg-[#0c0c12] px-4 py-3 relative">
|
|
352
|
+
{slashOpen && slashMatches.length > 0 && (
|
|
353
|
+
<div
|
|
354
|
+
className="absolute bottom-full left-4 right-4 mb-1 max-h-52 overflow-y-auto rounded-lg border border-[#26263a] bg-[#0c0c12] shadow-lg z-10"
|
|
355
|
+
role="listbox"
|
|
356
|
+
>
|
|
357
|
+
{slashMatches.map((c, i) => (
|
|
358
|
+
<div
|
|
359
|
+
key={c.name}
|
|
360
|
+
role="option"
|
|
361
|
+
aria-selected={i === slashSelected}
|
|
362
|
+
className={`flex items-center justify-between gap-3 px-3 py-2 cursor-pointer text-sm ${
|
|
363
|
+
i === slashSelected
|
|
364
|
+
? "bg-[#f97316]/20 text-[#f97316]"
|
|
365
|
+
: "text-[#e0dfe4] hover:bg-[#1a1a28]"
|
|
366
|
+
}`}
|
|
367
|
+
onClick={() => {
|
|
368
|
+
completeSlash(c.name);
|
|
369
|
+
setHistory((prev) => [...prev, c.name.trim()]);
|
|
370
|
+
onSubmit(c.name.trim());
|
|
371
|
+
setInput("");
|
|
372
|
+
}}
|
|
373
|
+
onMouseEnter={() => setSlashSelected(i)}
|
|
374
|
+
>
|
|
375
|
+
<span className="font-mono font-semibold">{c.name}</span>
|
|
376
|
+
<span className="text-[#6b6b80] text-xs">{c.desc}</span>
|
|
377
|
+
</div>
|
|
378
|
+
))}
|
|
379
|
+
</div>
|
|
380
|
+
)}
|
|
381
|
+
<div
|
|
382
|
+
className={`
|
|
383
|
+
flex items-end gap-3 bg-[#08080c] border rounded-xl px-4 py-2.5
|
|
384
|
+
transition-all duration-300
|
|
385
|
+
${disabled
|
|
386
|
+
? "border-[#1a1a28] opacity-60"
|
|
387
|
+
: "border-[#26263a] focus-within:border-[#f97316]/50 focus-within:shadow-[0_0_20px_rgba(249,115,22,0.08)]"
|
|
388
|
+
}
|
|
389
|
+
`}
|
|
390
|
+
>
|
|
391
|
+
{pendingAttachments.length > 0 && (
|
|
392
|
+
<div className="absolute left-8 right-8 -top-9 flex flex-wrap gap-1.5">
|
|
393
|
+
{pendingAttachments.map((file) => (
|
|
394
|
+
<span
|
|
395
|
+
key={`pending-${file.name}`}
|
|
396
|
+
className="inline-flex max-w-[240px] items-center gap-1.5 rounded-md border border-[#26263a] bg-[#0f0f16] px-2 py-1 text-[11px] text-[#b5b5c4]"
|
|
397
|
+
>
|
|
398
|
+
<span aria-hidden="true">📄</span>
|
|
399
|
+
<span className="truncate">{file.name}</span>
|
|
400
|
+
<button
|
|
401
|
+
type="button"
|
|
402
|
+
onClick={() => removeAttachment(file.name)}
|
|
403
|
+
className="text-[#7f7f95] hover:text-[#f97316] transition-colors"
|
|
404
|
+
aria-label={`Remove ${file.name}`}
|
|
405
|
+
title={`Remove ${file.name}`}
|
|
406
|
+
>
|
|
407
|
+
✕
|
|
408
|
+
</button>
|
|
409
|
+
</span>
|
|
410
|
+
))}
|
|
411
|
+
</div>
|
|
412
|
+
)}
|
|
413
|
+
<span
|
|
414
|
+
className={`font-mono text-sm pb-0.5 select-none transition-colors ${
|
|
415
|
+
disabled ? "text-[#26263a]" : "text-[#f97316]"
|
|
416
|
+
}`}
|
|
417
|
+
>
|
|
418
|
+
❯
|
|
419
|
+
</span>
|
|
420
|
+
<textarea
|
|
421
|
+
ref={textareaRef}
|
|
422
|
+
value={input}
|
|
423
|
+
onChange={handleChange}
|
|
424
|
+
onKeyDown={handleKeyDown}
|
|
425
|
+
placeholder={
|
|
426
|
+
disabled
|
|
427
|
+
? "Unavailable"
|
|
428
|
+
: streaming
|
|
429
|
+
? "Agent is responding… send a follow-up (it will queue). Esc to cancel"
|
|
430
|
+
: "Type a message… (/ for commands, Tab to complete, or attach a file)"
|
|
431
|
+
}
|
|
432
|
+
disabled={disabled}
|
|
433
|
+
rows={1}
|
|
434
|
+
className="flex-1 bg-transparent text-[#e0dfe4] text-sm font-mono outline-none resize-none
|
|
435
|
+
placeholder:text-[#2e2e40] disabled:cursor-not-allowed leading-relaxed"
|
|
436
|
+
/>
|
|
437
|
+
<input
|
|
438
|
+
ref={fileInputRef}
|
|
439
|
+
type="file"
|
|
440
|
+
multiple
|
|
441
|
+
className="hidden"
|
|
442
|
+
onChange={handleFileChange}
|
|
443
|
+
disabled={disabled}
|
|
444
|
+
/>
|
|
445
|
+
<button
|
|
446
|
+
type="button"
|
|
447
|
+
onClick={() => fileInputRef.current?.click()}
|
|
448
|
+
disabled={disabled}
|
|
449
|
+
title="Attach a text file"
|
|
450
|
+
className={`
|
|
451
|
+
flex items-center justify-center w-8 h-8 rounded-lg text-sm font-bold
|
|
452
|
+
transition-all duration-200
|
|
453
|
+
${
|
|
454
|
+
disabled
|
|
455
|
+
? "bg-[#18181f] text-[#3a3a50] cursor-not-allowed"
|
|
456
|
+
: "bg-[#18181f] text-[#8b8b9e] hover:text-[#f97316] hover:bg-[#1f1f2c]"
|
|
457
|
+
}
|
|
458
|
+
`}
|
|
459
|
+
>
|
|
460
|
+
📎
|
|
461
|
+
</button>
|
|
462
|
+
<button
|
|
463
|
+
type="button"
|
|
464
|
+
onClick={toggleDictation}
|
|
465
|
+
disabled={disabled || !sttSupported}
|
|
466
|
+
title={sttSupported ? (sttListening ? "Stop voice input" : "Start voice input") : "Voice input not supported in this browser"}
|
|
467
|
+
aria-label={sttSupported ? (sttListening ? "Stop voice input" : "Start voice input") : "Voice input not supported"}
|
|
468
|
+
className={`
|
|
469
|
+
flex items-center justify-center gap-1 min-w-14 h-8 px-2 rounded-lg text-[11px] font-bold border
|
|
470
|
+
transition-all duration-200
|
|
471
|
+
${
|
|
472
|
+
sttListening
|
|
473
|
+
? "bg-[#f97316] text-[#08080c] border-[#fb923c] shadow-[0_0_12px_rgba(249,115,22,0.3)]"
|
|
474
|
+
: disabled || !sttSupported
|
|
475
|
+
? "bg-[#18181f] text-[#6b6b80] border-[#303045] cursor-not-allowed"
|
|
476
|
+
: "bg-[#14141c] text-[#f5f5f7] border-[#5a5a78] hover:text-[#f97316] hover:border-[#f97316] hover:bg-[#1f1f2c]"
|
|
477
|
+
}
|
|
478
|
+
`}
|
|
479
|
+
>
|
|
480
|
+
<span aria-hidden="true">🎤</span>
|
|
481
|
+
Mic
|
|
482
|
+
</button>
|
|
483
|
+
<button
|
|
484
|
+
onClick={submitCurrent}
|
|
485
|
+
disabled={disabled || (!input.trim() && pendingAttachments.length === 0)}
|
|
486
|
+
className={`
|
|
487
|
+
flex items-center justify-center w-8 h-8 rounded-lg text-sm font-bold
|
|
488
|
+
transition-all duration-200
|
|
489
|
+
${
|
|
490
|
+
(input.trim() || pendingAttachments.length > 0) && !disabled
|
|
491
|
+
? "bg-[#f97316] text-[#08080c] hover:bg-[#fb923c] shadow-[0_0_12px_rgba(249,115,22,0.3)]"
|
|
492
|
+
: "bg-[#18181f] text-[#3a3a50] cursor-not-allowed"
|
|
493
|
+
}
|
|
494
|
+
`}
|
|
495
|
+
>
|
|
496
|
+
↑
|
|
497
|
+
</button>
|
|
498
|
+
</div>
|
|
499
|
+
{(streaming || queuedCount > 0) && (
|
|
500
|
+
<div className="mt-2 text-[11px] font-mono text-[#6b6b80]">
|
|
501
|
+
{streaming
|
|
502
|
+
? `Response in progress. ${queuedCount > 0 ? `${queuedCount} follow-up${queuedCount === 1 ? "" : "s"} queued.` : "Send a follow-up to queue it."}`
|
|
503
|
+
: `${queuedCount} queued follow-up${queuedCount === 1 ? "" : "s"} waiting to send.`}
|
|
504
|
+
</div>
|
|
505
|
+
)}
|
|
506
|
+
{attachStatus && (
|
|
507
|
+
<div className="mt-2 text-[11px] font-mono text-[#6b6b80]">
|
|
508
|
+
{attachStatus}
|
|
509
|
+
</div>
|
|
510
|
+
)}
|
|
511
|
+
{sttStatus && (
|
|
512
|
+
<div className="mt-1 text-[11px] font-mono text-[#6b6b80]">
|
|
513
|
+
{sttStatus}
|
|
514
|
+
</div>
|
|
515
|
+
)}
|
|
516
|
+
{!sttSupported && (
|
|
517
|
+
<div className="mt-1 text-[11px] font-mono text-[#6b6b80]">
|
|
518
|
+
Voice input not supported in this browser. Try Chrome/Edge.
|
|
519
|
+
</div>
|
|
520
|
+
)}
|
|
521
|
+
</div>
|
|
522
|
+
);
|
|
523
|
+
}
|