diff --git a/frontend/src/App.css b/frontend/src/App.css
index 75d71cc..c119e36 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -794,6 +794,31 @@ body {
gap: 4px;
}
+.modal-title-row.modal-title-with-close {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+ position: relative;
+ padding-right: 42px;
+ min-height: 28px;
+}
+
+.modal-title-main {
+ min-width: 0;
+ display: grid;
+ gap: 4px;
+}
+
+.modal-title-actions {
+ position: absolute;
+ right: 0;
+ top: 0;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
.modal-sub {
color: var(--subtitle);
font-size: 12px;
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index abf11b3..f45154a 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
-import { Boxes, MoonStar, Sparkles, SunMedium } from 'lucide-react';
+import { MoonStar, SunMedium, X } from 'lucide-react';
import {
useAppStore,
} from './store/appStore';
@@ -106,11 +106,15 @@ function App() {
{!urlView.compactMode && showImageFactory && (
setShowImageFactory(false)}>
e.stopPropagation()}>
-
-
{t.nav.images.title}
-
+
+
+
{t.nav.images.title}
+
+
+
+
@@ -122,11 +126,15 @@ function App() {
{!urlView.compactMode && showCreateWizard && (
setShowCreateWizard(false)}>
e.stopPropagation()}>
-
-
{t.nav.onboarding.title}
-
+
+
+
{t.nav.onboarding.title}
+
+
+
+
@@ -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 && (
setShowBaseModal(false)}>
e.stopPropagation()}>
-
-
{t.baseConfig}
-
{t.baseConfigSub}
+
+
+
{t.baseConfig}
+ {t.baseConfigSub}
+
+
+
+
@@ -2153,7 +2208,16 @@ export function BotDashboardModule({
{showParamModal && (
setShowParamModal(false)}>
e.stopPropagation()}>
-
{t.modelParams}
+
+
+
{t.modelParams}
+
+
+
+
+
setEditForm((p) => ({ ...p, temperature: clampTemperature(Number(e.target.value)) }))} />
@@ -2175,7 +2239,16 @@ export function BotDashboardModule({
{showChannelModal && (
setShowChannelModal(false)}>
e.stopPropagation()}>
-
{lc.wizardSectionTitle}
+
+
+
{lc.wizardSectionTitle}
+
+
+
+
+
{lc.wizardSectionDesc}
@@ -2278,10 +2351,6 @@ export function BotDashboardModule({
-
- {lc.wizardSectionDesc}
-
-
)}
@@ -2289,7 +2358,16 @@ export function BotDashboardModule({
{showSkillsModal && (
setShowSkillsModal(false)}>
e.stopPropagation()}>
-
{t.skillsPanel}
+
+
+
{t.skillsPanel}
+
+
+
+
+
{botSkills.length === 0 ? (
{t.skillsEmpty}
@@ -2338,10 +2416,6 @@ export function BotDashboardModule({
{t.zipOnlyHint}
-
-
-
-
)}
@@ -2349,7 +2423,16 @@ export function BotDashboardModule({
{showEnvParamsModal && (
setShowEnvParamsModal(false)}>
e.stopPropagation()}>
-
{t.envParams}
+
+
+
{t.envParams}
+
+
+
+
+
{t.envParamsDesc}
{envEntries.length === 0 ? (
@@ -2438,17 +2521,24 @@ export function BotDashboardModule({
{showCronModal && (
setShowCronModal(false)}>
e.stopPropagation()}>
-
-
{t.cronViewer}
-
+
+
+
{t.cronViewer}
+
+
+
+
+
{cronLoading ? (
{t.cronLoading}
@@ -2502,10 +2592,6 @@ export function BotDashboardModule({
})}
)}
-
-
-
-
)}
@@ -2513,7 +2599,16 @@ export function BotDashboardModule({
{showAgentModal && (
setShowAgentModal(false)}>
e.stopPropagation()}>
-
{t.agentFiles}
+
+
+
{t.agentFiles}
+
+
+
+
+
{(['AGENTS', 'SOUL', 'USER', 'TOOLS', 'IDENTITY'] as AgentTab[]).map((tab) => (
@@ -2533,16 +2628,19 @@ export function BotDashboardModule({
{showRuntimeActionModal && (
setShowRuntimeActionModal(false)}>
e.stopPropagation()}>
-
-
{t.lastAction}
+
+
+
{t.lastAction}
+
+
+
+
-
-
-
-
)}
@@ -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 ? (
+
) : workspacePreview.isMarkdown ? (
{workspacePreview.ext || '-'}
-
- {t.download}
-
+ {workspacePreview.isHtml ? (
+
+ ) : (
+
+ {t.download}
+
+ )}