v0.1.4-p2
parent
e6e46478fc
commit
c5b88d50df
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<NanobotImage[]>([]);
|
||||
const [localDockerImages, setLocalDockerImages] = useState<DockerImage[]>([]);
|
||||
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({
|
|||
</a>
|
||||
);
|
||||
},
|
||||
img: ({ src, alt, ...props }: ImgHTMLAttributes<HTMLImageElement>) => {
|
||||
const resolvedSrc = resolveWorkspaceMediaSrc(String(src || ''));
|
||||
return (
|
||||
<img
|
||||
src={resolvedSrc}
|
||||
alt={String(alt || '')}
|
||||
loading="lazy"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
p: ({ children, ...props }: { children?: ReactNode }) => (
|
||||
<p {...props}>{renderWorkspaceAwareChildren(children, 'md-p')}</p>
|
||||
),
|
||||
|
|
@ -1339,7 +1364,7 @@ export function BotDashboardModule({
|
|||
<code {...props}>{renderWorkspaceAwareChildren(children, 'md-code')}</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<BaseImageOption[]>(() => {
|
||||
const readyTags = new Set(
|
||||
availableImages
|
||||
.filter((img) => String(img.status || '').toUpperCase() === 'READY')
|
||||
.map((img) => String(img.tag || '').trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
const allTags = new Set<string>();
|
||||
localDockerImages.forEach((img) => {
|
||||
const tag = String(img.tag || '').trim();
|
||||
if (tag) allTags.add(tag);
|
||||
});
|
||||
const imagesByTag = new Map<string, NanobotImage>();
|
||||
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<NanobotImage[]>(`${APP_ENDPOINTS.apiBase}/images`),
|
||||
axios.get<DockerImage[]>(`${APP_ENDPOINTS.apiBase}/docker-images`),
|
||||
]);
|
||||
const [imagesRes] = await Promise.allSettled([axios.get<NanobotImage[]>(`${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({
|
|||
</option>
|
||||
))}
|
||||
</LucentSelect>
|
||||
{baseImageOptions.find((opt) => opt.tag === editForm.image_tag)?.needsRegister ? (
|
||||
<div className="field-label" style={{ color: 'var(--warning)' }}>
|
||||
{isZh ? '该镜像尚未登记,保存时会自动加入镜像注册表。' : 'This image is not registered yet. It will be auto-registered on save.'}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<label className="field-label">{isZh ? 'CPU 核心数' : 'CPU Cores'}</label>
|
||||
<input
|
||||
|
|
@ -5999,15 +5989,6 @@ export function BotDashboardModule({
|
|||
/>
|
||||
{t.topicActive}
|
||||
</label>
|
||||
<LucentIconButton
|
||||
className="btn btn-primary btn-sm icon-btn"
|
||||
disabled={isSavingTopic}
|
||||
onClick={() => void saveTopic(topic)}
|
||||
tooltip={t.save}
|
||||
aria-label={t.save}
|
||||
>
|
||||
<Save size={14} />
|
||||
</LucentIconButton>
|
||||
<LucentIconButton
|
||||
className="btn btn-danger btn-sm wizard-icon-btn"
|
||||
disabled={isSavingTopic}
|
||||
|
|
@ -6112,6 +6093,13 @@ export function BotDashboardModule({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row-between ops-config-footer">
|
||||
<span className="field-label">{t.topicAddHint}</span>
|
||||
<button className="btn btn-primary btn-sm" disabled={isSavingTopic} onClick={() => void saveTopic(topic)}>
|
||||
{isSavingTopic ? <RefreshCw size={14} className="animate-spin" /> : <Save size={14} />}
|
||||
<span style={{ marginLeft: 6 }}>{t.save}</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ export function ImageFactoryModule() {
|
|||
</div>
|
||||
|
||||
<div className="list-scroll" style={{ maxHeight: '62vh' }}>
|
||||
<table className="table">
|
||||
<table className="table image-factory-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tag</th>
|
||||
|
|
@ -173,8 +173,8 @@ export function ImageFactoryModule() {
|
|||
<tr key={img.tag}>
|
||||
<td>
|
||||
<div className="mono">{img.tag}</div>
|
||||
<div style={{ color: 'var(--muted)', fontSize: 11 }}>source: {img.source_dir || 'manual'}</div>
|
||||
{img.image_id && <div className="mono" style={{ color: 'var(--muted)', fontSize: 11 }}>id: {img.image_id}</div>}
|
||||
<div className="image-factory-meta-line">source: {img.source_dir || 'manual'}</div>
|
||||
{img.image_id && <div className="mono image-factory-meta-line">id: {img.image_id}</div>}
|
||||
</td>
|
||||
<td>{img.version}</td>
|
||||
<td><span className={statusClass(img.status)}>{img.status}</span></td>
|
||||
|
|
@ -218,7 +218,7 @@ export function ImageFactoryModule() {
|
|||
</div>
|
||||
|
||||
<div className="list-scroll" style={{ maxHeight: '64vh' }}>
|
||||
<table className="table">
|
||||
<table className="table image-factory-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tag</th>
|
||||
|
|
@ -231,10 +231,10 @@ export function ImageFactoryModule() {
|
|||
const already = registeredTags.has(img.tag);
|
||||
return (
|
||||
<tr key={img.tag}>
|
||||
<td>
|
||||
<div className="mono">{img.tag}</div>
|
||||
<div style={{ color: 'var(--muted)', fontSize: 11 }}>version: {img.version}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="mono">{img.tag}</div>
|
||||
<div className="image-factory-meta-line">version: {img.version}</div>
|
||||
</td>
|
||||
<td className="mono" style={{ fontSize: 11 }}>{img.image_id.slice(0, 22)}...</td>
|
||||
<td>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -626,7 +626,7 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
|||
{step === 1 && (
|
||||
<div className="stack">
|
||||
<button className="btn btn-secondary" onClick={() => void loadImages()}>{isLoadingImages ? ui.loading : ui.loadImages}</button>
|
||||
<div className="list-scroll" style={{ maxHeight: '52vh' }}>
|
||||
<div className="list-scroll wizard-image-list" style={{ maxHeight: '52vh' }}>
|
||||
{readyImages.map((img) => (
|
||||
<label key={img.tag} className="card selectable" style={{ display: 'block', cursor: 'pointer' }}>
|
||||
<input
|
||||
|
|
|
|||
Loading…
Reference in New Issue