feat: add shift-enter hint below chat composer
This commit is contained in:
parent
42ece2aceb
commit
890f4d6bcb
6 changed files with 129 additions and 109 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@
|
||||||
},
|
},
|
||||||
"sendMessage": "发送消息",
|
"sendMessage": "发送消息",
|
||||||
"sendHint": "按 Enter 发送\nShift + Enter 换行",
|
"sendHint": "按 Enter 发送\nShift + Enter 换行",
|
||||||
|
"composeHint": "按 Enter 发送,Shift + Enter 换行",
|
||||||
"contextTitle": "上下文",
|
"contextTitle": "上下文",
|
||||||
"contextDetail": "查看详情",
|
"contextDetail": "查看详情",
|
||||||
"contextCompressAt": "压缩阈值",
|
"contextCompressAt": "压缩阈值",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue