[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
228
components/ai-elements/prompt-input.tsx
Normal file
228
components/ai-elements/prompt-input.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* PromptInput - Adapted from Vercel AI Elements prompt-input for netcatty.
|
||||
*
|
||||
* Simplified: no file attachments, screenshots, drag-drop, command palette,
|
||||
* hover cards, referenced sources, or tabs. Core input + footer + submit.
|
||||
*/
|
||||
|
||||
import { ArrowUp, Square, X } from 'lucide-react';
|
||||
import type {
|
||||
ComponentProps,
|
||||
FormEvent,
|
||||
HTMLAttributes,
|
||||
KeyboardEvent,
|
||||
} from 'react';
|
||||
import { forwardRef, useCallback, useRef } from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from '../ui/input-group';
|
||||
import { Spinner } from '../ui/spinner';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PromptInput (form wrapper)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface PromptInputProps extends HTMLAttributes<HTMLFormElement> {
|
||||
onSubmit: (text: string, event: FormEvent<HTMLFormElement>) => void | Promise<void>;
|
||||
/** Allow attachments that carry their own prompt context to submit without textarea text. */
|
||||
allowEmptySubmit?: boolean;
|
||||
/** Optional styling for the bordered input group inside the form. */
|
||||
inputGroupClassName?: string;
|
||||
}
|
||||
|
||||
export const shouldSubmitPromptInput = (text: string, allowEmptySubmit = false): boolean =>
|
||||
Boolean(text.trim()) || allowEmptySubmit;
|
||||
|
||||
export const PromptInput = forwardRef<HTMLFormElement, PromptInputProps>(
|
||||
({ className, onSubmit, allowEmptySubmit = false, inputGroupClassName, children, ...props }, ref) => {
|
||||
const handleSubmit = useCallback(
|
||||
(e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const form = e.currentTarget;
|
||||
const textarea = form.querySelector('textarea');
|
||||
const text = textarea?.value?.trim() ?? '';
|
||||
if (!shouldSubmitPromptInput(text, allowEmptySubmit)) return;
|
||||
onSubmit(text, e);
|
||||
},
|
||||
[allowEmptySubmit, onSubmit],
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={handleSubmit}
|
||||
className={className}
|
||||
data-allow-empty-submit={allowEmptySubmit ? 'true' : undefined}
|
||||
{...props}
|
||||
>
|
||||
<InputGroup className={inputGroupClassName}>{children}</InputGroup>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
);
|
||||
PromptInput.displayName = 'PromptInput';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PromptInputTextarea
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface PromptInputTextareaProps extends ComponentProps<'textarea'> {
|
||||
/** Called when Enter is pressed (without Shift) to trigger form submit */
|
||||
onSubmitRequest?: () => void;
|
||||
}
|
||||
|
||||
export const PromptInputTextarea = forwardRef<HTMLTextAreaElement, PromptInputTextareaProps>(
|
||||
({ className, onSubmitRequest, onKeyDown, ...props }, ref) => {
|
||||
const internalRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const setRef = useCallback(
|
||||
(node: HTMLTextAreaElement | null) => {
|
||||
internalRef.current = node;
|
||||
if (typeof ref === 'function') ref(node);
|
||||
else if (ref) ref.current = node;
|
||||
},
|
||||
[ref],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
onKeyDown?.(e);
|
||||
if (e.defaultPrevented) return;
|
||||
|
||||
// CJK composition guard
|
||||
if (e.nativeEvent.isComposing) return;
|
||||
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
onSubmitRequest?.();
|
||||
// Trigger form submit
|
||||
const form = internalRef.current?.closest('form');
|
||||
if (form) {
|
||||
form.requestSubmit();
|
||||
}
|
||||
}
|
||||
},
|
||||
[onKeyDown, onSubmitRequest],
|
||||
);
|
||||
|
||||
return (
|
||||
<InputGroupTextarea
|
||||
ref={setRef}
|
||||
className={className}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
PromptInputTextarea.displayName = 'PromptInputTextarea';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PromptInputFooter
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type PromptInputFooterProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const PromptInputFooter = forwardRef<HTMLDivElement, PromptInputFooterProps>(
|
||||
({ className, ...props }, ref) => (
|
||||
<InputGroupAddon
|
||||
ref={ref}
|
||||
align="block-end"
|
||||
className={cn('gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
PromptInputFooter.displayName = 'PromptInputFooter';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PromptInputTools (left side of footer)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const PromptInputTools = forwardRef<HTMLDivElement, PromptInputToolsProps>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center gap-0.5', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
PromptInputTools.displayName = 'PromptInputTools';
|
||||
|
||||
export type PromptInputStatus = 'idle' | 'submitted' | 'streaming' | 'error';
|
||||
|
||||
export interface PromptInputSubmitProps extends ComponentProps<typeof InputGroupButton> {
|
||||
status?: PromptInputStatus;
|
||||
onStop?: () => void;
|
||||
}
|
||||
|
||||
export const PromptInputSubmit = forwardRef<HTMLButtonElement, PromptInputSubmitProps>(
|
||||
({ status = 'idle', onStop, className, disabled, ...props }, ref) => {
|
||||
const isRunning = status === 'submitted' || status === 'streaming';
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (isRunning && onStop) {
|
||||
onStop();
|
||||
}
|
||||
}, [isRunning, onStop]);
|
||||
|
||||
const icon =
|
||||
status === 'submitted' ? (
|
||||
<Spinner size={14} />
|
||||
) : status === 'streaming' ? (
|
||||
<Square size={14} />
|
||||
) : status === 'error' ? (
|
||||
<X size={14} />
|
||||
) : (
|
||||
<ArrowUp size={14} />
|
||||
);
|
||||
|
||||
const tooltipLabel =
|
||||
status === 'submitted'
|
||||
? 'Waiting...'
|
||||
: status === 'streaming'
|
||||
? 'Stop'
|
||||
: status === 'error'
|
||||
? 'Error'
|
||||
: 'Send';
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
ref={ref}
|
||||
type={isRunning ? 'button' : 'submit'}
|
||||
onClick={isRunning ? handleClick : undefined}
|
||||
aria-label={tooltipLabel}
|
||||
variant="ghost"
|
||||
disabled={disabled && !isRunning}
|
||||
className={cn(
|
||||
'h-8 w-8 rounded-full border p-0 shadow-sm disabled:opacity-100',
|
||||
isRunning
|
||||
? 'border-destructive/60 bg-destructive/85 text-destructive-foreground hover:bg-destructive'
|
||||
: disabled
|
||||
? 'border-border/80 bg-muted/52 text-foreground/72 hover:bg-muted/52'
|
||||
: 'border-foreground/20 bg-foreground text-background hover:bg-foreground/90',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">{tooltipLabel}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
PromptInputSubmit.displayName = 'PromptInputSubmit';
|
||||
Reference in New Issue
Block a user