v0.1.4-p3
parent
34988b2436
commit
775572e07c
|
|
@ -67,7 +67,7 @@ class WhisperSpeechService:
|
|||
raise SpeechServiceError(
|
||||
f"STT model file not found under STT_MODEL_DIR: {candidate}"
|
||||
)
|
||||
return str(candidate.resolve())
|
||||
return str(candidate.resolve())
|
||||
|
||||
def reset_runtime(self) -> None:
|
||||
with self._model_lock:
|
||||
|
|
|
|||
|
|
@ -126,6 +126,15 @@ export const dashboardEn = {
|
|||
user: 'User',
|
||||
inputPlaceholder: 'Type a command and press Enter (Shift+Enter for newline)',
|
||||
disabledPlaceholder: 'Bot is not running. Command input is disabled.',
|
||||
controlCommands: 'Control Commands',
|
||||
controlCommandsDesc: 'Send built-in nanobot slash commands',
|
||||
controlCommandNew: 'New session',
|
||||
controlCommandStop: 'Stop task',
|
||||
controlCommandRestart: 'Restart bot',
|
||||
controlCommandHelp: 'Show commands',
|
||||
controlCommandsShow: 'Show control commands',
|
||||
controlCommandsHide: 'Hide control commands',
|
||||
controlCommandSent: (command: string) => `Control command sent: ${command}`,
|
||||
sending: 'Sending...',
|
||||
send: 'Send',
|
||||
interrupt: 'Interrupt',
|
||||
|
|
|
|||
|
|
@ -126,6 +126,15 @@ export const dashboardZhCn = {
|
|||
user: '用户',
|
||||
inputPlaceholder: '输入指令后回车发送(Shift+Enter 换行)',
|
||||
disabledPlaceholder: 'Bot 未运行,无法发送指令。',
|
||||
controlCommands: '控制命令',
|
||||
controlCommandsDesc: '发送 nanobot 内建 slash 指令',
|
||||
controlCommandNew: '新会话',
|
||||
controlCommandStop: '停止任务',
|
||||
controlCommandRestart: '重启进程',
|
||||
controlCommandHelp: '查看命令',
|
||||
controlCommandsShow: '展开控制命令',
|
||||
controlCommandsHide: '收起控制命令',
|
||||
controlCommandSent: (command: string) => `已发送控制命令 ${command}。`,
|
||||
sending: '发送中...',
|
||||
send: '发送',
|
||||
interrupt: '中断',
|
||||
|
|
|
|||
|
|
@ -218,7 +218,6 @@
|
|||
|
||||
.ops-bot-card:hover {
|
||||
border-color: color-mix(in oklab, var(--brand) 58%, var(--line) 42%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 18px color-mix(in oklab, var(--brand) 14%, transparent);
|
||||
}
|
||||
|
||||
|
|
@ -1292,6 +1291,7 @@
|
|||
}
|
||||
|
||||
.ops-composer-shell {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: minmax(96px, auto) auto;
|
||||
gap: 0;
|
||||
|
|
@ -1320,6 +1320,95 @@
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
.ops-composer-float-controls {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 8px;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ops-control-command-drawer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
transform: translateX(10px);
|
||||
transition: max-width 0.22s ease, opacity 0.18s ease, transform 0.22s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ops-control-command-drawer.is-open {
|
||||
max-width: 240px;
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.ops-control-command-chip {
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid color-mix(in oklab, var(--line) 70%, transparent);
|
||||
border-radius: 999px;
|
||||
background: color-mix(in oklab, var(--panel) 94%, transparent);
|
||||
color: var(--text);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease;
|
||||
box-shadow: 0 4px 10px rgba(9, 15, 28, 0.08);
|
||||
}
|
||||
|
||||
.ops-control-command-chip .mono {
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
.ops-control-command-chip:hover:not(:disabled) {
|
||||
border-color: color-mix(in oklab, var(--brand) 56%, var(--line) 44%);
|
||||
background: color-mix(in oklab, var(--brand-soft) 28%, var(--panel) 72%);
|
||||
}
|
||||
|
||||
.ops-control-command-chip:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ops-control-command-toggle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid color-mix(in oklab, var(--brand) 50%, var(--line) 50%);
|
||||
border-radius: 999px;
|
||||
background: color-mix(in oklab, var(--panel) 95%, var(--brand-soft) 5%);
|
||||
color: var(--brand);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, background 0.2s ease, border-color 0.2s ease;
|
||||
box-shadow: 0 4px 10px rgba(9, 15, 28, 0.08);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.ops-control-command-toggle:hover {
|
||||
transform: translateX(-1px);
|
||||
background: color-mix(in oklab, var(--brand-soft) 26%, var(--panel) 74%);
|
||||
}
|
||||
|
||||
.ops-control-command-toggle.is-open {
|
||||
background: color-mix(in oklab, var(--brand-soft) 34%, var(--panel) 66%);
|
||||
}
|
||||
|
||||
.ops-composer-quote {
|
||||
grid-column: 1 / -1;
|
||||
border: 1px solid color-mix(in oklab, var(--brand) 42%, var(--line) 58%);
|
||||
|
|
@ -1360,7 +1449,7 @@
|
|||
resize: none;
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
padding: 14px 14px 10px 14px;
|
||||
padding: 14px 42px 10px 14px;
|
||||
border: 0 !important;
|
||||
border-bottom: 0 !important;
|
||||
border-radius: 10px 10px 0 0;
|
||||
|
|
@ -1369,6 +1458,11 @@
|
|||
outline: none;
|
||||
}
|
||||
|
||||
.ops-composer-shell.is-command-open .ops-composer-input,
|
||||
.ops-composer-shell.is-command-open .ops-composer-input.input {
|
||||
padding-right: 230px;
|
||||
}
|
||||
|
||||
.ops-composer-shell:focus-within {
|
||||
border-color: var(--brand);
|
||||
}
|
||||
|
|
@ -1494,6 +1588,11 @@
|
|||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.ops-control-command-drawer.is-open {
|
||||
max-width: 220px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ops-voice-wave {
|
||||
gap: 4px;
|
||||
padding: 0 4px;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 { Activity, ArrowUp, Boxes, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Clock3, Command, 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 remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
|
|
@ -1137,6 +1137,7 @@ export function BotDashboardModule({
|
|||
const [isSavingGlobalDelivery, setIsSavingGlobalDelivery] = useState(false);
|
||||
const [isBatchOperating, setIsBatchOperating] = useState(false);
|
||||
const [availableImages, setAvailableImages] = useState<NanobotImage[]>([]);
|
||||
const [controlCommandByBot, setControlCommandByBot] = useState<Record<string, string>>({});
|
||||
const [globalDelivery, setGlobalDelivery] = useState<{ sendProgress: boolean; sendToolHints: boolean }>({
|
||||
sendProgress: false,
|
||||
sendToolHints: false,
|
||||
|
|
@ -1180,6 +1181,7 @@ export function BotDashboardModule({
|
|||
const [isSavingTemplates, setIsSavingTemplates] = useState(false);
|
||||
const [templateAgentText, setTemplateAgentText] = useState('');
|
||||
const [templateTopicText, setTemplateTopicText] = useState('');
|
||||
const [controlCommandPanelOpen, setControlCommandPanelOpen] = useState(false);
|
||||
const [workspaceHoverCard, setWorkspaceHoverCard] = useState<WorkspaceHoverCardState | null>(null);
|
||||
const botSearchInputName = useMemo(
|
||||
() => `nbot-search-${Math.random().toString(36).slice(2, 10)}`,
|
||||
|
|
@ -1679,6 +1681,8 @@ export function BotDashboardModule({
|
|||
const isSending = selectedBot ? Boolean(sendingByBot[selectedBot.id]) : false;
|
||||
const canChat = Boolean(selectedBotEnabled && selectedBot && selectedBot.docker_status === 'RUNNING' && !selectedBotControlState);
|
||||
const isChatEnabled = Boolean(canChat && !isSending);
|
||||
const activeControlCommand = selectedBot ? controlCommandByBot[selectedBot.id] || '' : '';
|
||||
const canSendControlCommand = Boolean(selectedBot && canChat && !isVoiceRecording && !isVoiceTranscribing);
|
||||
|
||||
const conversation = useMemo(() => mergeConversation(messages), [messages]);
|
||||
const latestEvent = useMemo(() => [...events].reverse()[0], [events]);
|
||||
|
|
@ -2044,6 +2048,10 @@ export function BotDashboardModule({
|
|||
persistComposerDraft(selectedBotId, command, pendingAttachments);
|
||||
}, [selectedBotId, composerDraftHydrated, command, pendingAttachments]);
|
||||
|
||||
useEffect(() => {
|
||||
setControlCommandPanelOpen(false);
|
||||
}, [selectedBotId]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearVoiceTimer();
|
||||
|
|
@ -3903,6 +3911,38 @@ export function BotDashboardModule({
|
|||
}
|
||||
};
|
||||
|
||||
const sendControlCommand = async (slashCommand: '/new' | '/restart') => {
|
||||
if (!selectedBot || !canSendControlCommand) return;
|
||||
if (activeControlCommand) return;
|
||||
try {
|
||||
setControlCommandByBot((prev) => ({ ...prev, [selectedBot.id]: slashCommand }));
|
||||
const res = await axios.post(
|
||||
`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/command`,
|
||||
{ command: slashCommand },
|
||||
{ timeout: 12000 },
|
||||
);
|
||||
if (!res.data?.success) {
|
||||
throw new Error(t.backendDeliverFail);
|
||||
}
|
||||
if (slashCommand === '/new') {
|
||||
setCommand('');
|
||||
setPendingAttachments([]);
|
||||
setQuotedReply(null);
|
||||
}
|
||||
setControlCommandPanelOpen(false);
|
||||
notify(t.controlCommandSent(slashCommand), { tone: 'success' });
|
||||
} catch (error: any) {
|
||||
const msg = error?.response?.data?.detail || error?.message || t.sendFail;
|
||||
notify(msg, { tone: 'error' });
|
||||
} finally {
|
||||
setControlCommandByBot((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[selectedBot.id];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const interruptExecution = async () => {
|
||||
if (!selectedBot || !canChat) return;
|
||||
if (interruptingByBot[selectedBot.id]) return;
|
||||
|
|
@ -3916,6 +3956,7 @@ export function BotDashboardModule({
|
|||
if (!res.data?.success) {
|
||||
throw new Error(t.backendDeliverFail);
|
||||
}
|
||||
setControlCommandPanelOpen(false);
|
||||
notify(t.interruptSent, { tone: 'success' });
|
||||
} catch (error: any) {
|
||||
const msg = error?.response?.data?.detail || error?.message || t.sendFail;
|
||||
|
|
@ -5299,7 +5340,53 @@ export function BotDashboardModule({
|
|||
onChange={onPickAttachments}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<div className="ops-composer-shell">
|
||||
<div className={`ops-composer-shell ${controlCommandPanelOpen ? 'is-command-open' : ''}`}>
|
||||
<div className="ops-composer-float-controls">
|
||||
<div className={`ops-control-command-drawer ${controlCommandPanelOpen ? 'is-open' : ''}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="ops-control-command-chip"
|
||||
disabled={!canSendControlCommand || Boolean(activeControlCommand) || Boolean(interruptingByBot[selectedBot.id])}
|
||||
onClick={() => void sendControlCommand('/restart')}
|
||||
aria-label="/restart"
|
||||
title="/restart"
|
||||
>
|
||||
{activeControlCommand === '/restart' ? <RefreshCw size={11} className="animate-spin" /> : <RotateCcw size={11} />}
|
||||
<span className="mono">/restart</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ops-control-command-chip"
|
||||
disabled={!canSendControlCommand || Boolean(activeControlCommand) || Boolean(interruptingByBot[selectedBot.id])}
|
||||
onClick={() => void sendControlCommand('/new')}
|
||||
aria-label="/new"
|
||||
title="/new"
|
||||
>
|
||||
{activeControlCommand === '/new' ? <RefreshCw size={11} className="animate-spin" /> : <Plus size={11} />}
|
||||
<span className="mono">/new</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ops-control-command-chip"
|
||||
disabled={!selectedBot || !canChat || Boolean(activeControlCommand) || Boolean(interruptingByBot[selectedBot.id])}
|
||||
onClick={() => void interruptExecution()}
|
||||
aria-label="/stop"
|
||||
title="/stop"
|
||||
>
|
||||
{interruptingByBot[selectedBot.id] ? <RefreshCw size={11} className="animate-spin" /> : <Square size={11} />}
|
||||
<span className="mono">/stop</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`ops-control-command-toggle ${controlCommandPanelOpen ? 'is-open' : ''}`}
|
||||
onClick={() => setControlCommandPanelOpen((prev) => !prev)}
|
||||
aria-label={controlCommandPanelOpen ? t.controlCommandsHide : t.controlCommandsShow}
|
||||
title={controlCommandPanelOpen ? t.controlCommandsHide : t.controlCommandsShow}
|
||||
>
|
||||
{controlCommandPanelOpen ? <Command size={12} /> : <ChevronLeft size={13} />}
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
ref={composerTextareaRef}
|
||||
className="input ops-composer-input"
|
||||
|
|
|
|||
Loading…
Reference in New Issue