feat: concurrent input dialog, unlock textarea during loading, fix TS errors
- Add showConcurrentDialog modal when submitting while AI is loading
- Offer interrupt (stop + submit) or queue (poll until done) options
- Remove disabled={thread.isLoading} from textarea and FileUploadButton
- Fix TS2352: cast BaseMessage to unknown first before Record
- Fix TS6133: rename unused toolName param to _toolName
- Fix TS2769: chart_type typed as union literal, not string
- Fix TS2322: separate cmdMap lookup from const declaration in soc-client
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
e8a29e1f89
commit
f375220eb6
@@ -65,7 +65,7 @@ const ANALYSIS_KEYWORDS = /趋势|统计|分析|对比|汇总|多少|走势|变
|
||||
function hasAnalysisIntent(messages: EnterpriseState["messages"]): boolean {
|
||||
// Walk backwards to find the last human message
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const m = messages[i] as Record<string, unknown>;
|
||||
const m = messages[i] as unknown as Record<string, unknown>;
|
||||
const isHuman =
|
||||
m.role === "user" ||
|
||||
(typeof m._getType === "function" && (m._getType as () => string)() === "human") ||
|
||||
@@ -80,7 +80,7 @@ function hasAnalysisIntent(messages: EnterpriseState["messages"]): boolean {
|
||||
|
||||
/** Map error to a suggestion type for the error-result card */
|
||||
function errorSuggestion(
|
||||
toolName: string,
|
||||
_toolName: string,
|
||||
error: unknown,
|
||||
): "retry" | "contact_admin" | "check_input" {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
@@ -597,7 +597,7 @@ export async function toolExecutorNode(
|
||||
const priorityChart = Object.entries(priorityStats).map(
|
||||
([cname, value]) => ({ name: cname, value }),
|
||||
);
|
||||
const charts: Array<{ chart_type: string; title: string; data: Array<{ name: string; value: number }> }> = [
|
||||
const charts: Array<{ chart_type: "bar" | "line" | "pie" | "area"; title: string; data: Array<{ name: string; value: number }> }> = [
|
||||
{ chart_type: "pie", title: "状态分布", data: statusChart },
|
||||
{ chart_type: "bar", title: "优先级分布", data: priorityChart },
|
||||
];
|
||||
@@ -614,7 +614,7 @@ export async function toolExecutorNode(
|
||||
.map(([cname, value]) => ({ name: cname, value }));
|
||||
if (trendData.length >= 1) {
|
||||
charts.push({
|
||||
chart_type: "line",
|
||||
chart_type: "line" as const,
|
||||
title: "工单创建时间趋势",
|
||||
data: trendData,
|
||||
});
|
||||
|
||||
@@ -324,11 +324,12 @@ export async function sandboxRun(
|
||||
throw new Error(`Unsupported language: ${language}. Supported: ${SUPPORTED_LANGUAGES.join(", ")}`);
|
||||
}
|
||||
const escaped = code.replace(/'/g, "'\\''");
|
||||
const cmd: Record<SupportedLang, string> = {
|
||||
const cmdMap: Record<SupportedLang, string> = {
|
||||
python: `python3 -c '${escaped}'`,
|
||||
javascript: `node -e '${escaped}'`,
|
||||
bash: `bash -c '${escaped}'`,
|
||||
}[language as SupportedLang];
|
||||
};
|
||||
const cmd = cmdMap[language as SupportedLang];
|
||||
|
||||
let execResp: Response;
|
||||
try {
|
||||
|
||||
+82
-3
@@ -145,6 +145,10 @@ function App() {
|
||||
// Pending retry tool name — set by soc:retry-tool, consumed on next submit
|
||||
const pendingRetryToolRef = useRef<string | null>(null);
|
||||
|
||||
// ── Concurrent submit state ───────────────────────────────────────────────
|
||||
const [showConcurrentDialog, setShowConcurrentDialog] = useState(false);
|
||||
const pendingSubmitRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const thread = useStream<{ messages: Message[]; ui: UIMsgLocal[] }>({
|
||||
apiUrl: LANGGRAPH_URL,
|
||||
assistantId: "agent",
|
||||
@@ -283,7 +287,19 @@ function App() {
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const text = input.trim();
|
||||
if ((!text && !attachedFile) || thread.isLoading) return;
|
||||
if (!text && !attachedFile) return;
|
||||
|
||||
// If AI is still generating, show concurrent choice dialog
|
||||
if (thread.isLoading) {
|
||||
pendingSubmitRef.current = () => doSubmit(text);
|
||||
setShowConcurrentDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
doSubmit(text);
|
||||
}
|
||||
|
||||
async function doSubmit(text: string) {
|
||||
setInput("");
|
||||
setSourceLabel(null);
|
||||
// Clear draft and update lastActive for this thread
|
||||
@@ -354,6 +370,34 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Concurrent dialog handlers ────────────────────────────────────────────
|
||||
function handleConcurrentInterrupt() {
|
||||
setShowConcurrentDialog(false);
|
||||
thread.stop();
|
||||
const submit = pendingSubmitRef.current;
|
||||
pendingSubmitRef.current = null;
|
||||
setTimeout(() => submit?.(), 300);
|
||||
}
|
||||
|
||||
function handleConcurrentQueue() {
|
||||
setShowConcurrentDialog(false);
|
||||
const submit = pendingSubmitRef.current;
|
||||
pendingSubmitRef.current = null;
|
||||
const poll = () => {
|
||||
if (!thread.isLoading) {
|
||||
submit?.();
|
||||
} else {
|
||||
setTimeout(poll, 500);
|
||||
}
|
||||
};
|
||||
setTimeout(poll, 500);
|
||||
}
|
||||
|
||||
function handleConcurrentCancel() {
|
||||
setShowConcurrentDialog(false);
|
||||
pendingSubmitRef.current = null;
|
||||
}
|
||||
|
||||
// ── Regenerate last AI message ───────────────────────────────────────────
|
||||
function handleRegenerate() {
|
||||
// Find last human message
|
||||
@@ -842,7 +886,7 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit} className="flex gap-2">
|
||||
<FileUploadButton onFileSelect={setAttachedFile} disabled={thread.isLoading} />
|
||||
<FileUploadButton onFileSelect={setAttachedFile} />
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="flex-1 rounded-xl border border-input bg-background px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-ring placeholder:text-muted-foreground disabled:opacity-50 resize-none overflow-y-auto"
|
||||
@@ -860,7 +904,6 @@ function App() {
|
||||
}
|
||||
}}
|
||||
onPaste={handlePaste}
|
||||
disabled={thread.isLoading}
|
||||
autoFocus
|
||||
/>
|
||||
{thread.isLoading ? (
|
||||
@@ -901,6 +944,42 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Concurrent submit dialog ── */}
|
||||
{showConcurrentDialog && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||||
<div className="bg-background border border-border rounded-2xl shadow-xl p-6 w-80 flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="font-semibold text-sm">AI 正在处理上一条消息</p>
|
||||
<p className="text-xs text-muted-foreground">请选择如何处理新消息:</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleConcurrentInterrupt}
|
||||
className="w-full rounded-xl bg-destructive text-destructive-foreground px-4 py-2.5 text-sm font-medium hover:opacity-90 transition-opacity flex items-center gap-2"
|
||||
>
|
||||
<Square className="size-4 fill-current" />
|
||||
中断上一条,处理这条
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleConcurrentQueue}
|
||||
className="w-full rounded-xl bg-primary text-primary-foreground px-4 py-2.5 text-sm font-medium hover:opacity-90 transition-opacity"
|
||||
>
|
||||
加入队列,等上一条完成
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleConcurrentCancel}
|
||||
className="w-full rounded-xl border border-border px-4 py-2.5 text-sm font-medium hover:bg-accent transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Canvas panel (right drawer) ── */}
|
||||
<CanvasPanel doc={canvasDoc} onClose={() => setCanvasDoc(null)} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user