feat: add shift-enter hint below chat composer

This commit is contained in:
lc6464 2026-06-11 15:44:45 +08:00 committed by Guoguo
parent 42ece2aceb
commit 890f4d6bcb
6 changed files with 129 additions and 109 deletions

View file

@ -70,6 +70,7 @@ export function ChatComposer({
const { t } = useTranslation() const { t } = useTranslation()
const canInput = inputDisabledReason === null const canInput = inputDisabledReason === null
const composingRef = useRef(false) const composingRef = useRef(false)
const hasInput = input.trim().length > 0
const disabledMessage = const disabledMessage =
inputDisabledReason === null inputDisabledReason === null
? null ? null
@ -96,120 +97,134 @@ export function ChatComposer({
return ( return (
<div className="before:bg-background pointer-events-none relative z-10 -mt-[24px] shrink-0 [scrollbar-gutter:stable] overflow-y-auto px-4 pb-[calc(1rem+env(safe-area-inset-bottom))] before:pointer-events-none before:absolute before:inset-x-0 before:top-[24px] before:bottom-0 before:content-[''] md:px-8 md:pb-8 lg:px-24 xl:px-48"> <div className="before:bg-background pointer-events-none relative z-10 -mt-[24px] shrink-0 [scrollbar-gutter:stable] overflow-y-auto px-4 pb-[calc(1rem+env(safe-area-inset-bottom))] before:pointer-events-none before:absolute before:inset-x-0 before:top-[24px] before:bottom-0 before:content-[''] md:px-8 md:pb-8 lg:px-24 xl:px-48">
<div <div className="pointer-events-auto mx-auto flex max-w-[1000px] flex-col items-end">
className={cn( <div
"bg-card border-border/60 pointer-events-auto relative mx-auto flex max-w-[1000px] flex-col rounded-2xl border p-3 shadow-sm transition-colors", className={cn(
isDragActive && "border-violet-400/70 bg-violet-500/5", "bg-card border-border/60 relative flex w-full flex-col rounded-2xl border p-3 shadow-sm transition-colors",
)} isDragActive && "border-violet-400/70 bg-violet-500/5",
onDragEnter={onDragEnter} )}
onDragLeave={onDragLeave} onDragEnter={onDragEnter}
onDragOver={onDragOver} onDragLeave={onDragLeave}
onDrop={onDrop} onDragOver={onDragOver}
> onDrop={onDrop}
{isDragActive && ( >
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-2xl border-2 border-dashed border-violet-400/70 bg-violet-500/10"> {isDragActive && (
<div className="bg-background/95 text-foreground rounded-full px-4 py-2 text-sm font-medium shadow-sm"> <div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-2xl border-2 border-dashed border-violet-400/70 bg-violet-500/10">
{t("chat.dropImagesActive")} <div className="bg-background/95 text-foreground rounded-full px-4 py-2 text-sm font-medium shadow-sm">
{t("chat.dropImagesActive")}
</div>
</div>
)}
{attachments.length > 0 && (
<div className="mb-3 flex flex-wrap gap-2 px-2">
{attachments.map((attachment, index) => (
<div
key={`${attachment.url}-${index}`}
className="bg-background relative h-20 w-20 overflow-hidden rounded-xl border"
>
<img
src={attachment.url}
alt={attachment.filename || t("chat.uploadedImage")}
className="h-full w-full object-cover"
/>
<button
type="button"
onClick={() => onRemoveAttachment(index)}
className="bg-background/85 text-foreground absolute top-1 right-1 inline-flex h-6 w-6 items-center justify-center rounded-full border shadow-sm transition hover:bg-white"
aria-label={t("chat.removeImage")}
title={t("chat.removeImage")}
>
<IconX className="h-3.5 w-3.5" />
</button>
</div>
))}
</div>
)}
<TextareaAutosize
value={input}
onChange={(e) => onInputChange(e.target.value)}
onCompositionStart={() => {
composingRef.current = true
}}
onCompositionEnd={() => {
composingRef.current = false
}}
onPaste={onPaste}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={!canInput}
title={disabledMessage || undefined}
className={cn(
"placeholder:text-muted-foreground/50 max-h-[200px] min-h-[64px] resize-none border-0 bg-transparent px-2 py-1 text-[15px] shadow-none transition-colors focus-visible:ring-0 focus-visible:outline-none dark:bg-transparent",
!canInput && "cursor-not-allowed",
)}
minRows={1}
maxRows={8}
/>
<div className="mt-2 flex items-center justify-between px-1">
<div className="flex items-center gap-1">
<Button
type="button"
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-foreground h-8 w-8 rounded-full"
onClick={onAddImages}
disabled={!canInput}
aria-label={t("chat.attachImage")}
title={t("chat.attachImage")}
>
<IconPhotoPlus className="size-4" />
</Button>
</div>
<div className="flex items-center gap-1.5">
{contextUsage && (
<ContextUsageRing
usage={contextUsage}
onDetailClick={onContextDetail}
/>
)}
{canInput ? (
<Tooltip delayDuration={700}>
<TooltipTrigger asChild>
<span tabIndex={!canSend ? 0 : undefined}>
<Button
type="button"
size="icon"
className="size-8 rounded-full bg-violet-500 text-white transition-transform hover:bg-violet-600 active:scale-95"
onClick={onSend}
disabled={!canSend}
aria-label={t("chat.sendMessage")}
>
<IconArrowUp className="size-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent
className="border-border/70 bg-muted text-foreground border text-center whitespace-pre-line shadow-lg shadow-black/10 dark:shadow-black/30"
arrowClassName="bg-muted fill-muted"
>
{t("chat.sendHint")}
</TooltipContent>
</Tooltip>
) : null}
</div> </div>
</div> </div>
)} </div>
{attachments.length > 0 && ( <div
<div className="mb-3 flex flex-wrap gap-2 px-2"> aria-hidden={!hasInput}
{attachments.map((attachment, index) => (
<div
key={`${attachment.url}-${index}`}
className="bg-background relative h-20 w-20 overflow-hidden rounded-xl border"
>
<img
src={attachment.url}
alt={attachment.filename || t("chat.uploadedImage")}
className="h-full w-full object-cover"
/>
<button
type="button"
onClick={() => onRemoveAttachment(index)}
className="bg-background/85 text-foreground absolute top-1 right-1 inline-flex h-6 w-6 items-center justify-center rounded-full border shadow-sm transition hover:bg-white"
aria-label={t("chat.removeImage")}
title={t("chat.removeImage")}
>
<IconX className="h-3.5 w-3.5" />
</button>
</div>
))}
</div>
)}
<TextareaAutosize
value={input}
onChange={(e) => onInputChange(e.target.value)}
onCompositionStart={() => {
composingRef.current = true
}}
onCompositionEnd={() => {
composingRef.current = false
}}
onPaste={onPaste}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={!canInput}
title={disabledMessage || undefined}
className={cn( className={cn(
"placeholder:text-muted-foreground/50 max-h-[200px] min-h-[64px] resize-none border-0 bg-transparent px-2 py-1 text-[15px] shadow-none transition-colors focus-visible:ring-0 focus-visible:outline-none dark:bg-transparent", "border-border/70 bg-muted text-foreground mt-2 inline-flex items-center rounded-md border px-3 py-1.5 text-xs shadow-lg shadow-black/10 transition-all duration-200 dark:shadow-black/30",
!canInput && "cursor-not-allowed", hasInput
? "translate-y-0 opacity-100"
: "pointer-events-none -translate-y-1 opacity-0",
)} )}
minRows={1} >
maxRows={8} {t("chat.composeHint")}
/>
<div className="mt-2 flex items-center justify-between px-1">
<div className="flex items-center gap-1">
<Button
type="button"
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-foreground h-8 w-8 rounded-full"
onClick={onAddImages}
disabled={!canInput}
aria-label={t("chat.attachImage")}
title={t("chat.attachImage")}
>
<IconPhotoPlus className="size-4" />
</Button>
</div>
<div className="flex items-center gap-1.5">
{contextUsage && (
<ContextUsageRing
usage={contextUsage}
onDetailClick={onContextDetail}
/>
)}
{canInput ? (
<Tooltip delayDuration={700}>
<TooltipTrigger asChild>
<span tabIndex={!canSend ? 0 : undefined}>
<Button
type="button"
size="icon"
className="size-8 rounded-full bg-violet-500 text-white transition-transform hover:bg-violet-600 active:scale-95"
onClick={onSend}
disabled={!canSend}
aria-label={t("chat.sendMessage")}
>
<IconArrowUp className="size-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent
className="border-border/70 bg-muted text-foreground border text-center whitespace-pre-line shadow-lg shadow-black/10 dark:shadow-black/30"
arrowClassName="bg-muted fill-muted"
>
{t("chat.sendHint")}
</TooltipContent>
</Tooltip>
) : null}
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -94,6 +94,7 @@
}, },
"sendMessage": "বার্তা পাঠান", "sendMessage": "বার্তা পাঠান",
"sendHint": "পাঠাতে Enter চাপুন\nনতুন লাইনের জন্য Shift + Enter", "sendHint": "পাঠাতে Enter চাপুন\nনতুন লাইনের জন্য Shift + Enter",
"composeHint": "পাঠাতে Enter চাপুন, নতুন লাইনের জন্য Shift + Enter",
"contextTitle": "প্রসঙ্গ", "contextTitle": "প্রসঙ্গ",
"contextDetail": "বিস্তারিত দেখুন", "contextDetail": "বিস্তারিত দেখুন",
"contextCompressAt": "Compress at", "contextCompressAt": "Compress at",

View file

@ -92,6 +92,7 @@
}, },
"sendMessage": "Odeslat zprávu", "sendMessage": "Odeslat zprávu",
"sendHint": "Enter pro odeslání\nShift + Enter pro nový řádek", "sendHint": "Enter pro odeslání\nShift + Enter pro nový řádek",
"composeHint": "Enter pro odeslání, Shift + Enter pro nový řádek",
"contextTitle": "Kontext", "contextTitle": "Kontext",
"contextDetail": "Zobrazit detail", "contextDetail": "Zobrazit detail",
"contextCompressAt": "Komprimovat při", "contextCompressAt": "Komprimovat při",

