Files
UHD_Media_Tool/apps/web/app/components/agentui/prompt-input.tsx
T
rootandClaude Opus 5.5 8352e5e881 feat(web): W4-F rebuild agent conversation and drawer with agentui components
- Port agentui (MIT, ashish200729/agentui@b2b752b) Message/MessageBubble/MessageScroller,
  StreamingResponse, ToolResult/ToolApproval, TodoList, ApprovalCard, CodeBlock/FileDiff,
  Citations, PromptInput into components/agentui; drop motion/shiki/Next, switch colors,
  type scale, radii, durations and easing to semantic tokens, honour reduced motion
- Conversation: streaming cursor, collapsible thinking, tool cards with kind/scope/risk/
  idempotency badges and audit idempotency key, prominent server-rejection state for both
  whitelist rejections and unauthorized errors, compaction markers, budget meters with
  over-limit state, approval cards (admin session only), two-column final report
  (model said vs system verified with task/record links), follow-scroll with jump-to-bottom
- Composer: Enter send / Shift+Enter newline, stop = abort (confirmed), append,
  execute/plan_only switch, "execute this plan" from finished plan_only runs
- Session list sidebar with status filter and usage; reconnect/error banners with retry
- Drawer: route-derived context (entity/site/task), minimize to a live mini dock
- Fix stream refetch race when the run switches from loading to finished

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 23:08:17 +08:00

138 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Adapted from agentui (https://github.com/ashish200729/agentui), components/agents/prompt-input.tsx
// Copyright (c) 2026 Saurabh Chauhan. MIT License, see ./LICENSE-agentui.md
// 改造:保留自增高(隐藏测量层 + minRows/maxRows)、Enter 发送 / Shift+Enter 换行 / 输入法组字中不发送、
// loading 时发送键换成"停止";去掉模型选择与 MorphPopover 动作菜单,换成 `toolbar` 槽(放模式切换等);
// 外框用 border + focus-within 提亮(不做玻璃叠玻璃),按钮用项目 <Button>。
import { ArrowUp, Square } from "lucide-react";
import {
type FormEvent,
type KeyboardEvent,
type ReactNode,
type TextareaHTMLAttributes,
useCallback,
useLayoutEffect,
useRef,
} from "react";
import { Button } from "~/components/ui/button.tsx";
import { cn } from "~/lib/utils.ts";
export interface PromptInputProps
extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "value" | "onChange" | "onSubmit" | "children"> {
value: string;
onValueChange: (v: string) => void;
onSubmit: (v: string) => void;
/** 运行中:发送键变"停止" */
loading?: boolean;
onStop?: () => void;
/** 提交中(禁用发送) */
pending?: boolean;
minRows?: number;
maxRows?: number;
/** 输入框下方左侧工具条 */
toolbar?: ReactNode;
submitLabel?: string;
stopLabel?: string;
/** 顶部附加内容(如上下文前缀) */
header?: ReactNode;
className?: string;
}
/** 行高 24px(text-sm leading-6),与测量层一致 */
const LINE = 24;
export function PromptInput({
value,
onValueChange,
onSubmit,
loading = false,
onStop,
pending = false,
minRows = 2,
maxRows = 8,
toolbar,
submitLabel = "发送",
stopLabel = "停止",
header,
className,
disabled,
placeholder,
"aria-label": ariaLabel = "输入",
...rest
}: PromptInputProps) {
const ta = useRef<HTMLTextAreaElement>(null);
const measure = useRef<HTMLDivElement>(null);
const resize = useCallback(() => {
const t = ta.current;
const m = measure.current;
if (!t || !m) return;
const h = Math.min(Math.max(m.scrollHeight, minRows * LINE), maxRows * LINE);
t.style.height = `${h}px`;
}, [minRows, maxRows]);
// biome-ignore lint/correctness/useExhaustiveDependencies: 内容变化时重新测量
useLayoutEffect(resize, [value, resize]);
const canSubmit = !!value.trim() && !disabled && !pending;
const submit = (e?: FormEvent) => {
e?.preventDefault();
if (!canSubmit) return;
onSubmit(value.trim());
};
const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key !== "Enter" || e.shiftKey || e.nativeEvent.isComposing) return;
e.preventDefault();
submit();
};
return (
<form
onSubmit={submit}
className={cn(
"relative w-full min-w-0 rounded-2xl border border-border/80 p-2 transition-colors duration-base focus-within:border-foreground/30",
disabled && "opacity-60",
className,
)}
>
{header}
<div className="relative">
<div
ref={measure}
aria-hidden
className="pointer-events-none invisible absolute inset-x-0 top-0 px-2 pt-1.5 text-sm leading-6 break-words whitespace-pre-wrap"
>
{`${value}​`}
</div>
<textarea
ref={ta}
value={value}
disabled={disabled}
placeholder={placeholder}
aria-label={ariaLabel}
rows={minRows}
{...rest}
onChange={(e) => onValueChange(e.target.value)}
onKeyDown={onKeyDown}
className="block w-full resize-none overflow-y-auto bg-transparent px-2 pt-1.5 text-sm leading-6 text-foreground outline-none placeholder:text-muted-foreground/70"
/>
</div>
<div className="mt-1 flex min-h-8 flex-wrap items-center gap-1.5">
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-1.5">{toolbar}</div>
{loading && onStop ? (
<Button type="button" size="sm" variant="destructive" onClick={onStop} aria-label={stopLabel}>
<Square className="size-3.5" />
{stopLabel}
</Button>
) : null}
<Button
type="submit"
size="icon-sm"
disabled={!canSubmit}
aria-label={submitLabel}
title={`${submitLabel}(Enter)`}
>
<ArrowUp />
</Button>
</div>
</form>
);
}