v0.1.4-p3
parent
34988b2436
commit
775572e07c
|
|
@ -67,7 +67,7 @@ class WhisperSpeechService:
|
||||||
raise SpeechServiceError(
|
raise SpeechServiceError(
|
||||||
f"STT model file not found under STT_MODEL_DIR: {candidate}"
|
f"STT model file not found under STT_MODEL_DIR: {candidate}"
|
||||||
)
|
)
|
||||||
return str(candidate.resolve())
|
return str(candidate.resolve())
|
||||||
|
|
||||||
def reset_runtime(self) -> None:
|
def reset_runtime(self) -> None:
|
||||||
with self._model_lock:
|
with self._model_lock:
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,15 @@ export const dashboardEn = {
|
||||||
user: 'User',
|
user: 'User',
|
||||||
inputPlaceholder: 'Type a command and press Enter (Shift+Enter for newline)',
|
inputPlaceholder: 'Type a command and press Enter (Shift+Enter for newline)',
|
||||||
disabledPlaceholder: 'Bot is not running. Command input is disabled.',
|
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...',
|
sending: 'Sending...',
|
||||||
send: 'Send',
|
send: 'Send',
|
||||||
interrupt: 'Interrupt',
|
interrupt: 'Interrupt',
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,15 @@ export const dashboardZhCn = {
|
||||||
user: '用户',
|
user: '用户',
|
||||||
inputPlaceholder: '输入指令后回车发送(Shift+Enter 换行)',
|
inputPlaceholder: '输入指令后回车发送(Shift+Enter 换行)',
|
||||||
disabledPlaceholder: 'Bot 未运行,无法发送指令。',
|
disabledPlaceholder: 'Bot 未运行,无法发送指令。',
|
||||||
|
controlCommands: '控制命令',
|
||||||
|
controlCommandsDesc: '发送 nanobot 内建 slash 指令',
|
||||||
|
controlCommandNew: '新会话',
|
||||||
|
controlCommandStop: '停止任务',
|
||||||
|
controlCommandRestart: '重启进程',
|
||||||
|
controlCommandHelp: '查看命令',
|
||||||
|
controlCommandsShow: '展开控制命令',
|
||||||
|
controlCommandsHide: '收起控制命令',
|
||||||
|
controlCommandSent: (command: string) => `已发送控制命令 ${command}。`,
|
||||||
sending: '发送中...',
|
sending: '发送中...',
|
||||||
send: '发送',
|
send: '发送',
|
||||||
interrupt: '中断',
|
interrupt: '中断',
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,6 @@
|
||||||
|
|
||||||
.ops-bot-card:hover {
|
.ops-bot-card:hover {
|
||||||
border-color: color-mix(in oklab, var(--brand) 58%, var(--line) 42%);
|
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);
|
box-shadow: 0 8px 18px color-mix(in oklab, var(--brand) 14%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1292,6 +1291,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-composer-shell {
|
.ops-composer-shell {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: minmax(96px, auto) auto;
|
grid-template-rows: minmax(96px, auto) auto;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
|
|
@ -1320,6 +1320,95 @@
|
||||||
min-height: 0;
|
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 {
|
.ops-composer-quote {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
border: 1px solid color-mix(in oklab, var(--brand) 42%, var(--line) 58%);
|
border: 1px solid color-mix(in oklab, var(--brand) 42%, var(--line) 58%);
|
||||||
|
|
@ -1360,7 +1449,7 @@
|
||||||
resize: none;
|
resize: none;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
padding: 14px 14px 10px 14px;
|
padding: 14px 42px 10px 14px;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
border-bottom: 0 !important;
|
border-bottom: 0 !important;
|
||||||
border-radius: 10px 10px 0 0;
|
border-radius: 10px 10px 0 0;
|
||||||
|
|
@ -1369,6 +1458,11 @@
|
||||||
outline: none;
|
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 {
|
.ops-composer-shell:focus-within {
|
||||||
border-color: var(--brand);
|
border-color: var(--brand);
|
||||||
}
|
}
|
||||||
|
|
@ -1494,6 +1588,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
.ops-control-command-drawer.is-open {
|
||||||
|
max-width: 220px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.ops-voice-wave {
|
.ops-voice-wave {
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 0 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 { 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, 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 ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import rehypeRaw from 'rehype-raw';
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
|
@ -1137,6 +1137,7 @@ 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 [controlCommandByBot, setControlCommandByBot] = useState<Record<string, string>>({});
|
||||||
const [globalDelivery, setGlobalDelivery] = useState<{ sendProgress: boolean; sendToolHints: boolean }>({
|
const [globalDelivery, setGlobalDelivery] = useState<{ sendProgress: boolean; sendToolHints: boolean }>({
|
||||||
sendProgress: false,
|
sendProgress: false,
|
||||||
sendToolHints: false,
|
sendToolHints: false,
|
||||||
|
|
@ -1180,6 +1181,7 @@ export function BotDashboardModule({
|
||||||
const [isSavingTemplates, setIsSavingTemplates] = useState(false);
|
const [isSavingTemplates, setIsSavingTemplates] = useState(false);
|
||||||
const [templateAgentText, setTemplateAgentText] = useState('');
|
const [templateAgentText, setTemplateAgentText] = useState('');
|
||||||
const [templateTopicText, setTemplateTopicText] = useState('');
|
const [templateTopicText, setTemplateTopicText] = useState('');
|
||||||
|
const [controlCommandPanelOpen, setControlCommandPanelOpen] = useState(false);
|
||||||
const [workspaceHoverCard, setWorkspaceHoverCard] = useState<WorkspaceHoverCardState | null>(null);
|
const [workspaceHoverCard, setWorkspaceHoverCard] = useState<WorkspaceHoverCardState | null>(null);
|
||||||
const botSearchInputName = useMemo(
|
const botSearchInputName = useMemo(
|
||||||
() => `nbot-search-${Math.random().toString(36).slice(2, 10)}`,
|
() => `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 isSending = selectedBot ? Boolean(sendingByBot[selectedBot.id]) : false;
|
||||||
const canChat = Boolean(selectedBotEnabled && selectedBot && selectedBot.docker_status === 'RUNNING' && !selectedBotControlState);
|
const canChat = Boolean(selectedBotEnabled && selectedBot && selectedBot.docker_status === 'RUNNING' && !selectedBotControlState);
|
||||||
const isChatEnabled = Boolean(canChat && !isSending);
|
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 conversation = useMemo(() => mergeConversation(messages), [messages]);
|
||||||
const latestEvent = useMemo(() => [...events].reverse()[0], [events]);
|
const latestEvent = useMemo(() => [...events].reverse()[0], [events]);
|
||||||
|
|
@ -2044,6 +2048,10 @@ export function BotDashboardModule({
|
||||||
persistComposerDraft(selectedBotId, command, pendingAttachments);
|
persistComposerDraft(selectedBotId, command, pendingAttachments);
|
||||||
}, [selectedBotId, composerDraftHydrated, command, pendingAttachments]);
|
}, [selectedBotId, composerDraftHydrated, command, pendingAttachments]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setControlCommandPanelOpen(false);
|
||||||
|
}, [selectedBotId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
clearVoiceTimer();
|
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 () => {
|
const interruptExecution = async () => {
|
||||||
if (!selectedBot || !canChat) return;
|
if (!selectedBot || !canChat) return;
|
||||||
if (interruptingByBot[selectedBot.id]) return;
|
if (interruptingByBot[selectedBot.id]) return;
|
||||||
|
|
@ -3916,6 +3956,7 @@ export function BotDashboardModule({
|
||||||
if (!res.data?.success) {
|
if (!res.data?.success) {
|
||||||
throw new Error(t.backendDeliverFail);
|
throw new Error(t.backendDeliverFail);
|
||||||
}
|
}
|
||||||
|
setControlCommandPanelOpen(false);
|
||||||
notify(t.interruptSent, { tone: 'success' });
|
notify(t.interruptSent, { tone: 'success' });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const msg = error?.response?.data?.detail || error?.message || t.sendFail;
|
const msg = error?.response?.data?.detail || error?.message || t.sendFail;
|
||||||
|
|
@ -5299,7 +5340,53 @@ export function BotDashboardModule({
|
||||||
onChange={onPickAttachments}
|
onChange={onPickAttachments}
|
||||||
style={{ display: 'none' }}
|
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
|
<textarea
|
||||||
ref={composerTextareaRef}
|
ref={composerTextareaRef}
|
||||||
className="input ops-composer-input"
|
className="input ops-composer-input"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue