diff --git a/frontend/src/App.css b/frontend/src/App.css index 19b3829..dc3c69b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -513,6 +513,15 @@ body { overflow: auto; } +.wizard-image-list { + display: grid; + gap: 10px; +} + +.wizard-image-list .card { + margin: 0; +} + .table { width: 100%; border-collapse: collapse; @@ -532,6 +541,21 @@ body { font-weight: 700; } +.image-factory-table th, +.image-factory-table td { + padding-top: 11px; + padding-bottom: 11px; + line-height: 1.55; + vertical-align: top; +} + +.image-factory-meta-line { + margin-top: 4px; + color: var(--muted); + font-size: 11px; + line-height: 1.5; +} + .mono { font-family: 'SF Mono', Menlo, Consolas, monospace; } diff --git a/frontend/src/modules/dashboard/BotDashboardModule.tsx b/frontend/src/modules/dashboard/BotDashboardModule.tsx index 32bcb9d..390d2c3 100644 --- a/frontend/src/modules/dashboard/BotDashboardModule.tsx +++ b/frontend/src/modules/dashboard/BotDashboardModule.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState, type AnchorHTMLAttributes, type ChangeEvent, type KeyboardEvent, type ReactNode } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState, type AnchorHTMLAttributes, type ChangeEvent, type ImgHTMLAttributes, type KeyboardEvent, type ReactNode } from 'react'; import axios from 'axios'; import { Activity, ArrowUp, Boxes, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Clock3, Copy, Download, EllipsisVertical, ExternalLink, Eye, EyeOff, FileText, FolderOpen, Gauge, Hammer, Lock, Maximize2, MessageCircle, MessageSquareText, Mic, Minimize2, Paperclip, Pencil, Plus, Power, PowerOff, RefreshCw, Reply, RotateCcw, Save, Search, Settings2, SlidersHorizontal, Square, ThumbsDown, ThumbsUp, TriangleAlert, Trash2, UserRound, Waypoints, X } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; @@ -198,17 +198,10 @@ interface NanobotImage { status: string; } -interface DockerImage { - tag: string; - version?: string; - image_id?: string; -} - interface BaseImageOption { tag: string; label: string; disabled: boolean; - needsRegister: boolean; } interface WorkspaceSkillOption { @@ -1048,7 +1041,6 @@ export function BotDashboardModule({ const [isSavingGlobalDelivery, setIsSavingGlobalDelivery] = useState(false); const [isBatchOperating, setIsBatchOperating] = useState(false); const [availableImages, setAvailableImages] = useState([]); - const [localDockerImages, setLocalDockerImages] = useState([]); const [globalDelivery, setGlobalDelivery] = useState<{ sendProgress: boolean; sendToolHints: boolean }>({ sendProgress: false, sendToolHints: false, @@ -1228,6 +1220,28 @@ export function BotDashboardModule({ } } }; + const resolveWorkspaceMediaSrc = useCallback((srcRaw: string): string => { + const src = String(srcRaw || '').trim(); + if (!src || !selectedBotId) return src; + const lower = src.toLowerCase(); + if (lower.startsWith('data:') || lower.startsWith('blob:') || lower.startsWith('http://') || lower.startsWith('https://')) { + const workspacePathFromLink = parseWorkspaceLink(src); + if (workspacePathFromLink) { + return buildWorkspaceDownloadHref(workspacePathFromLink, false); + } + return src; + } + if (src.startsWith('/root/.nanobot/workspace/')) { + const normalized = normalizeDashboardAttachmentPath(src); + if (normalized) return buildWorkspaceDownloadHref(normalized, false); + return src; + } + const workspacePathFromLink = parseWorkspaceLink(src); + if (workspacePathFromLink) { + return buildWorkspaceDownloadHref(workspacePathFromLink, false); + } + return src; + }, [selectedBotId]); const renderWorkspaceAwareText = (text: string, keyPrefix: string): ReactNode[] => { const source = String(text || ''); if (!source) return [source]; @@ -1329,6 +1343,17 @@ export function BotDashboardModule({ ); }, + img: ({ src, alt, ...props }: ImgHTMLAttributes) => { + const resolvedSrc = resolveWorkspaceMediaSrc(String(src || '')); + return ( + {String(alt + ); + }, p: ({ children, ...props }: { children?: ReactNode }) => (

{renderWorkspaceAwareChildren(children, 'md-p')}

), @@ -1339,7 +1364,7 @@ export function BotDashboardModule({ {renderWorkspaceAwareChildren(children, 'md-code')} ), }), - [fileNotPreviewableLabel, notify, selectedBotId], + [fileNotPreviewableLabel, notify, resolveWorkspaceMediaSrc, selectedBotId], ); const [editForm, setEditForm] = useState({ @@ -1449,48 +1474,32 @@ export function BotDashboardModule({ }, [activeTopicOptions, topics]); const lc = isZh ? channelsZhCn : channelsEn; const baseImageOptions = useMemo(() => { - const readyTags = new Set( - availableImages - .filter((img) => String(img.status || '').toUpperCase() === 'READY') - .map((img) => String(img.tag || '').trim()) - .filter(Boolean), - ); - const allTags = new Set(); - localDockerImages.forEach((img) => { - const tag = String(img.tag || '').trim(); - if (tag) allTags.add(tag); - }); + const imagesByTag = new Map(); availableImages.forEach((img) => { const tag = String(img.tag || '').trim(); - if (tag) allTags.add(tag); + if (!tag || imagesByTag.has(tag)) return; + imagesByTag.set(tag, img); }); - if (editForm.image_tag) { - allTags.add(editForm.image_tag); - } - return Array.from(allTags) - .sort((a, b) => a.localeCompare(b)) - .map((tag) => { - const isReady = readyTags.has(tag); - if (isReady) { - return { tag, label: `${tag} · READY`, disabled: false, needsRegister: false }; - } - const hasInDocker = localDockerImages.some((row) => String(row.tag || '').trim() === tag); - if (hasInDocker) { - return { - tag, - label: isZh ? `${tag} · 本地镜像(未登记)` : `${tag} · local image (unregistered)`, - disabled: false, - needsRegister: true, - }; - } + const options = Array.from(imagesByTag.entries()) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([tag, img]) => { + const status = String(img.status || '').toUpperCase() || 'UNKNOWN'; return { tag, - label: isZh ? `${tag} · 不可用` : `${tag} · unavailable`, - disabled: true, - needsRegister: false, + label: `${tag} · ${status}`, + disabled: status !== 'READY', }; }); - }, [availableImages, localDockerImages, editForm.image_tag, isZh]); + const currentTag = String(editForm.image_tag || '').trim(); + if (currentTag && !options.some((opt) => opt.tag === currentTag)) { + options.unshift({ + tag: currentTag, + label: isZh ? `${currentTag} · 未登记(只读)` : `${currentTag} · unregistered (read-only)`, + disabled: true, + }); + } + return options; + }, [availableImages, editForm.image_tag, isZh]); const runtimeMoreLabel = isZh ? '更多' : 'More'; const effectiveTopicPresetTemplates = useMemo( () => (topicPresetTemplates.length > 0 ? topicPresetTemplates : DEFAULT_TOPIC_PRESET_TEMPLATES), @@ -2080,20 +2089,12 @@ export function BotDashboardModule({ }, [selectedBotId, selectedBot?.send_progress, selectedBot?.send_tool_hints]); const loadImageOptions = async () => { - const [imagesRes, dockerImagesRes] = await Promise.allSettled([ - axios.get(`${APP_ENDPOINTS.apiBase}/images`), - axios.get(`${APP_ENDPOINTS.apiBase}/docker-images`), - ]); + const [imagesRes] = await Promise.allSettled([axios.get(`${APP_ENDPOINTS.apiBase}/images`)]); if (imagesRes.status === 'fulfilled') { setAvailableImages(Array.isArray(imagesRes.value.data) ? imagesRes.value.data : []); } else { setAvailableImages([]); } - if (dockerImagesRes.status === 'fulfilled') { - setLocalDockerImages(Array.isArray(dockerImagesRes.value.data) ? dockerImagesRes.value.data : []); - } else { - setLocalDockerImages([]); - } }; const refresh = async () => { @@ -4388,12 +4389,6 @@ export function BotDashboardModule({ if (selectedImageOption?.disabled) { throw new Error(isZh ? '当前镜像不可用,请选择可用镜像。' : 'Selected image is unavailable.'); } - if (selectedImageOption?.needsRegister) { - await axios.post(`${APP_ENDPOINTS.apiBase}/images/register`, { - tag: editForm.image_tag, - source_dir: 'manual', - }); - } const normalizedCpuCores = clampCpuCores(Number(paramDraft.cpu_cores)); const normalizedMemoryMb = clampMemoryMb(Number(paramDraft.memory_mb)); const normalizedStorageGb = clampStorageGb(Number(paramDraft.storage_gb)); @@ -5578,11 +5573,6 @@ export function BotDashboardModule({ ))} - {baseImageOptions.find((opt) => opt.tag === editForm.image_tag)?.needsRegister ? ( -
- {isZh ? '该镜像尚未登记,保存时会自动加入镜像注册表。' : 'This image is not registered yet. It will be auto-registered on save.'} -
- ) : null} {t.topicActive} - void saveTopic(topic)} - tooltip={t.save} - aria-label={t.save} - > - - +
+ {t.topicAddHint} + +
) : null} diff --git a/frontend/src/modules/images/ImageFactoryModule.tsx b/frontend/src/modules/images/ImageFactoryModule.tsx index 48c0fb4..cf52af1 100644 --- a/frontend/src/modules/images/ImageFactoryModule.tsx +++ b/frontend/src/modules/images/ImageFactoryModule.tsx @@ -159,7 +159,7 @@ export function ImageFactoryModule() {
- +
@@ -173,8 +173,8 @@ export function ImageFactoryModule() { @@ -218,7 +218,7 @@ export function ImageFactoryModule() {
-
Tag
{img.tag}
-
source: {img.source_dir || 'manual'}
- {img.image_id &&
id: {img.image_id}
} +
source: {img.source_dir || 'manual'}
+ {img.image_id &&
id: {img.image_id}
}
{img.version} {img.status}
+
@@ -231,10 +231,10 @@ export function ImageFactoryModule() { const already = registeredTags.has(img.tag); return ( - +
Tag
-
{img.tag}
-
version: {img.version}
-
+
{img.tag}
+
version: {img.version}
+
{img.image_id.slice(0, 22)}... -
+
{readyImages.map((img) => (