v0.1.4-p2
parent
e6e46478fc
commit
c5b88d50df
|
|
@ -513,6 +513,15 @@ body {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wizard-image-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-image-list .card {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|
@ -532,6 +541,21 @@ body {
|
||||||
font-weight: 700;
|
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 {
|
.mono {
|
||||||
font-family: 'SF Mono', Menlo, Consolas, monospace;
|
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 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 { 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';
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
|
@ -198,17 +198,10 @@ interface NanobotImage {
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DockerImage {
|
|
||||||
tag: string;
|
|
||||||
version?: string;
|
|
||||||
image_id?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BaseImageOption {
|
interface BaseImageOption {
|
||||||
tag: string;
|
tag: string;
|
||||||
label: string;
|
label: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
needsRegister: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkspaceSkillOption {
|
interface WorkspaceSkillOption {
|
||||||
|
|
@ -1048,7 +1041,6 @@ export function BotDashboardModule({
|
||||||
const [isSavingGlobalDelivery, setIsSavingGlobalDelivery] = useState(false);
|
const [isSavingGlobalDelivery, setIsSavingGlobalDelivery] = useState(false);
|
||||||
const [isBatchOperating, setIsBatchOperating] = useState(false);
|
const [isBatchOperating, setIsBatchOperating] = useState(false);
|
||||||
const [availableImages, setAvailableImages] = useState<NanobotImage[]>([]);
|
const [availableImages, setAvailableImages] = useState<NanobotImage[]>([]);
|
||||||
const [localDockerImages, setLocalDockerImages] = useState<DockerImage[]>([]);
|
|
||||||
const [globalDelivery, setGlobalDelivery] = useState<{ sendProgress: boolean; sendToolHints: boolean }>({
|
const [globalDelivery, setGlobalDelivery] = useState<{ sendProgress: boolean; sendToolHints: boolean }>({
|
||||||
sendProgress: false,
|
sendProgress: false,
|
||||||
sendToolHints: 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 renderWorkspaceAwareText = (text: string, keyPrefix: string): ReactNode[] => {
|
||||||
const source = String(text || '');
|
const source = String(text || '');
|
||||||
if (!source) return [source];
|
if (!source) return [source];
|
||||||
|
|
@ -1329,6 +1343,17 @@ export function BotDashboardModule({
|
||||||
</a>
|
</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: ({ children, ...props }: { children?: ReactNode }) => (
|
||||||
<p {...props}>{renderWorkspaceAwareChildren(children, 'md-p')}</p>
|
<p {...props}>{renderWorkspaceAwareChildren(children, 'md-p')}</p>
|
||||||
),
|
),
|
||||||
|
|
@ -1339,7 +1364,7 @@ export function BotDashboardModule({
|
||||||
<code {...props}>{renderWorkspaceAwareChildren(children, 'md-code')}</code>
|
<code {...props}>{renderWorkspaceAwareChildren(children, 'md-code')}</code>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
[fileNotPreviewableLabel, notify, selectedBotId],
|
[fileNotPreviewableLabel, notify, resolveWorkspaceMediaSrc, selectedBotId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [editForm, setEditForm] = useState({
|
const [editForm, setEditForm] = useState({
|
||||||
|
|
@ -1449,48 +1474,32 @@ export function BotDashboardModule({
|
||||||
}, [activeTopicOptions, topics]);
|
}, [activeTopicOptions, topics]);
|
||||||
const lc = isZh ? channelsZhCn : channelsEn;
|
const lc = isZh ? channelsZhCn : channelsEn;
|
||||||
const baseImageOptions = useMemo<BaseImageOption[]>(() => {
|
const baseImageOptions = useMemo<BaseImageOption[]>(() => {
|
||||||
const readyTags = new Set(
|
const imagesByTag = new Map<string, NanobotImage>();
|
||||||
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);
|
|
||||||
});
|
|
||||||
availableImages.forEach((img) => {
|
availableImages.forEach((img) => {
|
||||||
const tag = String(img.tag || '').trim();
|
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) {
|
const options = Array.from(imagesByTag.entries())
|
||||||
allTags.add(editForm.image_tag);
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
}
|
.map(([tag, img]) => {
|
||||||
return Array.from(allTags)
|
const status = String(img.status || '').toUpperCase() || 'UNKNOWN';
|
||||||
.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 {
|
return {
|
||||||
tag,
|
tag,
|
||||||
label: isZh ? `${tag} · 本地镜像(未登记)` : `${tag} · local image (unregistered)`,
|
label: `${tag} · ${status}`,
|
||||||
disabled: false,
|
disabled: status !== 'READY',
|
||||||
needsRegister: true,
|
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
return {
|
const currentTag = String(editForm.image_tag || '').trim();
|
||||||
tag,
|
if (currentTag && !options.some((opt) => opt.tag === currentTag)) {
|
||||||
label: isZh ? `${tag} · 不可用` : `${tag} · unavailable`,
|
options.unshift({
|
||||||
|
tag: currentTag,
|
||||||
|
label: isZh ? `${currentTag} · 未登记(只读)` : `${currentTag} · unregistered (read-only)`,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
needsRegister: false,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}, [availableImages, localDockerImages, editForm.image_tag, isZh]);
|
}
|
||||||
|
return options;
|
||||||
|
}, [availableImages, editForm.image_tag, isZh]);
|
||||||
const runtimeMoreLabel = isZh ? '更多' : 'More';
|
const runtimeMoreLabel = isZh ? '更多' : 'More';
|
||||||
const effectiveTopicPresetTemplates = useMemo(
|
const effectiveTopicPresetTemplates = useMemo(
|
||||||
() => (topicPresetTemplates.length > 0 ? topicPresetTemplates : DEFAULT_TOPIC_PRESET_TEMPLATES),
|
() => (topicPresetTemplates.length > 0 ? topicPresetTemplates : DEFAULT_TOPIC_PRESET_TEMPLATES),
|
||||||
|
|
@ -2080,20 +2089,12 @@ export function BotDashboardModule({
|
||||||
}, [selectedBotId, selectedBot?.send_progress, selectedBot?.send_tool_hints]);
|
}, [selectedBotId, selectedBot?.send_progress, selectedBot?.send_tool_hints]);
|
||||||
|
|
||||||
const loadImageOptions = async () => {
|
const loadImageOptions = async () => {
|
||||||
const [imagesRes, dockerImagesRes] = await Promise.allSettled([
|
const [imagesRes] = await Promise.allSettled([axios.get<NanobotImage[]>(`${APP_ENDPOINTS.apiBase}/images`)]);
|
||||||
axios.get<NanobotImage[]>(`${APP_ENDPOINTS.apiBase}/images`),
|
|
||||||
axios.get<DockerImage[]>(`${APP_ENDPOINTS.apiBase}/docker-images`),
|
|
||||||
]);
|
|
||||||
if (imagesRes.status === 'fulfilled') {
|
if (imagesRes.status === 'fulfilled') {
|
||||||
setAvailableImages(Array.isArray(imagesRes.value.data) ? imagesRes.value.data : []);
|
setAvailableImages(Array.isArray(imagesRes.value.data) ? imagesRes.value.data : []);
|
||||||
} else {
|
} else {
|
||||||
setAvailableImages([]);
|
setAvailableImages([]);
|
||||||
}
|
}
|
||||||
if (dockerImagesRes.status === 'fulfilled') {
|
|
||||||
setLocalDockerImages(Array.isArray(dockerImagesRes.value.data) ? dockerImagesRes.value.data : []);
|
|
||||||
} else {
|
|
||||||
setLocalDockerImages([]);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
|
|
@ -4388,12 +4389,6 @@ export function BotDashboardModule({
|
||||||
if (selectedImageOption?.disabled) {
|
if (selectedImageOption?.disabled) {
|
||||||
throw new Error(isZh ? '当前镜像不可用,请选择可用镜像。' : 'Selected image is unavailable.');
|
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 normalizedCpuCores = clampCpuCores(Number(paramDraft.cpu_cores));
|
||||||
const normalizedMemoryMb = clampMemoryMb(Number(paramDraft.memory_mb));
|
const normalizedMemoryMb = clampMemoryMb(Number(paramDraft.memory_mb));
|
||||||
const normalizedStorageGb = clampStorageGb(Number(paramDraft.storage_gb));
|
const normalizedStorageGb = clampStorageGb(Number(paramDraft.storage_gb));
|
||||||
|
|
@ -5578,11 +5573,6 @@ export function BotDashboardModule({
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</LucentSelect>
|
</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>
|
<label className="field-label">{isZh ? 'CPU 核心数' : 'CPU Cores'}</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -5999,15 +5989,6 @@ export function BotDashboardModule({
|
||||||
/>
|
/>
|
||||||
{t.topicActive}
|
{t.topicActive}
|
||||||
</label>
|
</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
|
<LucentIconButton
|
||||||
className="btn btn-danger btn-sm wizard-icon-btn"
|
className="btn btn-danger btn-sm wizard-icon-btn"
|
||||||
disabled={isSavingTopic}
|
disabled={isSavingTopic}
|
||||||
|
|
@ -6112,6 +6093,13 @@ export function BotDashboardModule({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ export function ImageFactoryModule() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="list-scroll" style={{ maxHeight: '62vh' }}>
|
<div className="list-scroll" style={{ maxHeight: '62vh' }}>
|
||||||
<table className="table">
|
<table className="table image-factory-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Tag</th>
|
<th>Tag</th>
|
||||||
|
|
@ -173,8 +173,8 @@ export function ImageFactoryModule() {
|
||||||
<tr key={img.tag}>
|
<tr key={img.tag}>
|
||||||
<td>
|
<td>
|
||||||
<div className="mono">{img.tag}</div>
|
<div className="mono">{img.tag}</div>
|
||||||
<div style={{ color: 'var(--muted)', fontSize: 11 }}>source: {img.source_dir || 'manual'}</div>
|
<div className="image-factory-meta-line">source: {img.source_dir || 'manual'}</div>
|
||||||
{img.image_id && <div className="mono" style={{ color: 'var(--muted)', fontSize: 11 }}>id: {img.image_id}</div>}
|
{img.image_id && <div className="mono image-factory-meta-line">id: {img.image_id}</div>}
|
||||||
</td>
|
</td>
|
||||||
<td>{img.version}</td>
|
<td>{img.version}</td>
|
||||||
<td><span className={statusClass(img.status)}>{img.status}</span></td>
|
<td><span className={statusClass(img.status)}>{img.status}</span></td>
|
||||||
|
|
@ -218,7 +218,7 @@ export function ImageFactoryModule() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="list-scroll" style={{ maxHeight: '64vh' }}>
|
<div className="list-scroll" style={{ maxHeight: '64vh' }}>
|
||||||
<table className="table">
|
<table className="table image-factory-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Tag</th>
|
<th>Tag</th>
|
||||||
|
|
@ -233,7 +233,7 @@ export function ImageFactoryModule() {
|
||||||
<tr key={img.tag}>
|
<tr key={img.tag}>
|
||||||
<td>
|
<td>
|
||||||
<div className="mono">{img.tag}</div>
|
<div className="mono">{img.tag}</div>
|
||||||
<div style={{ color: 'var(--muted)', fontSize: 11 }}>version: {img.version}</div>
|
<div className="image-factory-meta-line">version: {img.version}</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="mono" style={{ fontSize: 11 }}>{img.image_id.slice(0, 22)}...</td>
|
<td className="mono" style={{ fontSize: 11 }}>{img.image_id.slice(0, 22)}...</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
||||||
|
|
@ -626,7 +626,7 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<div className="stack">
|
<div className="stack">
|
||||||
<button className="btn btn-secondary" onClick={() => void loadImages()}>{isLoadingImages ? ui.loading : ui.loadImages}</button>
|
<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) => (
|
{readyImages.map((img) => (
|
||||||
<label key={img.tag} className="card selectable" style={{ display: 'block', cursor: 'pointer' }}>
|
<label key={img.tag} className="card selectable" style={{ display: 'block', cursor: 'pointer' }}>
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue