|
|
|
|
@ -65,6 +65,7 @@ interface WorkspacePreviewState {
|
|
|
|
|
ext: string;
|
|
|
|
|
isMarkdown: boolean;
|
|
|
|
|
isImage: boolean;
|
|
|
|
|
isHtml: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface WorkspaceUploadResponse {
|
|
|
|
|
@ -208,7 +209,7 @@ function normalizeRuntimeState(s?: string) {
|
|
|
|
|
function isPreviewableWorkspaceFile(node: WorkspaceNode) {
|
|
|
|
|
if (node.type !== 'file') return false;
|
|
|
|
|
const ext = (node.ext || '').toLowerCase();
|
|
|
|
|
return ['.md', '.json', '.log', '.txt', '.csv', '.pdf', '.png', '.jpg', '.jpeg', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.odt', '.ods', '.odp', '.wps'].includes(ext);
|
|
|
|
|
return ['.md', '.json', '.log', '.txt', '.csv', '.html', '.htm', '.pdf', '.png', '.jpg', '.jpeg', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.odt', '.ods', '.odp', '.wps'].includes(ext);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isPdfPath(path: string) {
|
|
|
|
|
@ -220,6 +221,11 @@ function isImagePath(path: string) {
|
|
|
|
|
return normalized.endsWith('.png') || normalized.endsWith('.jpg') || normalized.endsWith('.jpeg') || normalized.endsWith('.webp');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isHtmlPath(path: string) {
|
|
|
|
|
const normalized = String(path || '').trim().toLowerCase();
|
|
|
|
|
return normalized.endsWith('.html') || normalized.endsWith('.htm');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isOfficePath(path: string) {
|
|
|
|
|
const normalized = String(path || '').trim().toLowerCase();
|
|
|
|
|
return ['.doc', '.docx', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.odt', '.ods', '.odp', '.wps'].some((ext) =>
|
|
|
|
|
@ -229,7 +235,7 @@ function isOfficePath(path: string) {
|
|
|
|
|
|
|
|
|
|
function isPreviewableWorkspacePath(path: string) {
|
|
|
|
|
const normalized = String(path || '').trim().toLowerCase();
|
|
|
|
|
return ['.md', '.json', '.log', '.txt', '.csv', '.pdf', '.png', '.jpg', '.jpeg', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.odt', '.ods', '.odp', '.wps'].some((ext) =>
|
|
|
|
|
return ['.md', '.json', '.log', '.txt', '.csv', '.html', '.htm', '.pdf', '.png', '.jpg', '.jpeg', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.odt', '.ods', '.odp', '.wps'].some((ext) =>
|
|
|
|
|
normalized.endsWith(ext),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
@ -238,7 +244,7 @@ function workspaceFileAction(path: string): 'preview' | 'download' | 'unsupporte
|
|
|
|
|
const normalized = String(path || '').trim();
|
|
|
|
|
if (!normalized) return 'unsupported';
|
|
|
|
|
if (isPdfPath(normalized) || isOfficePath(normalized)) return 'download';
|
|
|
|
|
if (isImagePath(normalized)) return 'preview';
|
|
|
|
|
if (isImagePath(normalized) || isHtmlPath(normalized)) return 'preview';
|
|
|
|
|
const lower = normalized.toLowerCase();
|
|
|
|
|
if (['.md', '.json', '.log', '.txt', '.csv'].some((ext) => lower.endsWith(ext))) return 'preview';
|
|
|
|
|
return 'unsupported';
|
|
|
|
|
@ -269,7 +275,7 @@ function decorateWorkspacePathsForMarkdown(text: string) {
|
|
|
|
|
'[$1]($2)',
|
|
|
|
|
);
|
|
|
|
|
const workspacePathPattern =
|
|
|
|
|
/\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps)\b/gi;
|
|
|
|
|
/\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|html|htm|pdf|png|jpg|jpeg|webp|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps)\b/gi;
|
|
|
|
|
return normalizedExistingLinks.replace(workspacePathPattern, (fullPath) => {
|
|
|
|
|
const normalized = normalizeDashboardAttachmentPath(fullPath);
|
|
|
|
|
if (!normalized) return fullPath;
|
|
|
|
|
@ -433,6 +439,33 @@ export function BotDashboardModule({
|
|
|
|
|
link.click();
|
|
|
|
|
link.remove();
|
|
|
|
|
};
|
|
|
|
|
const copyWorkspacePreviewUrl = async (filePath: string) => {
|
|
|
|
|
const normalized = String(filePath || '').trim();
|
|
|
|
|
if (!selectedBotId || !normalized) return;
|
|
|
|
|
const hrefRaw = buildWorkspaceDownloadHref(normalized, false);
|
|
|
|
|
const href = (() => {
|
|
|
|
|
try {
|
|
|
|
|
return new URL(hrefRaw, window.location.origin).href;
|
|
|
|
|
} catch {
|
|
|
|
|
return hrefRaw;
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
try {
|
|
|
|
|
if (navigator.clipboard?.writeText) {
|
|
|
|
|
await navigator.clipboard.writeText(href);
|
|
|
|
|
} else {
|
|
|
|
|
const ta = document.createElement('textarea');
|
|
|
|
|
ta.value = href;
|
|
|
|
|
document.body.appendChild(ta);
|
|
|
|
|
ta.select();
|
|
|
|
|
document.execCommand('copy');
|
|
|
|
|
ta.remove();
|
|
|
|
|
}
|
|
|
|
|
notify(t.urlCopied, { tone: 'success' });
|
|
|
|
|
} catch {
|
|
|
|
|
notify(t.urlCopyFail, { tone: 'error' });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const openWorkspacePathFromChat = (path: string) => {
|
|
|
|
|
const normalized = String(path || '').trim();
|
|
|
|
|
if (!normalized) return;
|
|
|
|
|
@ -454,7 +487,7 @@ export function BotDashboardModule({
|
|
|
|
|
const source = String(text || '');
|
|
|
|
|
if (!source) return [source];
|
|
|
|
|
const pattern =
|
|
|
|
|
/\[(\/root\/\.nanobot\/workspace\/[^\]]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps))\]\((https:\/\/workspace\.local\/open\/[^)\s]+)\)|\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps)\b|https:\/\/workspace\.local\/open\/[^\s)]+/gi;
|
|
|
|
|
/\[(\/root\/\.nanobot\/workspace\/[^\]]+?\.(?:md|json|log|txt|csv|html|htm|pdf|png|jpg|jpeg|webp|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps))\]\((https:\/\/workspace\.local\/open\/[^)\s]+)\)|\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|html|htm|pdf|png|jpg|jpeg|webp|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps)\b|https:\/\/workspace\.local\/open\/[^\s)]+/gi;
|
|
|
|
|
const nodes: ReactNode[] = [];
|
|
|
|
|
let lastIndex = 0;
|
|
|
|
|
let matchIndex = 0;
|
|
|
|
|
@ -885,6 +918,20 @@ export function BotDashboardModule({
|
|
|
|
|
ext: fileExt ? `.${fileExt}` : '',
|
|
|
|
|
isMarkdown: false,
|
|
|
|
|
isImage: true,
|
|
|
|
|
isHtml: false,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (isHtmlPath(normalizedPath)) {
|
|
|
|
|
const fileExt = (normalizedPath.split('.').pop() || '').toLowerCase();
|
|
|
|
|
setWorkspacePreview({
|
|
|
|
|
path: normalizedPath,
|
|
|
|
|
content: '',
|
|
|
|
|
truncated: false,
|
|
|
|
|
ext: fileExt ? `.${fileExt}` : '',
|
|
|
|
|
isMarkdown: false,
|
|
|
|
|
isImage: false,
|
|
|
|
|
isHtml: true,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@ -910,6 +957,7 @@ export function BotDashboardModule({
|
|
|
|
|
ext: textExt ? `.${textExt}` : '',
|
|
|
|
|
isMarkdown: textExt === 'md' || Boolean(res.data.is_markdown),
|
|
|
|
|
isImage: false,
|
|
|
|
|
isHtml: false,
|
|
|
|
|
});
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
const msg = error?.response?.data?.detail || t.fileReadFail;
|
|
|
|
|
@ -2102,9 +2150,16 @@ export function BotDashboardModule({
|
|
|
|
|
{showBaseModal && (
|
|
|
|
|
<div className="modal-mask" onClick={() => setShowBaseModal(false)}>
|
|
|
|
|
<div className="modal-card" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<div className="modal-title-row">
|
|
|
|
|
<h3>{t.baseConfig}</h3>
|
|
|
|
|
<span className="modal-sub">{t.baseConfigSub}</span>
|
|
|
|
|
<div className="modal-title-row modal-title-with-close">
|
|
|
|
|
<div className="modal-title-main">
|
|
|
|
|
<h3>{t.baseConfig}</h3>
|
|
|
|
|
<span className="modal-sub">{t.baseConfigSub}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="modal-title-actions">
|
|
|
|
|
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowBaseModal(false)} title={t.close} aria-label={t.close}>
|
|
|
|
|
<X size={14} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<label className="field-label">{t.botIdReadonly}</label>
|
|
|
|
|
@ -2153,7 +2208,16 @@ export function BotDashboardModule({
|
|
|
|
|
{showParamModal && (
|
|
|
|
|
<div className="modal-mask" onClick={() => setShowParamModal(false)}>
|
|
|
|
|
<div className="modal-card" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<h3>{t.modelParams}</h3>
|
|
|
|
|
<div className="modal-title-row modal-title-with-close">
|
|
|
|
|
<div className="modal-title-main">
|
|
|
|
|
<h3>{t.modelParams}</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="modal-title-actions">
|
|
|
|
|
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowParamModal(false)} title={t.close} aria-label={t.close}>
|
|
|
|
|
<X size={14} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="slider-row">
|
|
|
|
|
<label className="field-label">Temperature: {Number(editForm.temperature).toFixed(2)}</label>
|
|
|
|
|
<input type="range" min="0" max="1" step="0.01" value={editForm.temperature} onChange={(e) => setEditForm((p) => ({ ...p, temperature: clampTemperature(Number(e.target.value)) }))} />
|
|
|
|
|
@ -2175,7 +2239,16 @@ export function BotDashboardModule({
|
|
|
|
|
{showChannelModal && (
|
|
|
|
|
<div className="modal-mask" onClick={() => setShowChannelModal(false)}>
|
|
|
|
|
<div className="modal-card modal-wide" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<h3>{lc.wizardSectionTitle}</h3>
|
|
|
|
|
<div className="modal-title-row modal-title-with-close">
|
|
|
|
|
<div className="modal-title-main">
|
|
|
|
|
<h3>{lc.wizardSectionTitle}</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="modal-title-actions">
|
|
|
|
|
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowChannelModal(false)} title={t.close} aria-label={t.close}>
|
|
|
|
|
<X size={14} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="card" style={{ fontSize: 12, color: 'var(--muted)' }}>
|
|
|
|
|
{lc.wizardSectionDesc}
|
|
|
|
|
</div>
|
|
|
|
|
@ -2278,10 +2351,6 @@ export function BotDashboardModule({
|
|
|
|
|
<Plus size={14} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="row-between">
|
|
|
|
|
<span className="field-label">{lc.wizardSectionDesc}</span>
|
|
|
|
|
<button className="btn btn-primary" onClick={() => setShowChannelModal(false)}>{lc.close}</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
@ -2289,7 +2358,16 @@ export function BotDashboardModule({
|
|
|
|
|
{showSkillsModal && (
|
|
|
|
|
<div className="modal-mask" onClick={() => setShowSkillsModal(false)}>
|
|
|
|
|
<div className="modal-card modal-wide" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<h3>{t.skillsPanel}</h3>
|
|
|
|
|
<div className="modal-title-row modal-title-with-close">
|
|
|
|
|
<div className="modal-title-main">
|
|
|
|
|
<h3>{t.skillsPanel}</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="modal-title-actions">
|
|
|
|
|
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowSkillsModal(false)} title={t.close} aria-label={t.close}>
|
|
|
|
|
<X size={14} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="wizard-channel-list">
|
|
|
|
|
{botSkills.length === 0 ? (
|
|
|
|
|
<div className="ops-empty-inline">{t.skillsEmpty}</div>
|
|
|
|
|
@ -2338,10 +2416,6 @@ export function BotDashboardModule({
|
|
|
|
|
</button>
|
|
|
|
|
<span className="field-label">{t.zipOnlyHint}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="row-between">
|
|
|
|
|
<span className="field-label"> </span>
|
|
|
|
|
<button className="btn btn-primary" onClick={() => setShowSkillsModal(false)}>{t.close}</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
@ -2349,7 +2423,16 @@ export function BotDashboardModule({
|
|
|
|
|
{showEnvParamsModal && (
|
|
|
|
|
<div className="modal-mask" onClick={() => setShowEnvParamsModal(false)}>
|
|
|
|
|
<div className="modal-card modal-wide" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<h3>{t.envParams}</h3>
|
|
|
|
|
<div className="modal-title-row modal-title-with-close">
|
|
|
|
|
<div className="modal-title-main">
|
|
|
|
|
<h3>{t.envParams}</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="modal-title-actions">
|
|
|
|
|
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowEnvParamsModal(false)} title={t.close} aria-label={t.close}>
|
|
|
|
|
<X size={14} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="field-label" style={{ marginBottom: 8 }}>{t.envParamsDesc}</div>
|
|
|
|
|
<div className="wizard-channel-list">
|
|
|
|
|
{envEntries.length === 0 ? (
|
|
|
|
|
@ -2438,17 +2521,24 @@ export function BotDashboardModule({
|
|
|
|
|
{showCronModal && (
|
|
|
|
|
<div className="modal-mask" onClick={() => setShowCronModal(false)}>
|
|
|
|
|
<div className="modal-card modal-wide" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<div className="row-between">
|
|
|
|
|
<h3>{t.cronViewer}</h3>
|
|
|
|
|
<button
|
|
|
|
|
className="btn btn-secondary btn-sm icon-btn"
|
|
|
|
|
onClick={() => selectedBot && void loadCronJobs(selectedBot.id)}
|
|
|
|
|
title={t.cronReload}
|
|
|
|
|
aria-label={t.cronReload}
|
|
|
|
|
disabled={cronLoading}
|
|
|
|
|
>
|
|
|
|
|
<RefreshCw size={14} className={cronLoading ? 'animate-spin' : ''} />
|
|
|
|
|
</button>
|
|
|
|
|
<div className="modal-title-row modal-title-with-close">
|
|
|
|
|
<div className="modal-title-main">
|
|
|
|
|
<h3>{t.cronViewer}</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="modal-title-actions">
|
|
|
|
|
<button
|
|
|
|
|
className="btn btn-secondary btn-sm icon-btn"
|
|
|
|
|
onClick={() => selectedBot && void loadCronJobs(selectedBot.id)}
|
|
|
|
|
title={t.cronReload}
|
|
|
|
|
aria-label={t.cronReload}
|
|
|
|
|
disabled={cronLoading}
|
|
|
|
|
>
|
|
|
|
|
<RefreshCw size={14} className={cronLoading ? 'animate-spin' : ''} />
|
|
|
|
|
</button>
|
|
|
|
|
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowCronModal(false)} title={t.close} aria-label={t.close}>
|
|
|
|
|
<X size={14} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{cronLoading ? (
|
|
|
|
|
<div className="ops-empty-inline">{t.cronLoading}</div>
|
|
|
|
|
@ -2502,10 +2592,6 @@ export function BotDashboardModule({
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div className="row-between">
|
|
|
|
|
<button className="btn btn-secondary" onClick={() => setShowCronModal(false)}>{t.close}</button>
|
|
|
|
|
<span />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
@ -2513,7 +2599,16 @@ export function BotDashboardModule({
|
|
|
|
|
{showAgentModal && (
|
|
|
|
|
<div className="modal-mask" onClick={() => setShowAgentModal(false)}>
|
|
|
|
|
<div className="modal-card modal-wide" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<h3>{t.agentFiles}</h3>
|
|
|
|
|
<div className="modal-title-row modal-title-with-close">
|
|
|
|
|
<div className="modal-title-main">
|
|
|
|
|
<h3>{t.agentFiles}</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="modal-title-actions">
|
|
|
|
|
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowAgentModal(false)} title={t.close} aria-label={t.close}>
|
|
|
|
|
<X size={14} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="wizard-agent-layout">
|
|
|
|
|
<div className="agent-tabs-vertical">
|
|
|
|
|
{(['AGENTS', 'SOUL', 'USER', 'TOOLS', 'IDENTITY'] as AgentTab[]).map((tab) => (
|
|
|
|
|
@ -2533,16 +2628,19 @@ export function BotDashboardModule({
|
|
|
|
|
{showRuntimeActionModal && (
|
|
|
|
|
<div className="modal-mask" onClick={() => setShowRuntimeActionModal(false)}>
|
|
|
|
|
<div className="modal-card modal-preview" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<div className="modal-title-row">
|
|
|
|
|
<h3>{t.lastAction}</h3>
|
|
|
|
|
<div className="modal-title-row modal-title-with-close">
|
|
|
|
|
<div className="modal-title-main">
|
|
|
|
|
<h3>{t.lastAction}</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="modal-title-actions">
|
|
|
|
|
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowRuntimeActionModal(false)} title={t.close} aria-label={t.close}>
|
|
|
|
|
<X size={14} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="workspace-preview-body">
|
|
|
|
|
<pre>{runtimeAction}</pre>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="row-between">
|
|
|
|
|
<span />
|
|
|
|
|
<button className="btn btn-primary" onClick={() => setShowRuntimeActionModal(false)}>{t.close}</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
@ -2581,6 +2679,12 @@ export function BotDashboardModule({
|
|
|
|
|
src={`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(workspacePreview.path)}`}
|
|
|
|
|
alt={workspacePreview.path.split('/').pop() || 'workspace-image'}
|
|
|
|
|
/>
|
|
|
|
|
) : workspacePreview.isHtml ? (
|
|
|
|
|
<iframe
|
|
|
|
|
className="workspace-preview-embed"
|
|
|
|
|
src={`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(workspacePreview.path)}`}
|
|
|
|
|
title={workspacePreview.path}
|
|
|
|
|
/>
|
|
|
|
|
) : workspacePreview.isMarkdown ? (
|
|
|
|
|
<div className="workspace-markdown">
|
|
|
|
|
<ReactMarkdown
|
|
|
|
|
@ -2601,15 +2705,24 @@ export function BotDashboardModule({
|
|
|
|
|
<div className="row-between">
|
|
|
|
|
<span className="workspace-preview-meta mono">{workspacePreview.ext || '-'}</span>
|
|
|
|
|
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
|
|
|
|
<a
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
href={`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(workspacePreview.path)}&download=1`}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
download={workspacePreview.path.split('/').pop() || 'workspace-file'}
|
|
|
|
|
>
|
|
|
|
|
{t.download}
|
|
|
|
|
</a>
|
|
|
|
|
{workspacePreview.isHtml ? (
|
|
|
|
|
<button
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
onClick={() => void copyWorkspacePreviewUrl(workspacePreview.path)}
|
|
|
|
|
>
|
|
|
|
|
{t.copyAddress}
|
|
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<a
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
href={`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(workspacePreview.path)}&download=1`}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
download={workspacePreview.path.split('/').pop() || 'workspace-file'}
|
|
|
|
|
>
|
|
|
|
|
{t.download}
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|