View file

@ -94,6 +94,7 @@
}, },
"sendMessage": "Send message", "sendMessage": "Send message",
"sendHint": "Press Enter to send\nShift + Enter for a new line", "sendHint": "Press Enter to send\nShift + Enter for a new line",
"composeHint": "Press Enter to send, Shift + Enter for a new line",
"contextTitle": "Context", "contextTitle": "Context",
"contextDetail": "View Details", "contextDetail": "View Details",
"contextCompressAt": "Compress at", "contextCompressAt": "Compress at",

View file

@ -94,6 +94,7 @@
}, },
"sendMessage": "Enviar mensagem", "sendMessage": "Enviar mensagem",
"sendHint": "Pressione Enter para enviar\nShift + Enter para nova linha", "sendHint": "Pressione Enter para enviar\nShift + Enter para nova linha",
"composeHint": "Pressione Enter para enviar, Shift + Enter para nova linha",
"contextTitle": "Contexto", "contextTitle": "Contexto",
"contextDetail": "Ver Detalhes", "contextDetail": "Ver Detalhes",
"contextCompressAt": "Comprimir em", "contextCompressAt": "Comprimir em",

View file

@ -94,6 +94,7 @@
}, },
"sendMessage": "发送消息", "sendMessage": "发送消息",
"sendHint": "按 Enter 发送\nShift + Enter 换行", "sendHint": "按 Enter 发送\nShift + Enter 换行",
"composeHint": "按 Enter 发送Shift + Enter 换行",
"contextTitle": "上下文", "contextTitle": "上下文",
"contextDetail": "查看详情", "contextDetail": "查看详情",
"contextCompressAt": "压缩阈值", "contextCompressAt": "压缩阈值",