Files
pikasTech-HWLAB/web/hwlab-cloud-web/src/components/command-bar/CommandBar.tsx
T
2026-06-08 03:22:26 +08:00

98 lines
5.0 KiB
TypeScript

import type { ReactElement } from "react";
import { FormEvent, KeyboardEvent, useEffect, useMemo, useState } from "react";
import type { ProviderProfile } from "../../types/domain";
import { DraftsList } from "./DraftsList";
interface CommandBarProps {
disabled: boolean;
interactionDisabled?: boolean;
submitMode: "turn" | "steer";
targetTraceId: string | null;
providerProfile: ProviderProfile;
onProviderProfile(profile: ProviderProfile): void;
onSubmit(value: string): Promise<void>;
onClear(): void;
pickedDraft: string | null;
onPickedDraftConsumed(): void;
disabledReason: string | null;
/**
* Notify the workbench that the user is actively typing. Wired by App.tsx
* to `store.recordActivity` so the Code Agent inactivity-timeout (driven
* by the activityRef plumbed in workbench.ts) treats a user who is reading
* or composing a follow-up as "active". This avoids HWLAB #795 style
* false-positive timeouts where the user is at the keyboard but the
* backend has not yet emitted the next trace event.
*/
onTyping(): void;
onPickDraft(draft: string): void;
}
export function CommandBar({ pickedDraft, onPickedDraftConsumed, disabledReason, onTyping, interactionDisabled = false, ...props }: CommandBarProps): ReactElement {
const [value, setValue] = useState("");
const [submitting, setSubmitting] = useState(false);
const [draftsOpen, setDraftsOpen] = useState(false);
const inputRows = useMemo(() => commandInputRows(value), [value]);
useEffect(() => {
if (pickedDraft) {
setValue(pickedDraft);
onPickedDraftConsumed();
}
}, [pickedDraft, onPickedDraftConsumed]);
async function submit(event: FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault();
const next = value.trim();
if (!next || interactionDisabled || props.disabled || submitting) return;
setValue("");
setSubmitting(true);
const submitted = props.onSubmit(next);
window.setTimeout(() => setSubmitting(false), 0);
void submitted.catch(() => setSubmitting(false));
}
function keyDown(event: KeyboardEvent<HTMLTextAreaElement>): void {
if (event.key !== "Enter" || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return;
event.preventDefault();
event.currentTarget.form?.requestSubmit();
}
return (
<form className="command-bar" id="command-form" aria-label="Agent 输入" onSubmit={submit} title={interactionDisabled ? "正在检查登录状态" : disabledReason ?? steerTitle(props.submitMode, props.targetTraceId)}>
<label className="agent-timeout-control" htmlFor="code-agent-provider-profile">
<span>模型通道</span>
<select id="code-agent-provider-profile" aria-label="Code Agent 模型通道" value={props.providerProfile} disabled={interactionDisabled} onChange={(event) => props.onProviderProfile(event.currentTarget.value as ProviderProfile)}>
<option value="deepseek">DeepSeek</option>
<option value="dsflash-go">DeepSeek V4 Flash</option>
<option value="codex-api">Codex API</option>
<option value="minimax-m3">MiniMax-M3</option>
</select>
</label>
<div className="input-shell">
<span className="prompt-mark">hwlab</span>
<textarea id="command-input" className={inputRows.scrollable ? "is-scrollable" : undefined} value={value} placeholder="输入任务,Enter 发送,Shift+Enter 换行" rows={inputRows.rows} disabled={interactionDisabled} onChange={(event) => { setValue(event.currentTarget.value); onTyping(); }} onKeyDown={keyDown} />
</div>
<button className="command-button secondary command-drafts-toggle" id="command-drafts-toggle" type="button" aria-expanded={draftsOpen} aria-controls="command-drafts-panel" disabled={interactionDisabled} onClick={() => setDraftsOpen((open) => !open)}>最近输入</button>
<button className="command-button" id="command-send" type="submit" disabled={interactionDisabled || props.disabled || submitting}>{sendLabel(props.submitMode, submitting)}</button>
<button className="command-button secondary" id="command-clear" type="button" disabled={interactionDisabled} onClick={() => { setValue(""); props.onClear(); }}>清空</button>
{draftsOpen ? <DraftsList id="command-drafts-panel" onPick={(draft) => { props.onPickDraft(draft); setDraftsOpen(false); }} /> : null}
</form>
);
}
export function commandInputRows(value: string): { rows: number; scrollable: boolean } {
const physicalLines = Math.max(1, value.split("\n").length);
return { rows: Math.min(5, physicalLines), scrollable: physicalLines > 5 };
}
function sendLabel(submitMode: "turn" | "steer", submitting: boolean): string {
if (submitting) return submitMode === "steer" ? "引导中" : "发送中";
return submitMode === "steer" ? "引导" : "发送";
}
function steerTitle(submitMode: "turn" | "steer", targetTraceId: string | null): string | undefined {
if (submitMode !== "steer" || !targetTraceId) return undefined;
return `引导运行中的 turn: ${targetTraceId}`;
}