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 {
|
function hasAnalysisIntent(messages: EnterpriseState["messages"]): boolean {
|
||||||
// Walk backwards to find the last human message
|
// Walk backwards to find the last human message
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
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 =
|
const isHuman =
|
||||||
m.role === "user" ||
|
m.role === "user" ||
|
||||||
(typeof m._getType === "function" && (m._getType as () => string)() === "human") ||
|
(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 */
|
/** Map error to a suggestion type for the error-result card */
|
||||||
function errorSuggestion(
|
function errorSuggestion(
|
||||||
toolName: string,
|
_toolName: string,
|
||||||
error: unknown,
|
error: unknown,
|
||||||
): "retry" | "contact_admin" | "check_input" {
|
): "retry" | "contact_admin" | "check_input" {
|
||||||
const msg = error instanceof Error ? error.message : String(error);
|
const msg = error instanceof Error ? error.message : String(error);
|
||||||
@@ -597,7 +597,7 @@ export async function toolExecutorNode(
|
|||||||
const priorityChart = Object.entries(priorityStats).map(
|
const priorityChart = Object.entries(priorityStats).map(
|
||||||
([cname, value]) => ({ name: cname, value }),
|
([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: "pie", title: "状态分布", data: statusChart },
|
||||||
{ chart_type: "bar", title: "优先级分布", data: priorityChart },
|
{ chart_type: "bar", title: "优先级分布", data: priorityChart },
|
||||||
];
|
];
|
||||||
@@ -614,7 +614,7 @@ export async function toolExecutorNode(
|
|||||||
.map(([cname, value]) => ({ name: cname, value }));
|
.map(([cname, value]) => ({ name: cname, value }));
|
||||||
if (trendData.length >= 1) {
|
if (trendData.length >= 1) {
|
||||||
charts.push({
|
charts.push({
|
||||||
chart_type: "line",
|
chart_type: "line" as const,
|
||||||
title: "工单创建时间趋势",
|
title: "工单创建时间趋势",
|
||||||
data: trendData,
|
data: trendData,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -324,11 +324,12 @@ export async function sandboxRun(
|
|||||||
throw new Error(`Unsupported language: ${language}. Supported: ${SUPPORTED_LANGUAGES.join(", ")}`);
|
throw new Error(`Unsupported language: ${language}. Supported: ${SUPPORTED_LANGUAGES.join(", ")}`);
|
||||||
}
|
}
|
||||||
const escaped = code.replace(/'/g, "'\\''");
|
const escaped = code.replace(/'/g, "'\\''");
|
||||||
const cmd: Record<SupportedLang, string> = {
|
const cmdMap: Record<SupportedLang, string> = {
|
||||||
python: `python3 -c '${escaped}'`,
|
python: `python3 -c '${escaped}'`,
|
||||||
javascript: `node -e '${escaped}'`,
|
javascript: `node -e '${escaped}'`,
|
||||||
bash: `bash -c '${escaped}'`,
|
bash: `bash -c '${escaped}'`,
|
||||||
}[language as SupportedLang];
|
};
|
||||||
|
const cmd = cmdMap[language as SupportedLang];
|
||||||
|
|
||||||
let execResp: Response;
|
let execResp: Response;
|
||||||
try {
|
try {
|
||||||
|
|||||||
+82
-3
@@ -145,6 +145,10 @@ function App() {
|
|||||||
// Pending retry tool name — set by soc:retry-tool, consumed on next submit
|
// Pending retry tool name — set by soc:retry-tool, consumed on next submit
|
||||||
const pendingRetryToolRef = useRef<string | null>(null);
|
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[] }>({
|
const thread = useStream<{ messages: Message[]; ui: UIMsgLocal[] }>({
|
||||||
apiUrl: LANGGRAPH_URL,
|
apiUrl: LANGGRAPH_URL,
|
||||||
assistantId: "agent",
|
assistantId: "agent",
|
||||||
@@ -283,7 +287,19 @@ function App() {
|
|||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const text = input.trim();
|
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("");
|
setInput("");
|
||||||
setSourceLabel(null);
|
setSourceLabel(null);
|
||||||
// Clear draft and update lastActive for this thread
|
// 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 ───────────────────────────────────────────
|
// ── Regenerate last AI message ───────────────────────────────────────────
|
||||||
function handleRegenerate() {
|
function handleRegenerate() {
|
||||||
// Find last human message
|
// Find last human message
|
||||||
@@ -842,7 +886,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<form onSubmit={handleSubmit} className="flex gap-2">
|
<form onSubmit={handleSubmit} className="flex gap-2">
|
||||||
<FileUploadButton onFileSelect={setAttachedFile} disabled={thread.isLoading} />
|
<FileUploadButton onFileSelect={setAttachedFile} />
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
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"
|
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}
|
onPaste={handlePaste}
|
||||||
disabled={thread.isLoading}
|
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
{thread.isLoading ? (
|
{thread.isLoading ? (
|
||||||
@@ -901,6 +944,42 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</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) ── */}
|
{/* ── Canvas panel (right drawer) ── */}
|
||||||
<CanvasPanel doc={canvasDoc} onClose={() => setCanvasDoc(null)} />
|
<CanvasPanel doc={canvasDoc} onClose={() => setCanvasDoc(null)} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user