mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-07-28 01:27:58 +00:00
feat: add shift-enter hint below chat composer
This commit is contained in:
@@ -70,6 +70,7 @@ export function ChatComposer({
|
||||
const { t } = useTranslation()
|
||||
const canInput = inputDisabledReason === null
|
||||
const composingRef = useRef(false)
|
||||
const hasInput = input.trim().length > 0
|
||||
const disabledMessage =
|
||||
inputDisabledReason === null
|
||||
? null
|
||||
@@ -96,120 +97,134 @@ export function ChatComposer({
|
||||
|
||||
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={cn(
|
||||
"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",
|
||||
isDragActive && "border-violet-400/70 bg-violet-500/5",
|
||||
)}
|
||||
onDragEnter={onDragEnter}
|
||||
onDragLeave={onDragLeave}
|
||||
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">
|
||||
<div className="bg-background/95 text-foreground rounded-full px-4 py-2 text-sm font-medium shadow-sm">
|
||||
{t("chat.dropImagesActive")}
|
||||
<div className="pointer-events-auto mx-auto flex max-w-[1000px] flex-col items-end">
|
||||
<div
|
||||
className={cn(
|
||||
"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}
|
||||
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">
|
||||
<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>
|
||||
|
||||
{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}
|
||||
<div
|
||||
aria-hidden={!hasInput}
|
||||
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",
|
||||
"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",
|
||||
hasInput
|
||||
? "translate-y-0 opacity-100"
|
||||
: "pointer-events-none -translate-y-1 opacity-0",
|
||||
)}
|
||||
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>
|
||||
>
|
||||
{t("chat.composeHint")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
},
|
||||
"sendMessage": "বার্তা পাঠান",
|
||||
"sendHint": "পাঠাতে Enter চাপুন\nনতুন লাইনের জন্য Shift + Enter",
|
||||
"composeHint": "পাঠাতে Enter চাপুন, নতুন লাইনের জন্য Shift + Enter",
|
||||
"contextTitle": "প্রসঙ্গ",
|
||||
"contextDetail": "বিস্তারিত দেখুন",
|
||||
"contextCompressAt": "Compress at",
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
},
|
||||
"sendMessage": "Odeslat zprávu",
|
||||
"sendHint": "Enter pro odeslání\nShift + Enter pro nový řádek",
|
||||
"composeHint": "Enter pro odeslání, Shift + Enter pro nový řádek",
|
||||
"contextTitle": "Kontext",
|
||||
"contextDetail": "Zobrazit detail",
|
||||
"contextCompressAt": "Komprimovat při",
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
},
|
||||
"sendMessage": "Send message",
|
||||
"sendHint": "Press Enter to send\nShift + Enter for a new line",
|
||||
"composeHint": "Press Enter to send, Shift + Enter for a new line",
|
||||
"contextTitle": "Context",
|
||||
"contextDetail": "View Details",
|
||||
"contextCompressAt": "Compress at",
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
},
|
||||
"sendMessage": "Enviar mensagem",
|
||||
"sendHint": "Pressione Enter para enviar\nShift + Enter para nova linha",
|
||||
"composeHint": "Pressione Enter para enviar, Shift + Enter para nova linha",
|
||||
"contextTitle": "Contexto",
|
||||
"contextDetail": "Ver Detalhes",
|
||||
"contextCompressAt": "Comprimir em",
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
},
|
||||
"sendMessage": "发送消息",
|
||||
"sendHint": "按 Enter 发送\nShift + Enter 换行",
|
||||
"composeHint": "按 Enter 发送,Shift + Enter 换行",
|
||||
"contextTitle": "上下文",
|
||||
"contextDetail": "查看详情",
|
||||
"contextCompressAt": "压缩阈值",
|
||||
|
||||
Reference in New Issue
Block a user