@run0/jiki-ui 0.1.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/dist/AIChatPanel.d.ts +26 -0
- package/dist/BrowserWindow.d.ts +22 -0
- package/dist/CodeEditor.d.ts +8 -0
- package/dist/FileExplorer.d.ts +9 -0
- package/dist/MobileTabBar.d.ts +28 -0
- package/dist/PanelToggle.d.ts +9 -0
- package/dist/Terminal.d.ts +11 -0
- package/dist/chat-theme.d.ts +11 -0
- package/dist/index.cjs +1742 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.mjs +1720 -0
- package/dist/index.mjs.map +1 -0
- package/dist/language-labels.d.ts +1 -0
- package/dist/shiki-languages.d.ts +1 -0
- package/dist/theme.d.ts +19 -0
- package/dist/types.d.ts +12 -0
- package/dist/use-shiki-highlighter.d.ts +7 -0
- package/dist/useMediaQuery.d.ts +1 -0
- package/package.json +52 -0
- package/src/AIChatPanel.tsx +577 -0
- package/src/BrowserWindow.tsx +370 -0
- package/src/CodeEditor.tsx +185 -0
- package/src/FileExplorer.tsx +157 -0
- package/src/MobileTabBar.tsx +133 -0
- package/src/PanelToggle.tsx +101 -0
- package/src/Terminal.tsx +178 -0
- package/src/chat-theme.ts +81 -0
- package/src/index.ts +33 -0
- package/src/language-labels.ts +26 -0
- package/src/shiki-languages.ts +27 -0
- package/src/theme.ts +144 -0
- package/src/types.ts +21 -0
- package/src/use-shiki-highlighter.ts +113 -0
- package/src/useMediaQuery.ts +17 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1742 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
25
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
26
|
+
const react = require("react");
|
|
27
|
+
const EDITOR_THEMES = {
|
|
28
|
+
emerald: {
|
|
29
|
+
saveButtonActive: "bg-emerald-500/20 text-emerald-400 hover:bg-emerald-500/30 border border-emerald-500/30",
|
|
30
|
+
caret: "caret-emerald-400",
|
|
31
|
+
selection: "selection:bg-emerald-500/20"
|
|
32
|
+
},
|
|
33
|
+
violet: {
|
|
34
|
+
saveButtonActive: "bg-violet-500/20 text-violet-400 hover:bg-violet-500/30 border border-violet-500/30",
|
|
35
|
+
caret: "caret-violet-400",
|
|
36
|
+
selection: "selection:bg-violet-500/20"
|
|
37
|
+
},
|
|
38
|
+
orange: {
|
|
39
|
+
saveButtonActive: "bg-orange-500/20 text-orange-400 hover:bg-orange-500/30 border border-orange-500/30",
|
|
40
|
+
caret: "caret-orange-400",
|
|
41
|
+
selection: "selection:bg-orange-500/20"
|
|
42
|
+
},
|
|
43
|
+
blue: {
|
|
44
|
+
saveButtonActive: "bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 border border-blue-500/30",
|
|
45
|
+
caret: "caret-blue-400",
|
|
46
|
+
selection: "selection:bg-blue-500/20"
|
|
47
|
+
},
|
|
48
|
+
pink: {
|
|
49
|
+
saveButtonActive: "bg-pink-500/20 text-pink-400 hover:bg-pink-500/30 border border-pink-500/30",
|
|
50
|
+
caret: "caret-pink-400",
|
|
51
|
+
selection: "selection:bg-pink-500/20"
|
|
52
|
+
},
|
|
53
|
+
green: {
|
|
54
|
+
saveButtonActive: "bg-green-500/20 text-green-400 hover:bg-green-500/30 border border-green-500/30",
|
|
55
|
+
caret: "caret-green-400",
|
|
56
|
+
selection: "selection:bg-green-500/20"
|
|
57
|
+
},
|
|
58
|
+
amber: {
|
|
59
|
+
saveButtonActive: "bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 border border-amber-500/30",
|
|
60
|
+
caret: "caret-amber-400",
|
|
61
|
+
selection: "selection:bg-amber-500/20"
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const TERMINAL_THEMES = {
|
|
65
|
+
emerald: {
|
|
66
|
+
commandStyle: "text-emerald-400 font-semibold",
|
|
67
|
+
promptColor: "text-emerald-500",
|
|
68
|
+
spinnerBorder: "border-t-emerald-400",
|
|
69
|
+
inputCaret: "caret-emerald-400"
|
|
70
|
+
},
|
|
71
|
+
violet: {
|
|
72
|
+
commandStyle: "text-violet-400 font-semibold",
|
|
73
|
+
promptColor: "text-violet-500",
|
|
74
|
+
spinnerBorder: "border-t-violet-400",
|
|
75
|
+
inputCaret: "caret-violet-400"
|
|
76
|
+
},
|
|
77
|
+
orange: {
|
|
78
|
+
commandStyle: "text-orange-400 font-semibold",
|
|
79
|
+
promptColor: "text-orange-500",
|
|
80
|
+
spinnerBorder: "border-t-orange-400",
|
|
81
|
+
inputCaret: "caret-orange-400"
|
|
82
|
+
},
|
|
83
|
+
blue: {
|
|
84
|
+
commandStyle: "text-blue-400 font-semibold",
|
|
85
|
+
promptColor: "text-blue-500",
|
|
86
|
+
spinnerBorder: "border-t-blue-400",
|
|
87
|
+
inputCaret: "caret-blue-400"
|
|
88
|
+
},
|
|
89
|
+
pink: {
|
|
90
|
+
commandStyle: "text-pink-400 font-semibold",
|
|
91
|
+
promptColor: "text-pink-500",
|
|
92
|
+
spinnerBorder: "border-t-pink-400",
|
|
93
|
+
inputCaret: "caret-pink-400"
|
|
94
|
+
},
|
|
95
|
+
green: {
|
|
96
|
+
commandStyle: "text-green-400 font-semibold",
|
|
97
|
+
promptColor: "text-green-500",
|
|
98
|
+
spinnerBorder: "border-t-green-400",
|
|
99
|
+
inputCaret: "caret-green-400"
|
|
100
|
+
},
|
|
101
|
+
amber: {
|
|
102
|
+
commandStyle: "text-amber-400 font-semibold",
|
|
103
|
+
promptColor: "text-amber-500",
|
|
104
|
+
spinnerBorder: "border-t-amber-400",
|
|
105
|
+
inputCaret: "caret-amber-400"
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const FILE_EXPLORER_THEMES = {
|
|
109
|
+
emerald: { selected: "bg-emerald-500/15 text-emerald-300" },
|
|
110
|
+
violet: { selected: "bg-violet-500/15 text-violet-300" },
|
|
111
|
+
orange: { selected: "bg-orange-500/15 text-orange-300" },
|
|
112
|
+
blue: { selected: "bg-blue-500/15 text-blue-300" },
|
|
113
|
+
pink: { selected: "bg-pink-500/15 text-pink-300" },
|
|
114
|
+
green: { selected: "bg-green-500/15 text-green-300" },
|
|
115
|
+
amber: { selected: "bg-amber-500/15 text-amber-300" }
|
|
116
|
+
};
|
|
117
|
+
function getFileExplorerTheme(color) {
|
|
118
|
+
return FILE_EXPLORER_THEMES[color];
|
|
119
|
+
}
|
|
120
|
+
function getEditorTheme(color) {
|
|
121
|
+
return EDITOR_THEMES[color];
|
|
122
|
+
}
|
|
123
|
+
function getTerminalTheme(color) {
|
|
124
|
+
return TERMINAL_THEMES[color];
|
|
125
|
+
}
|
|
126
|
+
function getLineStyles(color, overrides) {
|
|
127
|
+
return {
|
|
128
|
+
command: TERMINAL_THEMES[color].commandStyle,
|
|
129
|
+
stdout: "text-zinc-300",
|
|
130
|
+
stderr: "text-red-400",
|
|
131
|
+
info: "text-blue-400 italic",
|
|
132
|
+
success: "text-emerald-400",
|
|
133
|
+
...overrides
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const LANGUAGE_MAP = {
|
|
137
|
+
js: "JavaScript",
|
|
138
|
+
jsx: "JSX",
|
|
139
|
+
mjs: "JavaScript",
|
|
140
|
+
cjs: "JavaScript",
|
|
141
|
+
ts: "TypeScript",
|
|
142
|
+
tsx: "TSX",
|
|
143
|
+
mts: "TypeScript",
|
|
144
|
+
cts: "TypeScript",
|
|
145
|
+
json: "JSON",
|
|
146
|
+
md: "Markdown",
|
|
147
|
+
html: "HTML",
|
|
148
|
+
css: "CSS",
|
|
149
|
+
vue: "Vue SFC",
|
|
150
|
+
svelte: "Svelte",
|
|
151
|
+
astro: "Astro",
|
|
152
|
+
yaml: "YAML",
|
|
153
|
+
yml: "YAML",
|
|
154
|
+
toml: "TOML",
|
|
155
|
+
sh: "Shell"
|
|
156
|
+
};
|
|
157
|
+
function getLanguageLabel(filename) {
|
|
158
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
159
|
+
return LANGUAGE_MAP[ext || ""] || "Plain Text";
|
|
160
|
+
}
|
|
161
|
+
const SHIKI_LANG_MAP = {
|
|
162
|
+
js: "javascript",
|
|
163
|
+
mjs: "javascript",
|
|
164
|
+
cjs: "javascript",
|
|
165
|
+
ts: "typescript",
|
|
166
|
+
mts: "typescript",
|
|
167
|
+
cts: "typescript",
|
|
168
|
+
jsx: "jsx",
|
|
169
|
+
tsx: "tsx",
|
|
170
|
+
json: "json",
|
|
171
|
+
html: "html",
|
|
172
|
+
css: "css",
|
|
173
|
+
md: "markdown",
|
|
174
|
+
vue: "vue",
|
|
175
|
+
svelte: "svelte",
|
|
176
|
+
astro: "astro",
|
|
177
|
+
yaml: "yaml",
|
|
178
|
+
yml: "yaml",
|
|
179
|
+
toml: "toml",
|
|
180
|
+
sh: "shellscript"
|
|
181
|
+
};
|
|
182
|
+
function getShikiLang(filename) {
|
|
183
|
+
if (!filename) return "plaintext";
|
|
184
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
185
|
+
return SHIKI_LANG_MAP[ext || ""] || "plaintext";
|
|
186
|
+
}
|
|
187
|
+
let highlighterInstance = null;
|
|
188
|
+
let highlighterPromise = null;
|
|
189
|
+
async function getOrCreateHighlighter() {
|
|
190
|
+
if (highlighterInstance) return highlighterInstance;
|
|
191
|
+
if (highlighterPromise) return highlighterPromise;
|
|
192
|
+
highlighterPromise = import("shiki").then(
|
|
193
|
+
({ createHighlighter }) => createHighlighter({ themes: ["github-dark"], langs: [] })
|
|
194
|
+
).then((h) => {
|
|
195
|
+
highlighterInstance = h;
|
|
196
|
+
return h;
|
|
197
|
+
});
|
|
198
|
+
return highlighterPromise;
|
|
199
|
+
}
|
|
200
|
+
function escapeHtml(text) {
|
|
201
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
202
|
+
}
|
|
203
|
+
function buildFallbackHtml(code) {
|
|
204
|
+
return `<pre style="margin:0;background:transparent"><code style="color:#e4e4e7">${escapeHtml(code)}</code></pre>`;
|
|
205
|
+
}
|
|
206
|
+
async function doHighlight(code, filename, el) {
|
|
207
|
+
if (!el || !highlighterInstance) return;
|
|
208
|
+
const lang = getShikiLang(filename);
|
|
209
|
+
if (lang !== "plaintext" && !highlighterInstance.getLoadedLanguages().includes(lang)) {
|
|
210
|
+
try {
|
|
211
|
+
await highlighterInstance.loadLanguage(
|
|
212
|
+
lang
|
|
213
|
+
);
|
|
214
|
+
} catch {
|
|
215
|
+
el.innerHTML = buildFallbackHtml(code);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
el.innerHTML = highlighterInstance.codeToHtml(code, {
|
|
221
|
+
lang,
|
|
222
|
+
theme: "github-dark"
|
|
223
|
+
});
|
|
224
|
+
} catch {
|
|
225
|
+
el.innerHTML = buildFallbackHtml(code);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function useShikiHighlighter(code, filename, targetRef) {
|
|
229
|
+
const readyRef = react.useRef(false);
|
|
230
|
+
const debounceRef = react.useRef(
|
|
231
|
+
void 0
|
|
232
|
+
);
|
|
233
|
+
react.useEffect(() => {
|
|
234
|
+
let cancelled = false;
|
|
235
|
+
getOrCreateHighlighter().then(() => {
|
|
236
|
+
if (cancelled) return;
|
|
237
|
+
readyRef.current = true;
|
|
238
|
+
doHighlight(code, filename, targetRef.current);
|
|
239
|
+
});
|
|
240
|
+
return () => {
|
|
241
|
+
cancelled = true;
|
|
242
|
+
};
|
|
243
|
+
}, []);
|
|
244
|
+
react.useEffect(() => {
|
|
245
|
+
if (!readyRef.current) {
|
|
246
|
+
if (targetRef.current) {
|
|
247
|
+
targetRef.current.innerHTML = buildFallbackHtml(code);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
clearTimeout(debounceRef.current);
|
|
252
|
+
debounceRef.current = setTimeout(() => {
|
|
253
|
+
doHighlight(code, filename, targetRef.current);
|
|
254
|
+
}, 30);
|
|
255
|
+
return () => clearTimeout(debounceRef.current);
|
|
256
|
+
}, [code, filename]);
|
|
257
|
+
}
|
|
258
|
+
function CodeEditor({
|
|
259
|
+
filename,
|
|
260
|
+
content,
|
|
261
|
+
onSave,
|
|
262
|
+
accentColor = "emerald"
|
|
263
|
+
}) {
|
|
264
|
+
const [localContent, setLocalContent] = react.useState(content);
|
|
265
|
+
const [isDirty, setIsDirty] = react.useState(false);
|
|
266
|
+
const textareaRef = react.useRef(null);
|
|
267
|
+
const lineCountRef = react.useRef(null);
|
|
268
|
+
const highlightRef = react.useRef(null);
|
|
269
|
+
const theme = getEditorTheme(accentColor);
|
|
270
|
+
useShikiHighlighter(localContent, filename, highlightRef);
|
|
271
|
+
react.useEffect(() => {
|
|
272
|
+
setLocalContent(content);
|
|
273
|
+
setIsDirty(false);
|
|
274
|
+
}, [content, filename]);
|
|
275
|
+
const handleChange = react.useCallback(
|
|
276
|
+
(e) => {
|
|
277
|
+
setLocalContent(e.target.value);
|
|
278
|
+
setIsDirty(e.target.value !== content);
|
|
279
|
+
},
|
|
280
|
+
[content]
|
|
281
|
+
);
|
|
282
|
+
const handleSave = react.useCallback(() => {
|
|
283
|
+
if (filename && isDirty) {
|
|
284
|
+
onSave(filename, localContent);
|
|
285
|
+
setIsDirty(false);
|
|
286
|
+
}
|
|
287
|
+
}, [filename, isDirty, localContent, onSave]);
|
|
288
|
+
const handleKeyDown = react.useCallback(
|
|
289
|
+
(e) => {
|
|
290
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
|
291
|
+
e.preventDefault();
|
|
292
|
+
handleSave();
|
|
293
|
+
}
|
|
294
|
+
if (e.key === "Tab") {
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
const ta = textareaRef.current;
|
|
297
|
+
if (!ta) return;
|
|
298
|
+
const start = ta.selectionStart;
|
|
299
|
+
const end = ta.selectionEnd;
|
|
300
|
+
const val = ta.value;
|
|
301
|
+
const newVal = val.substring(0, start) + " " + val.substring(end);
|
|
302
|
+
setLocalContent(newVal);
|
|
303
|
+
setIsDirty(newVal !== content);
|
|
304
|
+
requestAnimationFrame(() => {
|
|
305
|
+
ta.selectionStart = ta.selectionEnd = start + 2;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
[handleSave, content]
|
|
310
|
+
);
|
|
311
|
+
const handleScroll = react.useCallback(() => {
|
|
312
|
+
if (textareaRef.current) {
|
|
313
|
+
if (lineCountRef.current) {
|
|
314
|
+
lineCountRef.current.scrollTop = textareaRef.current.scrollTop;
|
|
315
|
+
}
|
|
316
|
+
if (highlightRef.current) {
|
|
317
|
+
highlightRef.current.scrollTop = textareaRef.current.scrollTop;
|
|
318
|
+
highlightRef.current.scrollLeft = textareaRef.current.scrollLeft;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}, []);
|
|
322
|
+
if (!filename) {
|
|
323
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full flex items-center justify-center text-zinc-600 text-sm", children: "Select a file to edit" });
|
|
324
|
+
}
|
|
325
|
+
const lineCount = localContent.split("\n").length;
|
|
326
|
+
const lineNumbers = react.useMemo(
|
|
327
|
+
() => Array.from({ length: lineCount }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
328
|
+
"div",
|
|
329
|
+
{
|
|
330
|
+
className: "px-3 text-right text-zinc-600 text-[12px] leading-5",
|
|
331
|
+
style: { minWidth: "3rem" },
|
|
332
|
+
children: i + 1
|
|
333
|
+
},
|
|
334
|
+
i
|
|
335
|
+
)),
|
|
336
|
+
[lineCount]
|
|
337
|
+
);
|
|
338
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-full flex flex-col", children: [
|
|
339
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-3 py-1.5 bg-zinc-900/50 border-b border-zinc-800", children: [
|
|
340
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
341
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[13px] font-mono text-zinc-300", children: filename }),
|
|
342
|
+
isDirty && /* @__PURE__ */ jsxRuntime.jsx(
|
|
343
|
+
"span",
|
|
344
|
+
{
|
|
345
|
+
className: "inline-block h-2 w-2 rounded-full bg-amber-400",
|
|
346
|
+
title: "Unsaved changes"
|
|
347
|
+
}
|
|
348
|
+
)
|
|
349
|
+
] }),
|
|
350
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
351
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] text-zinc-600", children: getLanguageLabel(filename) }),
|
|
352
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
353
|
+
"button",
|
|
354
|
+
{
|
|
355
|
+
onClick: handleSave,
|
|
356
|
+
disabled: !isDirty,
|
|
357
|
+
className: `
|
|
358
|
+
text-[11px] px-2.5 py-1 rounded font-medium transition-colors
|
|
359
|
+
${isDirty ? theme.saveButtonActive : "bg-zinc-800 text-zinc-600 border border-zinc-700 cursor-not-allowed"}
|
|
360
|
+
`,
|
|
361
|
+
children: "Save"
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
] })
|
|
365
|
+
] }),
|
|
366
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex overflow-hidden relative font-mono text-[13px] leading-5", children: [
|
|
367
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
368
|
+
"div",
|
|
369
|
+
{
|
|
370
|
+
ref: lineCountRef,
|
|
371
|
+
className: "flex-shrink-0 overflow-hidden bg-zinc-900/30 select-none",
|
|
372
|
+
children: lineNumbers
|
|
373
|
+
}
|
|
374
|
+
),
|
|
375
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 relative min-w-0", children: [
|
|
376
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
377
|
+
"div",
|
|
378
|
+
{
|
|
379
|
+
ref: highlightRef,
|
|
380
|
+
className: "absolute inset-0 overflow-hidden p-2 pointer-events-none [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:!font-[inherit] [&_pre]:![font-size:inherit] [&_pre]:![line-height:inherit] [&_pre]:![letter-spacing:inherit] [&_pre]:![white-space:pre] [&_pre]:![word-wrap:normal] [&_code]:!font-[inherit] [&_code]:![font-size:inherit] [&_code]:![line-height:inherit] [&_.shiki]:!overflow-visible",
|
|
381
|
+
style: { tabSize: 2 }
|
|
382
|
+
}
|
|
383
|
+
),
|
|
384
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
385
|
+
"textarea",
|
|
386
|
+
{
|
|
387
|
+
ref: textareaRef,
|
|
388
|
+
value: localContent,
|
|
389
|
+
onChange: handleChange,
|
|
390
|
+
onKeyDown: handleKeyDown,
|
|
391
|
+
onScroll: handleScroll,
|
|
392
|
+
wrap: "off",
|
|
393
|
+
spellCheck: false,
|
|
394
|
+
className: `
|
|
395
|
+
absolute inset-0 w-full h-full resize-none bg-transparent p-2
|
|
396
|
+
text-transparent outline-none overflow-auto whitespace-pre
|
|
397
|
+
${theme.caret} ${theme.selection}
|
|
398
|
+
`,
|
|
399
|
+
style: { tabSize: 2, overflowWrap: "normal", wordBreak: "normal" }
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
] })
|
|
403
|
+
] }),
|
|
404
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-3 py-1 bg-zinc-900/30 border-t border-zinc-800 text-[11px] text-zinc-600", children: [
|
|
405
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
406
|
+
lineCount,
|
|
407
|
+
" lines"
|
|
408
|
+
] }),
|
|
409
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Ctrl+S / Cmd+S to save" })
|
|
410
|
+
] })
|
|
411
|
+
] });
|
|
412
|
+
}
|
|
413
|
+
function Terminal({
|
|
414
|
+
lines,
|
|
415
|
+
onCommand,
|
|
416
|
+
onClear,
|
|
417
|
+
accentColor = "emerald",
|
|
418
|
+
variant = "default",
|
|
419
|
+
title = "Terminal",
|
|
420
|
+
lineStyles: lineStyleOverrides
|
|
421
|
+
}) {
|
|
422
|
+
const [input, setInput] = react.useState("");
|
|
423
|
+
const [history, setHistory] = react.useState([]);
|
|
424
|
+
const [historyIdx, setHistoryIdx] = react.useState(-1);
|
|
425
|
+
const [isRunning, setIsRunning] = react.useState(false);
|
|
426
|
+
const scrollRef = react.useRef(null);
|
|
427
|
+
const inputRef = react.useRef(null);
|
|
428
|
+
const termTheme = getTerminalTheme(accentColor);
|
|
429
|
+
const styles = getLineStyles(accentColor, lineStyleOverrides);
|
|
430
|
+
const isCompact = variant === "compact";
|
|
431
|
+
const hasInput = !!onCommand;
|
|
432
|
+
react.useEffect(() => {
|
|
433
|
+
const el = scrollRef.current;
|
|
434
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
435
|
+
}, [lines]);
|
|
436
|
+
const executeCommand = react.useCallback(
|
|
437
|
+
async (cmd) => {
|
|
438
|
+
if (cmd === "clear") {
|
|
439
|
+
onClear();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (!onCommand) return;
|
|
443
|
+
setIsRunning(true);
|
|
444
|
+
await onCommand(cmd);
|
|
445
|
+
setIsRunning(false);
|
|
446
|
+
inputRef.current?.focus();
|
|
447
|
+
},
|
|
448
|
+
[onCommand, onClear]
|
|
449
|
+
);
|
|
450
|
+
const handleKeyDown = react.useCallback(
|
|
451
|
+
(e) => {
|
|
452
|
+
if (e.key === "Enter") {
|
|
453
|
+
e.preventDefault();
|
|
454
|
+
const cmd = e.currentTarget.value.trim();
|
|
455
|
+
if (!cmd || isRunning) return;
|
|
456
|
+
setInput("");
|
|
457
|
+
setHistory((prev) => [...prev, cmd]);
|
|
458
|
+
setHistoryIdx(-1);
|
|
459
|
+
executeCommand(cmd);
|
|
460
|
+
} else if (e.key === "ArrowUp") {
|
|
461
|
+
e.preventDefault();
|
|
462
|
+
if (history.length === 0) return;
|
|
463
|
+
const newIdx = historyIdx === -1 ? history.length - 1 : Math.max(0, historyIdx - 1);
|
|
464
|
+
setHistoryIdx(newIdx);
|
|
465
|
+
setInput(history[newIdx]);
|
|
466
|
+
} else if (e.key === "ArrowDown") {
|
|
467
|
+
e.preventDefault();
|
|
468
|
+
if (historyIdx === -1) return;
|
|
469
|
+
const newIdx = historyIdx + 1;
|
|
470
|
+
if (newIdx >= history.length) {
|
|
471
|
+
setHistoryIdx(-1);
|
|
472
|
+
setInput("");
|
|
473
|
+
} else {
|
|
474
|
+
setHistoryIdx(newIdx);
|
|
475
|
+
setInput(history[newIdx]);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
[history, historyIdx, isRunning, executeCommand]
|
|
480
|
+
);
|
|
481
|
+
const headerPy = isCompact ? "py-1" : "py-1.5";
|
|
482
|
+
const dotSize = isCompact ? "h-2 w-2" : "h-2.5 w-2.5";
|
|
483
|
+
const dotOpacity = isCompact ? "bg-red-500/60" : "bg-red-500/70";
|
|
484
|
+
const dotYellow = isCompact ? "bg-yellow-500/60" : "bg-yellow-500/70";
|
|
485
|
+
const dotGreen = isCompact ? "bg-green-500/60" : "bg-green-500/70";
|
|
486
|
+
const titleSize = isCompact ? "text-[10px]" : "text-[11px]";
|
|
487
|
+
const titleMl = isCompact ? "ml-1" : "";
|
|
488
|
+
const clearSize = isCompact ? "text-[10px]" : "text-[11px]";
|
|
489
|
+
const outputClass = isCompact ? "flex-1 overflow-y-auto px-3 py-1.5 font-mono text-[11px] leading-4" : "flex-1 overflow-y-auto p-3 font-mono text-[13px] leading-5";
|
|
490
|
+
const inputPy = isCompact ? "py-1.5" : "py-2";
|
|
491
|
+
const inputGap = isCompact ? "gap-1.5" : "gap-2";
|
|
492
|
+
const promptSize = isCompact ? "text-[11px]" : "text-sm";
|
|
493
|
+
const inputFontSize = isCompact ? "text-[11px]" : "text-[13px]";
|
|
494
|
+
const spinnerSize = isCompact ? "h-2.5 w-2.5" : "h-3 w-3";
|
|
495
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-full flex flex-col bg-zinc-950", children: [
|
|
496
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
497
|
+
"div",
|
|
498
|
+
{
|
|
499
|
+
className: `flex items-center justify-between px-3 ${headerPy} bg-zinc-900/50 border-b border-zinc-800`,
|
|
500
|
+
children: [
|
|
501
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
502
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
|
|
503
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `${dotSize} rounded-full ${dotOpacity}` }),
|
|
504
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `${dotSize} rounded-full ${dotYellow}` }),
|
|
505
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `${dotSize} rounded-full ${dotGreen}` })
|
|
506
|
+
] }),
|
|
507
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
508
|
+
"span",
|
|
509
|
+
{
|
|
510
|
+
className: `${titleSize} font-semibold uppercase tracking-wider text-zinc-500 ${titleMl}`,
|
|
511
|
+
children: title
|
|
512
|
+
}
|
|
513
|
+
)
|
|
514
|
+
] }),
|
|
515
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
516
|
+
"button",
|
|
517
|
+
{
|
|
518
|
+
onClick: onClear,
|
|
519
|
+
className: `${clearSize} text-zinc-600 hover:text-zinc-400 transition-colors`,
|
|
520
|
+
children: "Clear"
|
|
521
|
+
}
|
|
522
|
+
)
|
|
523
|
+
]
|
|
524
|
+
}
|
|
525
|
+
),
|
|
526
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
527
|
+
"div",
|
|
528
|
+
{
|
|
529
|
+
ref: scrollRef,
|
|
530
|
+
"data-testid": "terminal-output",
|
|
531
|
+
className: outputClass,
|
|
532
|
+
children: lines.map((line) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles[line.type], children: line.text.split("\n").map((segment, i) => /* @__PURE__ */ jsxRuntime.jsx("div", { children: segment || " " }, i)) }, line.id))
|
|
533
|
+
}
|
|
534
|
+
),
|
|
535
|
+
hasInput && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 border-t border-zinc-800", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-center px-3 ${inputPy} ${inputGap}`, children: [
|
|
536
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
537
|
+
"span",
|
|
538
|
+
{
|
|
539
|
+
className: `${termTheme.promptColor} font-mono ${promptSize} font-bold select-none`,
|
|
540
|
+
children: "$"
|
|
541
|
+
}
|
|
542
|
+
),
|
|
543
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
544
|
+
"input",
|
|
545
|
+
{
|
|
546
|
+
ref: inputRef,
|
|
547
|
+
type: "text",
|
|
548
|
+
value: input,
|
|
549
|
+
onChange: (e) => setInput(e.target.value),
|
|
550
|
+
onKeyDown: handleKeyDown,
|
|
551
|
+
disabled: isRunning,
|
|
552
|
+
placeholder: isRunning ? "Running..." : "Type a command...",
|
|
553
|
+
className: `
|
|
554
|
+
flex-1 bg-transparent text-zinc-200 ${inputFontSize} font-mono
|
|
555
|
+
outline-none placeholder:text-zinc-700
|
|
556
|
+
disabled:opacity-50
|
|
557
|
+
${termTheme.inputCaret}
|
|
558
|
+
`
|
|
559
|
+
}
|
|
560
|
+
),
|
|
561
|
+
isRunning && /* @__PURE__ */ jsxRuntime.jsx(
|
|
562
|
+
"div",
|
|
563
|
+
{
|
|
564
|
+
className: `${spinnerSize} animate-spin rounded-full border border-zinc-600 ${termTheme.spinnerBorder}`
|
|
565
|
+
}
|
|
566
|
+
)
|
|
567
|
+
] }) })
|
|
568
|
+
] });
|
|
569
|
+
}
|
|
570
|
+
function BrowserWindow({
|
|
571
|
+
htmlSrc,
|
|
572
|
+
url,
|
|
573
|
+
canGoBack,
|
|
574
|
+
canGoForward,
|
|
575
|
+
onBack,
|
|
576
|
+
onForward,
|
|
577
|
+
onRefresh,
|
|
578
|
+
onNavigate,
|
|
579
|
+
port = 3e3,
|
|
580
|
+
title = "App Preview",
|
|
581
|
+
previewUrl,
|
|
582
|
+
onInspectElement
|
|
583
|
+
}) {
|
|
584
|
+
const iframeRef = react.useRef(null);
|
|
585
|
+
const [docTitle, setDocTitle] = react.useState(null);
|
|
586
|
+
const [isFocused, setIsFocused] = react.useState(false);
|
|
587
|
+
const [inspecting, setInspecting] = react.useState(false);
|
|
588
|
+
const inspectCleanupRef = react.useRef(null);
|
|
589
|
+
const displayUrl = previewUrl ? `${previewUrl}${url === "/" ? "" : url}` : `http://localhost:${port}${url === "/" ? "" : url}`;
|
|
590
|
+
const lastSrcdocRef = react.useRef("");
|
|
591
|
+
react.useEffect(() => {
|
|
592
|
+
const iframe = iframeRef.current;
|
|
593
|
+
if (!iframe) return;
|
|
594
|
+
if (previewUrl) {
|
|
595
|
+
iframe.src = `${previewUrl}${url === "/" ? "/" : url}`;
|
|
596
|
+
} else if (htmlSrc !== lastSrcdocRef.current) {
|
|
597
|
+
lastSrcdocRef.current = htmlSrc;
|
|
598
|
+
iframe.srcdoc = htmlSrc;
|
|
599
|
+
}
|
|
600
|
+
}, [htmlSrc, previewUrl]);
|
|
601
|
+
react.useEffect(() => {
|
|
602
|
+
const iframe = iframeRef.current;
|
|
603
|
+
if (!iframe || previewUrl) return;
|
|
604
|
+
try {
|
|
605
|
+
iframe.contentWindow?.postMessage({ type: "navigate", path: url }, "*");
|
|
606
|
+
} catch {
|
|
607
|
+
}
|
|
608
|
+
}, [url, previewUrl]);
|
|
609
|
+
react.useEffect(() => {
|
|
610
|
+
const iframe = iframeRef.current;
|
|
611
|
+
if (!iframe) return;
|
|
612
|
+
let observer;
|
|
613
|
+
const observe = () => {
|
|
614
|
+
try {
|
|
615
|
+
const doc = iframe.contentDocument;
|
|
616
|
+
if (!doc) return;
|
|
617
|
+
const readTitle = () => {
|
|
618
|
+
const t = doc.title;
|
|
619
|
+
if (t) setDocTitle(t);
|
|
620
|
+
};
|
|
621
|
+
readTitle();
|
|
622
|
+
const head = doc.head ?? doc.documentElement;
|
|
623
|
+
if (!head) return;
|
|
624
|
+
observer = new MutationObserver(readTitle);
|
|
625
|
+
observer.observe(head, {
|
|
626
|
+
childList: true,
|
|
627
|
+
subtree: true,
|
|
628
|
+
characterData: true
|
|
629
|
+
});
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
iframe.addEventListener("load", observe);
|
|
634
|
+
return () => {
|
|
635
|
+
iframe.removeEventListener("load", observe);
|
|
636
|
+
observer?.disconnect();
|
|
637
|
+
};
|
|
638
|
+
}, [htmlSrc, previewUrl]);
|
|
639
|
+
react.useEffect(() => {
|
|
640
|
+
if (inspecting) {
|
|
641
|
+
inspectCleanupRef.current?.();
|
|
642
|
+
inspectCleanupRef.current = null;
|
|
643
|
+
const timer = setTimeout(() => {
|
|
644
|
+
if (inspecting) injectInspectMode();
|
|
645
|
+
}, 100);
|
|
646
|
+
return () => clearTimeout(timer);
|
|
647
|
+
}
|
|
648
|
+
}, [htmlSrc]);
|
|
649
|
+
const injectInspectMode = react.useCallback(() => {
|
|
650
|
+
const iframe = iframeRef.current;
|
|
651
|
+
if (!iframe) return;
|
|
652
|
+
try {
|
|
653
|
+
const doc = iframe.contentDocument;
|
|
654
|
+
if (!doc) return;
|
|
655
|
+
let currentHighlight = null;
|
|
656
|
+
const OUTLINE = "2px solid #3b82f6";
|
|
657
|
+
const OUTLINE_OFFSET = "-2px";
|
|
658
|
+
const onMouseOver = (e) => {
|
|
659
|
+
const target = e.target;
|
|
660
|
+
if (target === doc.body || target === doc.documentElement) return;
|
|
661
|
+
if (currentHighlight) {
|
|
662
|
+
currentHighlight.style.outline = "";
|
|
663
|
+
currentHighlight.style.outlineOffset = "";
|
|
664
|
+
}
|
|
665
|
+
target.style.outline = OUTLINE;
|
|
666
|
+
target.style.outlineOffset = OUTLINE_OFFSET;
|
|
667
|
+
currentHighlight = target;
|
|
668
|
+
};
|
|
669
|
+
const onMouseOut = (e) => {
|
|
670
|
+
const target = e.target;
|
|
671
|
+
target.style.outline = "";
|
|
672
|
+
target.style.outlineOffset = "";
|
|
673
|
+
if (currentHighlight === target) currentHighlight = null;
|
|
674
|
+
};
|
|
675
|
+
const onClick = (e) => {
|
|
676
|
+
e.preventDefault();
|
|
677
|
+
e.stopPropagation();
|
|
678
|
+
const target = e.target;
|
|
679
|
+
target.style.outline = "";
|
|
680
|
+
target.style.outlineOffset = "";
|
|
681
|
+
const text = (target.textContent || "").trim().slice(0, 200);
|
|
682
|
+
const html = target.outerHTML.slice(0, 500);
|
|
683
|
+
onInspectElement?.({
|
|
684
|
+
tagName: target.tagName.toLowerCase(),
|
|
685
|
+
className: target.className || "",
|
|
686
|
+
textContent: text,
|
|
687
|
+
outerHTML: html
|
|
688
|
+
});
|
|
689
|
+
cleanup();
|
|
690
|
+
setInspecting(false);
|
|
691
|
+
};
|
|
692
|
+
doc.addEventListener("mouseover", onMouseOver, true);
|
|
693
|
+
doc.addEventListener("mouseout", onMouseOut, true);
|
|
694
|
+
doc.addEventListener("click", onClick, true);
|
|
695
|
+
doc.body.style.cursor = "crosshair";
|
|
696
|
+
const cleanup = () => {
|
|
697
|
+
doc.removeEventListener("mouseover", onMouseOver, true);
|
|
698
|
+
doc.removeEventListener("mouseout", onMouseOut, true);
|
|
699
|
+
doc.removeEventListener("click", onClick, true);
|
|
700
|
+
doc.body.style.cursor = "";
|
|
701
|
+
if (currentHighlight) {
|
|
702
|
+
currentHighlight.style.outline = "";
|
|
703
|
+
currentHighlight.style.outlineOffset = "";
|
|
704
|
+
currentHighlight = null;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
inspectCleanupRef.current = cleanup;
|
|
708
|
+
} catch {
|
|
709
|
+
}
|
|
710
|
+
}, [onInspectElement]);
|
|
711
|
+
const toggleInspect = react.useCallback(() => {
|
|
712
|
+
if (inspecting) {
|
|
713
|
+
inspectCleanupRef.current?.();
|
|
714
|
+
inspectCleanupRef.current = null;
|
|
715
|
+
setInspecting(false);
|
|
716
|
+
} else {
|
|
717
|
+
setInspecting(true);
|
|
718
|
+
requestAnimationFrame(() => injectInspectMode());
|
|
719
|
+
}
|
|
720
|
+
}, [inspecting, injectInspectMode]);
|
|
721
|
+
const handleAddressKeyDown = react.useCallback(
|
|
722
|
+
(e) => {
|
|
723
|
+
if (e.key === "Enter") {
|
|
724
|
+
const val = e.currentTarget.value.trim();
|
|
725
|
+
try {
|
|
726
|
+
const parsed = new URL(
|
|
727
|
+
val.startsWith("http") ? val : `http://localhost:${port}${val}`
|
|
728
|
+
);
|
|
729
|
+
onNavigate(parsed.pathname || "/");
|
|
730
|
+
} catch {
|
|
731
|
+
onNavigate(val.startsWith("/") ? val : `/${val}`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
[onNavigate, port]
|
|
736
|
+
);
|
|
737
|
+
const tabLabel = docTitle || title;
|
|
738
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-full flex flex-col rounded-xl overflow-hidden border border-zinc-800 bg-zinc-950 shadow-[0_0_0_1px_rgba(255,255,255,0.03),0_8px_40px_-12px_rgba(0,0,0,0.6)]", children: [
|
|
739
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-shrink-0 flex items-center gap-2 h-10 px-3 bg-zinc-900/80 border-b border-zinc-800/80 select-none", children: [
|
|
740
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-[6px] mr-1", children: [
|
|
741
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "block h-[10px] w-[10px] rounded-full bg-[#ff5f57] ring-1 ring-black/10" }),
|
|
742
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "block h-[10px] w-[10px] rounded-full bg-[#febc2e] ring-1 ring-black/10" }),
|
|
743
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "block h-[10px] w-[10px] rounded-full bg-[#28c840] ring-1 ring-black/10" })
|
|
744
|
+
] }),
|
|
745
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center", children: [
|
|
746
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
747
|
+
"button",
|
|
748
|
+
{
|
|
749
|
+
onClick: onBack,
|
|
750
|
+
disabled: !canGoBack,
|
|
751
|
+
className: "p-1 rounded-md text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800 disabled:text-zinc-700 disabled:hover:bg-transparent transition-colors",
|
|
752
|
+
title: "Back",
|
|
753
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
754
|
+
"svg",
|
|
755
|
+
{
|
|
756
|
+
className: "w-3.5 h-3.5",
|
|
757
|
+
viewBox: "0 0 16 16",
|
|
758
|
+
fill: "currentColor",
|
|
759
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10.354 3.354a.5.5 0 0 0-.708-.708l-5 5a.5.5 0 0 0 0 .708l5 5a.5.5 0 0 0 .708-.708L5.707 8l4.647-4.646z" })
|
|
760
|
+
}
|
|
761
|
+
)
|
|
762
|
+
}
|
|
763
|
+
),
|
|
764
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
765
|
+
"button",
|
|
766
|
+
{
|
|
767
|
+
onClick: onForward,
|
|
768
|
+
disabled: !canGoForward,
|
|
769
|
+
className: "p-1 rounded-md text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800 disabled:text-zinc-700 disabled:hover:bg-transparent transition-colors",
|
|
770
|
+
title: "Forward",
|
|
771
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
772
|
+
"svg",
|
|
773
|
+
{
|
|
774
|
+
className: "w-3.5 h-3.5",
|
|
775
|
+
viewBox: "0 0 16 16",
|
|
776
|
+
fill: "currentColor",
|
|
777
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5.646 3.354a.5.5 0 0 1 .708-.708l5 5a.5.5 0 0 1 0 .708l-5 5a.5.5 0 0 1-.708-.708L10.293 8 5.646 3.354z" })
|
|
778
|
+
}
|
|
779
|
+
)
|
|
780
|
+
}
|
|
781
|
+
),
|
|
782
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
783
|
+
"button",
|
|
784
|
+
{
|
|
785
|
+
onClick: onRefresh,
|
|
786
|
+
className: "p-1 rounded-md text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800 transition-colors",
|
|
787
|
+
title: "Refresh",
|
|
788
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
789
|
+
"svg",
|
|
790
|
+
{
|
|
791
|
+
className: "w-3.5 h-3.5",
|
|
792
|
+
viewBox: "0 0 16 16",
|
|
793
|
+
fill: "none",
|
|
794
|
+
stroke: "currentColor",
|
|
795
|
+
strokeWidth: 1.5,
|
|
796
|
+
strokeLinecap: "round",
|
|
797
|
+
strokeLinejoin: "round",
|
|
798
|
+
children: [
|
|
799
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2.5 8a5.5 5.5 0 0 1 9.22-4.05M13.5 8a5.5 5.5 0 0 1-9.22 4.05" }),
|
|
800
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M13.5 2.5v3h-3M2.5 13.5v-3h3" })
|
|
801
|
+
]
|
|
802
|
+
}
|
|
803
|
+
)
|
|
804
|
+
}
|
|
805
|
+
),
|
|
806
|
+
onInspectElement && /* @__PURE__ */ jsxRuntime.jsx(
|
|
807
|
+
"button",
|
|
808
|
+
{
|
|
809
|
+
onClick: toggleInspect,
|
|
810
|
+
className: `p-1 rounded-md transition-colors ${inspecting ? "text-blue-400 bg-blue-500/15" : "text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800"}`,
|
|
811
|
+
title: inspecting ? "Cancel inspect" : "Inspect element",
|
|
812
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
813
|
+
"svg",
|
|
814
|
+
{
|
|
815
|
+
className: "w-3.5 h-3.5",
|
|
816
|
+
viewBox: "0 0 24 24",
|
|
817
|
+
fill: "none",
|
|
818
|
+
stroke: "currentColor",
|
|
819
|
+
strokeWidth: 2,
|
|
820
|
+
strokeLinecap: "round",
|
|
821
|
+
strokeLinejoin: "round",
|
|
822
|
+
children: [
|
|
823
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 12l3 0" }),
|
|
824
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 3l0 3" }),
|
|
825
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7.8 7.8l-2.2 -2.2" }),
|
|
826
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16.2 7.8l2.2 -2.2" }),
|
|
827
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7.8 16.2l-2.2 2.2" }),
|
|
828
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 12l9 3l-4 2l-2 4l-3 -9" })
|
|
829
|
+
]
|
|
830
|
+
}
|
|
831
|
+
)
|
|
832
|
+
}
|
|
833
|
+
)
|
|
834
|
+
] }),
|
|
835
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
836
|
+
"div",
|
|
837
|
+
{
|
|
838
|
+
className: `flex-1 flex items-center gap-2 h-[26px] rounded-md px-2.5 transition-colors ${isFocused ? "bg-zinc-950 ring-1 ring-zinc-600" : "bg-zinc-800/60 hover:bg-zinc-800"}`,
|
|
839
|
+
children: [
|
|
840
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
841
|
+
"svg",
|
|
842
|
+
{
|
|
843
|
+
className: "w-3 h-3 flex-shrink-0 text-zinc-500",
|
|
844
|
+
viewBox: "0 0 16 16",
|
|
845
|
+
fill: "currentColor",
|
|
846
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
847
|
+
"path",
|
|
848
|
+
{
|
|
849
|
+
fillRule: "evenodd",
|
|
850
|
+
d: "M8 1a4.5 4.5 0 0 0-4.5 4.5V7H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1h-.5V5.5A4.5 4.5 0 0 0 8 1zm2.5 6V5.5a2.5 2.5 0 0 0-5 0V7h5z"
|
|
851
|
+
}
|
|
852
|
+
)
|
|
853
|
+
}
|
|
854
|
+
),
|
|
855
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
856
|
+
"input",
|
|
857
|
+
{
|
|
858
|
+
type: "text",
|
|
859
|
+
defaultValue: displayUrl,
|
|
860
|
+
onKeyDown: handleAddressKeyDown,
|
|
861
|
+
onFocus: () => setIsFocused(true),
|
|
862
|
+
onBlur: () => setIsFocused(false),
|
|
863
|
+
className: "flex-1 bg-transparent text-[11px] text-zinc-400 outline-none font-mono leading-none placeholder:text-zinc-600",
|
|
864
|
+
spellCheck: false
|
|
865
|
+
},
|
|
866
|
+
displayUrl
|
|
867
|
+
),
|
|
868
|
+
tabLabel && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:block text-[10px] text-zinc-600 truncate max-w-[120px] leading-none", children: tabLabel })
|
|
869
|
+
]
|
|
870
|
+
}
|
|
871
|
+
)
|
|
872
|
+
] }),
|
|
873
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 bg-white relative", children: [
|
|
874
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
875
|
+
"iframe",
|
|
876
|
+
{
|
|
877
|
+
ref: iframeRef,
|
|
878
|
+
title: docTitle || title,
|
|
879
|
+
sandbox: "allow-scripts allow-forms allow-popups allow-same-origin",
|
|
880
|
+
className: "absolute inset-0 w-full h-full border-0"
|
|
881
|
+
}
|
|
882
|
+
),
|
|
883
|
+
inspecting && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute top-2 left-1/2 -translate-x-1/2 z-10 px-3 py-1 rounded-full bg-blue-500/90 text-white text-[10px] font-mono shadow-lg pointer-events-none", children: "Click an element to inspect" })
|
|
884
|
+
] })
|
|
885
|
+
] });
|
|
886
|
+
}
|
|
887
|
+
const FILE_COLOR_MAP = {
|
|
888
|
+
js: "text-yellow-400",
|
|
889
|
+
mjs: "text-yellow-400",
|
|
890
|
+
cjs: "text-yellow-400",
|
|
891
|
+
jsx: "text-cyan-400",
|
|
892
|
+
ts: "text-blue-400",
|
|
893
|
+
tsx: "text-cyan-400",
|
|
894
|
+
json: "text-amber-300",
|
|
895
|
+
html: "text-orange-400",
|
|
896
|
+
css: "text-pink-400",
|
|
897
|
+
md: "text-blue-400",
|
|
898
|
+
vue: "text-emerald-400",
|
|
899
|
+
svelte: "text-orange-400",
|
|
900
|
+
astro: "text-orange-400",
|
|
901
|
+
yaml: "text-blue-400",
|
|
902
|
+
yml: "text-blue-400",
|
|
903
|
+
toml: "text-blue-400",
|
|
904
|
+
sh: "text-green-400",
|
|
905
|
+
svg: "text-emerald-400"
|
|
906
|
+
};
|
|
907
|
+
function FileIcon({
|
|
908
|
+
isDir,
|
|
909
|
+
name,
|
|
910
|
+
compact
|
|
911
|
+
}) {
|
|
912
|
+
const iconSize = compact ? "text-[11px] w-3.5" : "text-xs w-4";
|
|
913
|
+
if (isDir) {
|
|
914
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
915
|
+
"span",
|
|
916
|
+
{
|
|
917
|
+
className: `text-amber-400 ${iconSize} inline-block text-center mr-1.5`,
|
|
918
|
+
children: "📁"
|
|
919
|
+
}
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
const ext = name.split(".").pop()?.toLowerCase();
|
|
923
|
+
const color = FILE_COLOR_MAP[ext || ""] || "text-zinc-400";
|
|
924
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${color} ${iconSize} inline-block text-center mr-1.5`, children: "📄" });
|
|
925
|
+
}
|
|
926
|
+
function TreeNode({
|
|
927
|
+
entry,
|
|
928
|
+
depth,
|
|
929
|
+
selectedFile,
|
|
930
|
+
onSelect,
|
|
931
|
+
theme,
|
|
932
|
+
compact
|
|
933
|
+
}) {
|
|
934
|
+
const [expanded, setExpanded] = react.useState(true);
|
|
935
|
+
const isSelected = entry.path === selectedFile;
|
|
936
|
+
const py = compact ? "py-[3px]" : "py-0.5";
|
|
937
|
+
const px = compact ? "px-1.5" : "px-2";
|
|
938
|
+
const fontSize = compact ? "text-[12px]" : "text-[13px]";
|
|
939
|
+
const arrowSize = compact ? "text-[9px] w-2.5" : "text-[10px] w-3";
|
|
940
|
+
const indent = compact ? depth * 10 + 6 : depth * 12 + 8;
|
|
941
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
942
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
943
|
+
"button",
|
|
944
|
+
{
|
|
945
|
+
onClick: () => entry.isDir ? setExpanded(!expanded) : onSelect(entry.path),
|
|
946
|
+
className: `
|
|
947
|
+
w-full text-left flex items-center ${py} ${px} rounded ${fontSize} font-mono
|
|
948
|
+
transition-colors duration-75
|
|
949
|
+
${isSelected ? theme.selected : "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200"}
|
|
950
|
+
`,
|
|
951
|
+
style: { paddingLeft: `${indent}px` },
|
|
952
|
+
children: [
|
|
953
|
+
entry.isDir && /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${arrowSize} mr-1 text-zinc-500 inline-block`, children: expanded ? "▾" : "▸" }),
|
|
954
|
+
!entry.isDir && /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${arrowSize} inline-block` }),
|
|
955
|
+
/* @__PURE__ */ jsxRuntime.jsx(FileIcon, { isDir: entry.isDir, name: entry.name, compact }),
|
|
956
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: entry.name })
|
|
957
|
+
]
|
|
958
|
+
}
|
|
959
|
+
),
|
|
960
|
+
entry.isDir && expanded && entry.children?.map((child) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
961
|
+
TreeNode,
|
|
962
|
+
{
|
|
963
|
+
entry: child,
|
|
964
|
+
depth: depth + 1,
|
|
965
|
+
selectedFile,
|
|
966
|
+
onSelect,
|
|
967
|
+
theme,
|
|
968
|
+
compact
|
|
969
|
+
},
|
|
970
|
+
child.path
|
|
971
|
+
))
|
|
972
|
+
] });
|
|
973
|
+
}
|
|
974
|
+
function FileExplorer({
|
|
975
|
+
files,
|
|
976
|
+
selectedFile,
|
|
977
|
+
onSelect,
|
|
978
|
+
accentColor = "emerald",
|
|
979
|
+
variant = "default"
|
|
980
|
+
}) {
|
|
981
|
+
const theme = getFileExplorerTheme(accentColor);
|
|
982
|
+
const compact = variant === "compact";
|
|
983
|
+
if (files.length === 0) {
|
|
984
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
985
|
+
"div",
|
|
986
|
+
{
|
|
987
|
+
className: `p-2 ${compact ? "text-[11px]" : "text-xs"} text-zinc-600 italic`,
|
|
988
|
+
children: "No files"
|
|
989
|
+
}
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: compact ? "py-0.5" : "py-1", children: files.map((entry) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
993
|
+
TreeNode,
|
|
994
|
+
{
|
|
995
|
+
entry,
|
|
996
|
+
depth: 0,
|
|
997
|
+
selectedFile,
|
|
998
|
+
onSelect,
|
|
999
|
+
theme,
|
|
1000
|
+
compact
|
|
1001
|
+
},
|
|
1002
|
+
entry.path
|
|
1003
|
+
)) });
|
|
1004
|
+
}
|
|
1005
|
+
const ACCENT_LABELS = {
|
|
1006
|
+
emerald: "text-emerald-500/70",
|
|
1007
|
+
violet: "text-violet-500/70",
|
|
1008
|
+
orange: "text-orange-500/70",
|
|
1009
|
+
blue: "text-blue-500/70",
|
|
1010
|
+
pink: "text-pink-500/70",
|
|
1011
|
+
green: "text-green-500/70",
|
|
1012
|
+
amber: "text-amber-500/70"
|
|
1013
|
+
};
|
|
1014
|
+
const ACCENT_CHEVRONS = {
|
|
1015
|
+
emerald: "text-emerald-400",
|
|
1016
|
+
violet: "text-violet-400",
|
|
1017
|
+
orange: "text-orange-400",
|
|
1018
|
+
blue: "text-blue-400",
|
|
1019
|
+
pink: "text-pink-400",
|
|
1020
|
+
green: "text-green-400",
|
|
1021
|
+
amber: "text-amber-400"
|
|
1022
|
+
};
|
|
1023
|
+
const ACCENT_HOVER_BG = {
|
|
1024
|
+
emerald: "hover:bg-emerald-500/5",
|
|
1025
|
+
violet: "hover:bg-violet-500/5",
|
|
1026
|
+
orange: "hover:bg-orange-500/5",
|
|
1027
|
+
blue: "hover:bg-blue-500/5",
|
|
1028
|
+
pink: "hover:bg-pink-500/5",
|
|
1029
|
+
green: "hover:bg-green-500/5",
|
|
1030
|
+
amber: "hover:bg-amber-500/5"
|
|
1031
|
+
};
|
|
1032
|
+
function PanelToggle({
|
|
1033
|
+
collapsed,
|
|
1034
|
+
onClick,
|
|
1035
|
+
label,
|
|
1036
|
+
side = "left",
|
|
1037
|
+
accentColor = "emerald"
|
|
1038
|
+
}) {
|
|
1039
|
+
const chevronRight = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1040
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { stroke: "none", d: "M0 0h24v24H0z", fill: "none" }),
|
|
1041
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 6l6 6l-6 6" })
|
|
1042
|
+
] });
|
|
1043
|
+
const chevronLeft = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1044
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { stroke: "none", d: "M0 0h24v24H0z" }),
|
|
1045
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m15 6-6 6 6 6" })
|
|
1046
|
+
] });
|
|
1047
|
+
if (collapsed) {
|
|
1048
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1049
|
+
"button",
|
|
1050
|
+
{
|
|
1051
|
+
onClick,
|
|
1052
|
+
className: "flex-shrink-0 w-7 flex flex-col items-center gap-2 pt-3 pb-3\n border-r border-zinc-800 bg-zinc-900/40\n hover:bg-zinc-800/60 transition-colors cursor-pointer",
|
|
1053
|
+
title: `Show ${label}`,
|
|
1054
|
+
children: [
|
|
1055
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1056
|
+
"svg",
|
|
1057
|
+
{
|
|
1058
|
+
className: `w-3 h-3 ${ACCENT_CHEVRONS[accentColor]}`,
|
|
1059
|
+
viewBox: "0 0 24 24",
|
|
1060
|
+
stroke: "currentColor",
|
|
1061
|
+
fill: "none",
|
|
1062
|
+
children: side === "left" ? chevronRight : chevronLeft
|
|
1063
|
+
}
|
|
1064
|
+
),
|
|
1065
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1066
|
+
"span",
|
|
1067
|
+
{
|
|
1068
|
+
className: `text-[10px] font-mono tracking-wider uppercase select-none ${ACCENT_LABELS[accentColor]}`,
|
|
1069
|
+
style: { writingMode: "vertical-rl", transform: "rotate(180deg)" },
|
|
1070
|
+
children: label
|
|
1071
|
+
}
|
|
1072
|
+
)
|
|
1073
|
+
]
|
|
1074
|
+
}
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1078
|
+
"button",
|
|
1079
|
+
{
|
|
1080
|
+
onClick,
|
|
1081
|
+
className: `flex-shrink-0 w-[9px] flex items-center justify-center
|
|
1082
|
+
border-r border-zinc-800 bg-transparent
|
|
1083
|
+
${ACCENT_HOVER_BG[accentColor]} transition-colors cursor-pointer group`,
|
|
1084
|
+
title: `Hide ${label}`,
|
|
1085
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1086
|
+
"svg",
|
|
1087
|
+
{
|
|
1088
|
+
className: `w-2.5 h-2.5 text-zinc-600 opacity-0 group-hover:opacity-100
|
|
1089
|
+
transition-opacity ${ACCENT_CHEVRONS[accentColor]}`,
|
|
1090
|
+
viewBox: "0 0 16 16",
|
|
1091
|
+
fill: "currentColor",
|
|
1092
|
+
children: side === "left" ? chevronLeft : chevronRight
|
|
1093
|
+
}
|
|
1094
|
+
)
|
|
1095
|
+
}
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
const ACTIVE_BG = {
|
|
1099
|
+
emerald: "bg-emerald-500/15",
|
|
1100
|
+
violet: "bg-violet-500/15",
|
|
1101
|
+
orange: "bg-orange-500/15",
|
|
1102
|
+
blue: "bg-blue-500/15",
|
|
1103
|
+
pink: "bg-pink-500/15",
|
|
1104
|
+
green: "bg-green-500/15",
|
|
1105
|
+
amber: "bg-amber-500/15"
|
|
1106
|
+
};
|
|
1107
|
+
const ACTIVE_TEXT = {
|
|
1108
|
+
emerald: "text-emerald-300",
|
|
1109
|
+
violet: "text-violet-300",
|
|
1110
|
+
orange: "text-orange-300",
|
|
1111
|
+
blue: "text-blue-300",
|
|
1112
|
+
pink: "text-pink-300",
|
|
1113
|
+
green: "text-green-300",
|
|
1114
|
+
amber: "text-amber-300"
|
|
1115
|
+
};
|
|
1116
|
+
function MobileTabBar({
|
|
1117
|
+
tabs,
|
|
1118
|
+
activeTab,
|
|
1119
|
+
onTabChange,
|
|
1120
|
+
accentColor = "emerald"
|
|
1121
|
+
}) {
|
|
1122
|
+
const containerRef = react.useRef(null);
|
|
1123
|
+
const [indicator, setIndicator] = react.useState({ left: 0, width: 0 });
|
|
1124
|
+
react.useEffect(() => {
|
|
1125
|
+
if (!containerRef.current) return;
|
|
1126
|
+
const activeIndex = tabs.findIndex((t) => t.id === activeTab);
|
|
1127
|
+
if (activeIndex < 0) return;
|
|
1128
|
+
const buttons = containerRef.current.querySelectorAll(
|
|
1129
|
+
"[data-tab-id]"
|
|
1130
|
+
);
|
|
1131
|
+
const btn = buttons[activeIndex];
|
|
1132
|
+
if (!btn) return;
|
|
1133
|
+
setIndicator({
|
|
1134
|
+
left: btn.offsetLeft,
|
|
1135
|
+
width: btn.offsetWidth
|
|
1136
|
+
});
|
|
1137
|
+
}, [activeTab, tabs]);
|
|
1138
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 px-3 py-1.5 border-b border-zinc-800 bg-zinc-950", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1139
|
+
"div",
|
|
1140
|
+
{
|
|
1141
|
+
ref: containerRef,
|
|
1142
|
+
className: "relative flex rounded-lg bg-zinc-900 border border-zinc-800 p-0.5",
|
|
1143
|
+
children: [
|
|
1144
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1145
|
+
"div",
|
|
1146
|
+
{
|
|
1147
|
+
className: `absolute top-0.5 bottom-0.5 rounded-md ${ACTIVE_BG[accentColor]} transition-all duration-200 ease-out`,
|
|
1148
|
+
style: { left: indicator.left, width: indicator.width }
|
|
1149
|
+
}
|
|
1150
|
+
),
|
|
1151
|
+
tabs.map((tab) => {
|
|
1152
|
+
const isActive = tab.id === activeTab;
|
|
1153
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1154
|
+
"button",
|
|
1155
|
+
{
|
|
1156
|
+
"data-tab-id": tab.id,
|
|
1157
|
+
onClick: () => onTabChange(tab.id),
|
|
1158
|
+
className: `relative z-10 flex-1 flex items-center justify-center gap-1.5
|
|
1159
|
+
rounded-md py-1.5 px-3 font-mono text-[11px] tracking-wide
|
|
1160
|
+
transition-colors duration-150
|
|
1161
|
+
${isActive ? ACTIVE_TEXT[accentColor] : "text-zinc-500 hover:text-zinc-400"}`,
|
|
1162
|
+
children: [
|
|
1163
|
+
tab.icon,
|
|
1164
|
+
tab.label
|
|
1165
|
+
]
|
|
1166
|
+
},
|
|
1167
|
+
tab.id
|
|
1168
|
+
);
|
|
1169
|
+
})
|
|
1170
|
+
]
|
|
1171
|
+
}
|
|
1172
|
+
) });
|
|
1173
|
+
}
|
|
1174
|
+
function ChatIcon({ className = "w-3.5 h-3.5" }) {
|
|
1175
|
+
return /* @__PURE__ */ jsxRuntime.jsx("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" }) });
|
|
1176
|
+
}
|
|
1177
|
+
function PreviewIcon({ className = "w-3.5 h-3.5" }) {
|
|
1178
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
1179
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" }),
|
|
1180
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" })
|
|
1181
|
+
] });
|
|
1182
|
+
}
|
|
1183
|
+
function CodeIcon({ className = "w-3.5 h-3.5" }) {
|
|
1184
|
+
return /* @__PURE__ */ jsxRuntime.jsx("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" }) });
|
|
1185
|
+
}
|
|
1186
|
+
function TerminalIcon({ className = "w-3.5 h-3.5" }) {
|
|
1187
|
+
return /* @__PURE__ */ jsxRuntime.jsx("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" }) });
|
|
1188
|
+
}
|
|
1189
|
+
function FilesIcon({ className = "w-3.5 h-3.5" }) {
|
|
1190
|
+
return /* @__PURE__ */ jsxRuntime.jsx("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" }) });
|
|
1191
|
+
}
|
|
1192
|
+
function useMediaQuery(query) {
|
|
1193
|
+
const [matches, setMatches] = react.useState(() => {
|
|
1194
|
+
if (typeof window === "undefined") return false;
|
|
1195
|
+
return window.matchMedia(query).matches;
|
|
1196
|
+
});
|
|
1197
|
+
react.useEffect(() => {
|
|
1198
|
+
const mql = window.matchMedia(query);
|
|
1199
|
+
const handler = (e) => setMatches(e.matches);
|
|
1200
|
+
mql.addEventListener("change", handler);
|
|
1201
|
+
return () => mql.removeEventListener("change", handler);
|
|
1202
|
+
}, [query]);
|
|
1203
|
+
return matches;
|
|
1204
|
+
}
|
|
1205
|
+
const CHAT_THEMES = {
|
|
1206
|
+
emerald: {
|
|
1207
|
+
userBubble: "bg-emerald-500/15 border border-emerald-500/20",
|
|
1208
|
+
assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
|
|
1209
|
+
sendButton: "bg-emerald-500 hover:bg-emerald-600 text-white",
|
|
1210
|
+
sendButtonDisabled: "bg-zinc-700 text-zinc-500",
|
|
1211
|
+
streamingDot: "bg-emerald-400",
|
|
1212
|
+
inputCaret: "caret-emerald-400",
|
|
1213
|
+
modelBadge: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20"
|
|
1214
|
+
},
|
|
1215
|
+
violet: {
|
|
1216
|
+
userBubble: "bg-violet-500/15 border border-violet-500/20",
|
|
1217
|
+
assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
|
|
1218
|
+
sendButton: "bg-violet-500 hover:bg-violet-600 text-white",
|
|
1219
|
+
sendButtonDisabled: "bg-zinc-700 text-zinc-500",
|
|
1220
|
+
streamingDot: "bg-violet-400",
|
|
1221
|
+
inputCaret: "caret-violet-400",
|
|
1222
|
+
modelBadge: "bg-violet-500/10 text-violet-400 border-violet-500/20"
|
|
1223
|
+
},
|
|
1224
|
+
orange: {
|
|
1225
|
+
userBubble: "bg-orange-500/15 border border-orange-500/20",
|
|
1226
|
+
assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
|
|
1227
|
+
sendButton: "bg-orange-500 hover:bg-orange-600 text-white",
|
|
1228
|
+
sendButtonDisabled: "bg-zinc-700 text-zinc-500",
|
|
1229
|
+
streamingDot: "bg-orange-400",
|
|
1230
|
+
inputCaret: "caret-orange-400",
|
|
1231
|
+
modelBadge: "bg-orange-500/10 text-orange-400 border-orange-500/20"
|
|
1232
|
+
},
|
|
1233
|
+
blue: {
|
|
1234
|
+
userBubble: "bg-blue-500/15 border border-blue-500/20",
|
|
1235
|
+
assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
|
|
1236
|
+
sendButton: "bg-blue-500 hover:bg-blue-600 text-white",
|
|
1237
|
+
sendButtonDisabled: "bg-zinc-700 text-zinc-500",
|
|
1238
|
+
streamingDot: "bg-blue-400",
|
|
1239
|
+
inputCaret: "caret-blue-400",
|
|
1240
|
+
modelBadge: "bg-blue-500/10 text-blue-400 border-blue-500/20"
|
|
1241
|
+
},
|
|
1242
|
+
pink: {
|
|
1243
|
+
userBubble: "bg-pink-500/15 border border-pink-500/20",
|
|
1244
|
+
assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
|
|
1245
|
+
sendButton: "bg-pink-500 hover:bg-pink-600 text-white",
|
|
1246
|
+
sendButtonDisabled: "bg-zinc-700 text-zinc-500",
|
|
1247
|
+
streamingDot: "bg-pink-400",
|
|
1248
|
+
inputCaret: "caret-pink-400",
|
|
1249
|
+
modelBadge: "bg-pink-500/10 text-pink-400 border-pink-500/20"
|
|
1250
|
+
},
|
|
1251
|
+
green: {
|
|
1252
|
+
userBubble: "bg-green-500/15 border border-green-500/20",
|
|
1253
|
+
assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
|
|
1254
|
+
sendButton: "bg-green-500 hover:bg-green-600 text-white",
|
|
1255
|
+
sendButtonDisabled: "bg-zinc-700 text-zinc-500",
|
|
1256
|
+
streamingDot: "bg-green-400",
|
|
1257
|
+
inputCaret: "caret-green-400",
|
|
1258
|
+
modelBadge: "bg-green-500/10 text-green-400 border-green-500/20"
|
|
1259
|
+
},
|
|
1260
|
+
amber: {
|
|
1261
|
+
userBubble: "bg-amber-500/15 border border-amber-500/20",
|
|
1262
|
+
assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
|
|
1263
|
+
sendButton: "bg-amber-500 hover:bg-amber-600 text-white",
|
|
1264
|
+
sendButtonDisabled: "bg-zinc-700 text-zinc-500",
|
|
1265
|
+
streamingDot: "bg-amber-400",
|
|
1266
|
+
inputCaret: "caret-amber-400",
|
|
1267
|
+
modelBadge: "bg-amber-500/10 text-amber-400 border-amber-500/20"
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
function getChatTheme(color) {
|
|
1271
|
+
return CHAT_THEMES[color];
|
|
1272
|
+
}
|
|
1273
|
+
function CodeBlock({ lang, content }) {
|
|
1274
|
+
const [collapsed, setCollapsed] = react.useState(false);
|
|
1275
|
+
const lineCount = content.split("\n").length;
|
|
1276
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "my-2 rounded-lg bg-zinc-900 border border-zinc-700/50 overflow-hidden", children: [
|
|
1277
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1278
|
+
"div",
|
|
1279
|
+
{
|
|
1280
|
+
className: "flex items-center justify-between px-3 py-1.5 bg-zinc-800/50 cursor-pointer select-none",
|
|
1281
|
+
onClick: () => setCollapsed((c) => !c),
|
|
1282
|
+
children: [
|
|
1283
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1284
|
+
lang && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-zinc-500 font-mono uppercase tracking-wider", children: lang }),
|
|
1285
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[10px] text-zinc-600 font-mono", children: [
|
|
1286
|
+
lineCount,
|
|
1287
|
+
" lines"
|
|
1288
|
+
] })
|
|
1289
|
+
] }),
|
|
1290
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1291
|
+
"svg",
|
|
1292
|
+
{
|
|
1293
|
+
className: `w-3 h-3 text-zinc-500 transition-transform ${collapsed ? "" : "rotate-180"}`,
|
|
1294
|
+
viewBox: "0 0 16 16",
|
|
1295
|
+
fill: "currentColor",
|
|
1296
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4.646 5.646a.5.5 0 0 1 .708 0L8 8.293l2.646-2.647a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 0 1 0-.708z" })
|
|
1297
|
+
}
|
|
1298
|
+
)
|
|
1299
|
+
]
|
|
1300
|
+
}
|
|
1301
|
+
),
|
|
1302
|
+
!collapsed && /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "p-3 overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsx("code", { className: "text-[12px] font-mono text-zinc-300 leading-relaxed whitespace-pre", children: content }) })
|
|
1303
|
+
] });
|
|
1304
|
+
}
|
|
1305
|
+
function renderContent(text) {
|
|
1306
|
+
const parts = [];
|
|
1307
|
+
const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
|
|
1308
|
+
let lastIndex = 0;
|
|
1309
|
+
let match;
|
|
1310
|
+
while ((match = codeBlockRegex.exec(text)) !== null) {
|
|
1311
|
+
if (match.index > lastIndex) {
|
|
1312
|
+
parts.push({ type: "text", content: text.slice(lastIndex, match.index) });
|
|
1313
|
+
}
|
|
1314
|
+
parts.push({
|
|
1315
|
+
type: "code",
|
|
1316
|
+
content: match[2],
|
|
1317
|
+
lang: match[1] || void 0
|
|
1318
|
+
});
|
|
1319
|
+
lastIndex = match.index + match[0].length;
|
|
1320
|
+
}
|
|
1321
|
+
if (lastIndex < text.length) {
|
|
1322
|
+
parts.push({ type: "text", content: text.slice(lastIndex) });
|
|
1323
|
+
}
|
|
1324
|
+
if (parts.length === 0) {
|
|
1325
|
+
parts.push({ type: "text", content: text });
|
|
1326
|
+
}
|
|
1327
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: parts.map((part, i) => {
|
|
1328
|
+
if (part.type === "code") {
|
|
1329
|
+
return /* @__PURE__ */ jsxRuntime.jsx(CodeBlock, { lang: part.lang, content: part.content }, i);
|
|
1330
|
+
}
|
|
1331
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "whitespace-pre-wrap", children: part.content }, i);
|
|
1332
|
+
}) });
|
|
1333
|
+
}
|
|
1334
|
+
function renderUserContent(text) {
|
|
1335
|
+
const parts = text.split(/(@\/[^\s]+)/g);
|
|
1336
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: parts.map(
|
|
1337
|
+
(part, i) => part.startsWith("@/") ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1338
|
+
"span",
|
|
1339
|
+
{
|
|
1340
|
+
className: "inline-flex items-center gap-0.5 px-1 py-0.5 rounded bg-blue-500/20 text-blue-300 font-mono text-[11px]",
|
|
1341
|
+
children: [
|
|
1342
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1343
|
+
"svg",
|
|
1344
|
+
{
|
|
1345
|
+
className: "w-2.5 h-2.5 flex-shrink-0",
|
|
1346
|
+
viewBox: "0 0 16 16",
|
|
1347
|
+
fill: "currentColor",
|
|
1348
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.75 1.5A2.25 2.25 0 0 0 1.5 3.75v8.5A2.25 2.25 0 0 0 3.75 14.5h8.5a2.25 2.25 0 0 0 2.25-2.25V6.621a2.25 2.25 0 0 0-.659-1.591L10.47 1.659A2.25 2.25 0 0 0 8.879 1.5H3.75z" })
|
|
1349
|
+
}
|
|
1350
|
+
),
|
|
1351
|
+
part.slice(1)
|
|
1352
|
+
]
|
|
1353
|
+
},
|
|
1354
|
+
i
|
|
1355
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("span", { children: part }, i)
|
|
1356
|
+
) });
|
|
1357
|
+
}
|
|
1358
|
+
function MessageBubble({
|
|
1359
|
+
msg,
|
|
1360
|
+
theme
|
|
1361
|
+
}) {
|
|
1362
|
+
const [collapsed, setCollapsed] = react.useState(false);
|
|
1363
|
+
const hasCode = msg.role === "assistant" && /```/.test(msg.content);
|
|
1364
|
+
const isLong = msg.content.length > 500;
|
|
1365
|
+
const canCollapse = (hasCode || isLong) && !msg.isStreaming;
|
|
1366
|
+
if (msg.role === "user") {
|
|
1367
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1368
|
+
"div",
|
|
1369
|
+
{
|
|
1370
|
+
className: `max-w-[85%] rounded-xl px-3.5 py-2.5 text-[13px] leading-relaxed ${theme.userBubble} text-zinc-200`,
|
|
1371
|
+
children: renderUserContent(msg.content)
|
|
1372
|
+
}
|
|
1373
|
+
) });
|
|
1374
|
+
}
|
|
1375
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-[13px] leading-relaxed text-zinc-300", children: collapsed ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1376
|
+
"button",
|
|
1377
|
+
{
|
|
1378
|
+
onClick: () => setCollapsed(false),
|
|
1379
|
+
className: "flex items-center gap-1.5 text-left",
|
|
1380
|
+
children: [
|
|
1381
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1382
|
+
"svg",
|
|
1383
|
+
{
|
|
1384
|
+
className: "w-2.5 h-2.5 text-zinc-600 flex-shrink-0",
|
|
1385
|
+
viewBox: "0 0 16 16",
|
|
1386
|
+
fill: "currentColor",
|
|
1387
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6.646 3.646a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L9.293 7 6.646 4.354a.5.5 0 0 1 0-.708z" })
|
|
1388
|
+
}
|
|
1389
|
+
),
|
|
1390
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[11px] text-zinc-600", children: [
|
|
1391
|
+
msg.content.split("\n")[0].replace(/```\w*/, "").trim().slice(0, 60) || "Response",
|
|
1392
|
+
"..."
|
|
1393
|
+
] })
|
|
1394
|
+
]
|
|
1395
|
+
}
|
|
1396
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1397
|
+
renderContent(msg.content),
|
|
1398
|
+
msg.isStreaming && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex items-center gap-1 ml-1", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1399
|
+
"span",
|
|
1400
|
+
{
|
|
1401
|
+
className: `inline-block h-1.5 w-1.5 rounded-full ${theme.streamingDot} animate-pulse`
|
|
1402
|
+
}
|
|
1403
|
+
) }),
|
|
1404
|
+
canCollapse && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1405
|
+
"button",
|
|
1406
|
+
{
|
|
1407
|
+
onClick: () => setCollapsed(true),
|
|
1408
|
+
className: "mt-1 flex items-center gap-1 text-zinc-600 hover:text-zinc-400 transition-colors",
|
|
1409
|
+
children: [
|
|
1410
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1411
|
+
"svg",
|
|
1412
|
+
{
|
|
1413
|
+
className: "w-2.5 h-2.5",
|
|
1414
|
+
viewBox: "0 0 16 16",
|
|
1415
|
+
fill: "currentColor",
|
|
1416
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4.646 5.646a.5.5 0 0 1 .708 0L8 8.293l2.646-2.647a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 0 1 0-.708z" })
|
|
1417
|
+
}
|
|
1418
|
+
),
|
|
1419
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px]", children: "Collapse" })
|
|
1420
|
+
]
|
|
1421
|
+
}
|
|
1422
|
+
)
|
|
1423
|
+
] }) });
|
|
1424
|
+
}
|
|
1425
|
+
function AIChatPanel({
|
|
1426
|
+
messages,
|
|
1427
|
+
onSendMessage,
|
|
1428
|
+
isStreaming = false,
|
|
1429
|
+
accentColor = "blue",
|
|
1430
|
+
title = "Chat",
|
|
1431
|
+
modelLabel,
|
|
1432
|
+
onLoadMore,
|
|
1433
|
+
hasMoreMessages = false,
|
|
1434
|
+
filePaths = [],
|
|
1435
|
+
inspectedElement,
|
|
1436
|
+
onClearInspectedElement
|
|
1437
|
+
}) {
|
|
1438
|
+
const [input, setInput] = react.useState("");
|
|
1439
|
+
const scrollRef = react.useRef(null);
|
|
1440
|
+
const sentinelRef = react.useRef(null);
|
|
1441
|
+
const textareaRef = react.useRef(null);
|
|
1442
|
+
const isAtBottomRef = react.useRef(true);
|
|
1443
|
+
const [mentionQuery, setMentionQuery] = react.useState(null);
|
|
1444
|
+
const [mentionStart, setMentionStart] = react.useState(0);
|
|
1445
|
+
const [mentionIdx, setMentionIdx] = react.useState(0);
|
|
1446
|
+
const theme = getChatTheme(accentColor);
|
|
1447
|
+
const mentionFiltered = react.useMemo(() => {
|
|
1448
|
+
if (mentionQuery === null) return [];
|
|
1449
|
+
const q = mentionQuery.toLowerCase();
|
|
1450
|
+
return filePaths.filter((p) => p.toLowerCase().includes(q)).slice(0, 8);
|
|
1451
|
+
}, [mentionQuery, filePaths]);
|
|
1452
|
+
react.useEffect(() => {
|
|
1453
|
+
setMentionIdx(0);
|
|
1454
|
+
}, [mentionFiltered.length]);
|
|
1455
|
+
react.useEffect(() => {
|
|
1456
|
+
const el = scrollRef.current;
|
|
1457
|
+
if (el && isAtBottomRef.current) {
|
|
1458
|
+
el.scrollTop = el.scrollHeight;
|
|
1459
|
+
}
|
|
1460
|
+
}, [messages]);
|
|
1461
|
+
react.useEffect(() => {
|
|
1462
|
+
const el = scrollRef.current;
|
|
1463
|
+
if (!el) return;
|
|
1464
|
+
const handleScroll = () => {
|
|
1465
|
+
const threshold = 40;
|
|
1466
|
+
isAtBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
|
|
1467
|
+
};
|
|
1468
|
+
el.addEventListener("scroll", handleScroll, { passive: true });
|
|
1469
|
+
return () => el.removeEventListener("scroll", handleScroll);
|
|
1470
|
+
}, []);
|
|
1471
|
+
react.useEffect(() => {
|
|
1472
|
+
const sentinel = sentinelRef.current;
|
|
1473
|
+
if (!sentinel || !onLoadMore || !hasMoreMessages) return;
|
|
1474
|
+
const observer = new IntersectionObserver(
|
|
1475
|
+
(entries) => {
|
|
1476
|
+
if (entries[0].isIntersecting) {
|
|
1477
|
+
onLoadMore();
|
|
1478
|
+
}
|
|
1479
|
+
},
|
|
1480
|
+
{ root: scrollRef.current, threshold: 0 }
|
|
1481
|
+
);
|
|
1482
|
+
observer.observe(sentinel);
|
|
1483
|
+
return () => observer.disconnect();
|
|
1484
|
+
}, [onLoadMore, hasMoreMessages]);
|
|
1485
|
+
react.useEffect(() => {
|
|
1486
|
+
const ta = textareaRef.current;
|
|
1487
|
+
if (!ta) return;
|
|
1488
|
+
ta.style.height = "auto";
|
|
1489
|
+
ta.style.height = `${Math.min(ta.scrollHeight, 200)}px`;
|
|
1490
|
+
}, [input]);
|
|
1491
|
+
const handleInputChange = react.useCallback(
|
|
1492
|
+
(e) => {
|
|
1493
|
+
const val = e.target.value;
|
|
1494
|
+
setInput(val);
|
|
1495
|
+
const cursorPos = e.target.selectionStart;
|
|
1496
|
+
const textBefore = val.slice(0, cursorPos);
|
|
1497
|
+
const atMatch = textBefore.match(/(?:^|\s)@([^\s]*)$/);
|
|
1498
|
+
if (atMatch) {
|
|
1499
|
+
setMentionQuery(atMatch[1]);
|
|
1500
|
+
setMentionStart(cursorPos - atMatch[1].length - 1);
|
|
1501
|
+
} else {
|
|
1502
|
+
setMentionQuery(null);
|
|
1503
|
+
}
|
|
1504
|
+
},
|
|
1505
|
+
[]
|
|
1506
|
+
);
|
|
1507
|
+
const handleMentionSelect = react.useCallback(
|
|
1508
|
+
(path) => {
|
|
1509
|
+
const before = input.slice(0, mentionStart);
|
|
1510
|
+
const after = input.slice(mentionStart + 1 + (mentionQuery?.length ?? 0));
|
|
1511
|
+
const newInput = `${before}@${path}${after ? after : " "}`;
|
|
1512
|
+
setInput(newInput);
|
|
1513
|
+
setMentionQuery(null);
|
|
1514
|
+
requestAnimationFrame(() => {
|
|
1515
|
+
const ta = textareaRef.current;
|
|
1516
|
+
if (ta) {
|
|
1517
|
+
ta.focus();
|
|
1518
|
+
const pos = before.length + 1 + path.length + 1;
|
|
1519
|
+
ta.selectionStart = pos;
|
|
1520
|
+
ta.selectionEnd = pos;
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
},
|
|
1524
|
+
[input, mentionStart, mentionQuery]
|
|
1525
|
+
);
|
|
1526
|
+
const handleSend = react.useCallback(() => {
|
|
1527
|
+
const trimmed = input.trim();
|
|
1528
|
+
if (!trimmed || isStreaming) return;
|
|
1529
|
+
let messageContent = trimmed;
|
|
1530
|
+
if (inspectedElement) {
|
|
1531
|
+
const ctx = `[Inspected element: <${inspectedElement.tagName} class="${inspectedElement.className}"> "${inspectedElement.textContent.slice(0, 100)}"]
|
|
1532
|
+
|
|
1533
|
+
`;
|
|
1534
|
+
messageContent = ctx + trimmed;
|
|
1535
|
+
onClearInspectedElement?.();
|
|
1536
|
+
}
|
|
1537
|
+
onSendMessage(messageContent);
|
|
1538
|
+
setInput("");
|
|
1539
|
+
setMentionQuery(null);
|
|
1540
|
+
isAtBottomRef.current = true;
|
|
1541
|
+
}, [
|
|
1542
|
+
input,
|
|
1543
|
+
isStreaming,
|
|
1544
|
+
onSendMessage,
|
|
1545
|
+
inspectedElement,
|
|
1546
|
+
onClearInspectedElement
|
|
1547
|
+
]);
|
|
1548
|
+
const handleKeyDown = react.useCallback(
|
|
1549
|
+
(e) => {
|
|
1550
|
+
if (mentionQuery !== null && mentionFiltered.length > 0) {
|
|
1551
|
+
if (e.key === "ArrowDown") {
|
|
1552
|
+
e.preventDefault();
|
|
1553
|
+
setMentionIdx((i) => (i + 1) % mentionFiltered.length);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
if (e.key === "ArrowUp") {
|
|
1557
|
+
e.preventDefault();
|
|
1558
|
+
setMentionIdx(
|
|
1559
|
+
(i) => (i - 1 + mentionFiltered.length) % mentionFiltered.length
|
|
1560
|
+
);
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
if (e.key === "Enter" || e.key === "Tab") {
|
|
1564
|
+
e.preventDefault();
|
|
1565
|
+
handleMentionSelect(mentionFiltered[mentionIdx]);
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (e.key === "Escape") {
|
|
1569
|
+
e.preventDefault();
|
|
1570
|
+
setMentionQuery(null);
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
1575
|
+
e.preventDefault();
|
|
1576
|
+
handleSend();
|
|
1577
|
+
}
|
|
1578
|
+
},
|
|
1579
|
+
[
|
|
1580
|
+
handleSend,
|
|
1581
|
+
mentionQuery,
|
|
1582
|
+
mentionFiltered,
|
|
1583
|
+
mentionIdx,
|
|
1584
|
+
handleMentionSelect
|
|
1585
|
+
]
|
|
1586
|
+
);
|
|
1587
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-full flex flex-col bg-zinc-950", children: [
|
|
1588
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-3 py-1.5 bg-zinc-900/50 border-b border-zinc-800", children: [
|
|
1589
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1590
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
|
|
1591
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-red-500/70" }),
|
|
1592
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-yellow-500/70" }),
|
|
1593
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-green-500/70" })
|
|
1594
|
+
] }),
|
|
1595
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wider text-zinc-500", children: title })
|
|
1596
|
+
] }),
|
|
1597
|
+
modelLabel && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1598
|
+
"span",
|
|
1599
|
+
{
|
|
1600
|
+
className: `text-[10px] font-mono px-2 py-0.5 rounded-full border ${theme.modelBadge}`,
|
|
1601
|
+
children: modelLabel
|
|
1602
|
+
}
|
|
1603
|
+
)
|
|
1604
|
+
] }),
|
|
1605
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1606
|
+
"div",
|
|
1607
|
+
{
|
|
1608
|
+
ref: scrollRef,
|
|
1609
|
+
className: "flex-1 overflow-y-auto px-3 py-3 space-y-3",
|
|
1610
|
+
children: [
|
|
1611
|
+
hasMoreMessages && /* @__PURE__ */ jsxRuntime.jsx("div", { ref: sentinelRef, className: "flex justify-center py-2", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-4 animate-spin rounded-full border border-zinc-600 border-t-zinc-400" }) }),
|
|
1612
|
+
messages.map((msg) => /* @__PURE__ */ jsxRuntime.jsx(MessageBubble, { msg, theme }, msg.id))
|
|
1613
|
+
]
|
|
1614
|
+
}
|
|
1615
|
+
),
|
|
1616
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-shrink-0 border-t border-zinc-800 p-3", children: [
|
|
1617
|
+
inspectedElement && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center gap-2 px-2.5 py-1.5 rounded-lg bg-blue-500/10 border border-blue-500/20", children: [
|
|
1618
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1619
|
+
"svg",
|
|
1620
|
+
{
|
|
1621
|
+
className: "w-3.5 h-3.5 text-blue-400 flex-shrink-0",
|
|
1622
|
+
viewBox: "0 0 24 24",
|
|
1623
|
+
fill: "none",
|
|
1624
|
+
stroke: "currentColor",
|
|
1625
|
+
strokeWidth: 2,
|
|
1626
|
+
strokeLinecap: "round",
|
|
1627
|
+
strokeLinejoin: "round",
|
|
1628
|
+
children: [
|
|
1629
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 12l3 0" }),
|
|
1630
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 3l0 3" }),
|
|
1631
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7.8 7.8l-2.2 -2.2" }),
|
|
1632
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16.2 7.8l2.2 -2.2" }),
|
|
1633
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7.8 16.2l-2.2 2.2" }),
|
|
1634
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 12l9 3l-4 2l-2 4l-3 -9" })
|
|
1635
|
+
]
|
|
1636
|
+
}
|
|
1637
|
+
),
|
|
1638
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
1639
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[11px] font-mono text-blue-300", children: [
|
|
1640
|
+
"<",
|
|
1641
|
+
inspectedElement.tagName,
|
|
1642
|
+
">"
|
|
1643
|
+
] }),
|
|
1644
|
+
inspectedElement.textContent && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[11px] text-zinc-500 ml-1.5 truncate", children: [
|
|
1645
|
+
'"',
|
|
1646
|
+
inspectedElement.textContent.slice(0, 40),
|
|
1647
|
+
inspectedElement.textContent.length > 40 ? "..." : "",
|
|
1648
|
+
'"'
|
|
1649
|
+
] })
|
|
1650
|
+
] }),
|
|
1651
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1652
|
+
"button",
|
|
1653
|
+
{
|
|
1654
|
+
onClick: onClearInspectedElement,
|
|
1655
|
+
className: "text-zinc-500 hover:text-zinc-300 transition-colors flex-shrink-0",
|
|
1656
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-3 h-3", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" }) })
|
|
1657
|
+
}
|
|
1658
|
+
)
|
|
1659
|
+
] }),
|
|
1660
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-end gap-2", children: [
|
|
1661
|
+
mentionQuery !== null && mentionFiltered.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute bottom-full left-0 right-0 mb-1 bg-zinc-900 border border-zinc-700 rounded-lg shadow-xl overflow-hidden z-10", children: mentionFiltered.map((path, i) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1662
|
+
"button",
|
|
1663
|
+
{
|
|
1664
|
+
onMouseDown: (e) => {
|
|
1665
|
+
e.preventDefault();
|
|
1666
|
+
handleMentionSelect(path);
|
|
1667
|
+
},
|
|
1668
|
+
onMouseEnter: () => setMentionIdx(i),
|
|
1669
|
+
className: `w-full text-left px-3 py-1.5 text-[12px] font-mono flex items-center gap-2 transition-colors ${i === mentionIdx ? "bg-blue-500/15 text-blue-300" : "text-zinc-400 hover:bg-zinc-800"}`,
|
|
1670
|
+
children: [
|
|
1671
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1672
|
+
"svg",
|
|
1673
|
+
{
|
|
1674
|
+
className: "w-3 h-3 flex-shrink-0 text-zinc-500",
|
|
1675
|
+
viewBox: "0 0 16 16",
|
|
1676
|
+
fill: "currentColor",
|
|
1677
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.75 1.5A2.25 2.25 0 0 0 1.5 3.75v8.5A2.25 2.25 0 0 0 3.75 14.5h8.5a2.25 2.25 0 0 0 2.25-2.25V6.621a2.25 2.25 0 0 0-.659-1.591L10.47 1.659A2.25 2.25 0 0 0 8.879 1.5H3.75z" })
|
|
1678
|
+
}
|
|
1679
|
+
),
|
|
1680
|
+
path
|
|
1681
|
+
]
|
|
1682
|
+
},
|
|
1683
|
+
path
|
|
1684
|
+
)) }),
|
|
1685
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1686
|
+
"textarea",
|
|
1687
|
+
{
|
|
1688
|
+
ref: textareaRef,
|
|
1689
|
+
value: input,
|
|
1690
|
+
onChange: handleInputChange,
|
|
1691
|
+
onKeyDown: handleKeyDown,
|
|
1692
|
+
disabled: isStreaming,
|
|
1693
|
+
placeholder: isStreaming ? "Generating..." : "Describe a component... (@ to reference files)",
|
|
1694
|
+
rows: 1,
|
|
1695
|
+
className: `
|
|
1696
|
+
flex-1 resize-none overflow-hidden bg-zinc-900 border border-zinc-700/50 rounded-lg px-3 py-2
|
|
1697
|
+
text-[13px] text-zinc-200 font-mono outline-none
|
|
1698
|
+
placeholder:text-zinc-600 disabled:opacity-50
|
|
1699
|
+
focus:border-zinc-600 transition-colors
|
|
1700
|
+
min-h-[60px]
|
|
1701
|
+
${theme.inputCaret}
|
|
1702
|
+
`
|
|
1703
|
+
}
|
|
1704
|
+
),
|
|
1705
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1706
|
+
"button",
|
|
1707
|
+
{
|
|
1708
|
+
onClick: handleSend,
|
|
1709
|
+
disabled: isStreaming || !input.trim(),
|
|
1710
|
+
className: `
|
|
1711
|
+
flex-shrink-0 h-9 w-9 rounded-lg flex items-center justify-center
|
|
1712
|
+
transition-colors disabled:cursor-not-allowed
|
|
1713
|
+
${isStreaming || !input.trim() ? theme.sendButtonDisabled : theme.sendButton}
|
|
1714
|
+
`,
|
|
1715
|
+
children: isStreaming ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-4 animate-spin rounded-full border border-zinc-500 border-t-zinc-300" }) : /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-4 h-4", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M1.724 1.053a.5.5 0 0 0-.714.545l1.403 4.85a.5.5 0 0 0 .397.354l5.69.953c.268.045.268.545 0 .59l-5.69.953a.5.5 0 0 0-.397.354l-1.403 4.85a.5.5 0 0 0 .714.545l13-6.5a.5.5 0 0 0 0-.894l-13-6.5z" }) })
|
|
1716
|
+
}
|
|
1717
|
+
)
|
|
1718
|
+
] }),
|
|
1719
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-zinc-600 mt-1.5 px-1", children: "Enter to send · Shift+Enter for newline · @ to reference files" })
|
|
1720
|
+
] })
|
|
1721
|
+
] });
|
|
1722
|
+
}
|
|
1723
|
+
exports.AIChatPanel = AIChatPanel;
|
|
1724
|
+
exports.BrowserWindow = BrowserWindow;
|
|
1725
|
+
exports.ChatIcon = ChatIcon;
|
|
1726
|
+
exports.CodeEditor = CodeEditor;
|
|
1727
|
+
exports.CodeIcon = CodeIcon;
|
|
1728
|
+
exports.FileExplorer = FileExplorer;
|
|
1729
|
+
exports.FilesIcon = FilesIcon;
|
|
1730
|
+
exports.MobileTabBar = MobileTabBar;
|
|
1731
|
+
exports.PanelToggle = PanelToggle;
|
|
1732
|
+
exports.PreviewIcon = PreviewIcon;
|
|
1733
|
+
exports.Terminal = Terminal;
|
|
1734
|
+
exports.TerminalIcon = TerminalIcon;
|
|
1735
|
+
exports.getChatTheme = getChatTheme;
|
|
1736
|
+
exports.getEditorTheme = getEditorTheme;
|
|
1737
|
+
exports.getFileExplorerTheme = getFileExplorerTheme;
|
|
1738
|
+
exports.getLanguageLabel = getLanguageLabel;
|
|
1739
|
+
exports.getLineStyles = getLineStyles;
|
|
1740
|
+
exports.getTerminalTheme = getTerminalTheme;
|
|
1741
|
+
exports.useMediaQuery = useMediaQuery;
|
|
1742
|
+
//# sourceMappingURL=index.cjs.map
|