mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-17 04:15:05 +00:00
feat(ui): add image download overlay, improve agent selector defaults
- Add hover download button with filename/size on chat image thumbnails - Add download button and file info bar to image lightbox overlay - Add hover download button on inline markdown images - Increase agent selector dropdown height on desktop (sm:max-h-80) - Select default agent (is_default) instead of first in list - Increase session title generation timeout to 120s
This commit is contained in:
1 parent
394ded7836
commit
e2c131c57d
5 files changed
+100
-38
No files matched your search
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
)
|
||||
|
||||
const titleGenerateTimeout = 30 * time.Second
|
||||
const titleGenerateTimeout = 120 * time.Second
|
||||
|
||||
const titleSystemPrompt = `Generate a short title (max 15 words) for this conversation based on the user's message. Reply with only the title, no quotes or punctuation wrapping.`
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@ export function AgentSelector({ value, onChange }: AgentSelectorProps) {
|
||||
const active = (res.agents ?? []).filter((a) => a.status === "active");
|
||||
setAgents(active);
|
||||
if (active.length > 0 && !active.some((a) => a.agent_key === value)) {
|
||||
onChange(active[0]!.agent_key);
|
||||
const defaultAgent = active.find((a) => a.is_default) ?? active[0]!;
|
||||
onChange(defaultAgent.agent_key);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
@@ -92,7 +93,7 @@ export function AgentSelector({ value, onChange }: AgentSelectorProps) {
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
style={dropdownStyle}
|
||||
className="pointer-events-auto max-h-60 overflow-y-auto rounded-lg border bg-popover p-1 shadow-md"
|
||||
className="pointer-events-auto max-h-60 sm:max-h-80 overflow-y-auto rounded-lg border bg-popover p-1 shadow-md"
|
||||
>
|
||||
{agents.length === 0 && (
|
||||
<div className="px-3 py-2 text-sm text-muted-foreground">
|
||||
|
||||
@@ -72,19 +72,38 @@ export function MediaGallery({ items }: MediaGalleryProps) {
|
||||
{images.length > 0 && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{images.map((item, i) => (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
onClick={() => setLightboxIdx(i)}
|
||||
className="relative overflow-hidden rounded-lg border cursor-pointer"
|
||||
>
|
||||
<img
|
||||
src={item.path}
|
||||
alt={item.fileName ?? ""}
|
||||
className="h-40 w-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
<div key={i} className="group relative overflow-hidden rounded-lg border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLightboxIdx(i)}
|
||||
className="block w-full cursor-pointer"
|
||||
>
|
||||
<img
|
||||
src={item.path}
|
||||
alt={item.fileName ?? ""}
|
||||
className="h-40 w-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
<div className="absolute inset-x-0 bottom-0 flex items-end justify-between px-2 pb-1.5 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<div className="flex min-w-0 flex-col text-xs text-white drop-shadow-sm">
|
||||
{item.fileName && <span className="truncate">{item.fileName}</span>}
|
||||
{item.size != null && item.size > 0 && (
|
||||
<span className="text-white/70">{formatSize(item.size)}</span>
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
href={item.path}
|
||||
download={item.fileName ?? "image"}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="shrink-0 rounded-md bg-black/60 p-1 text-white shadow-md hover:bg-black/80 transition-colors cursor-pointer"
|
||||
title="Download"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -121,6 +140,8 @@ export function MediaGallery({ items }: MediaGalleryProps) {
|
||||
<ImageLightbox
|
||||
src={images[lightboxIdx]!.path}
|
||||
alt={images[lightboxIdx]!.fileName ?? ""}
|
||||
fileName={images[lightboxIdx]!.fileName}
|
||||
size={images[lightboxIdx]!.size}
|
||||
onClose={() => setLightboxIdx(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { X, Download } from "lucide-react";
|
||||
import { formatSize } from "@/lib/file-helpers";
|
||||
|
||||
interface ImageLightboxProps {
|
||||
src: string;
|
||||
alt?: string;
|
||||
fileName?: string;
|
||||
size?: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ImageLightbox({ src, alt, onClose }: ImageLightboxProps) {
|
||||
export function ImageLightbox({ src, alt, fileName, size, onClose }: ImageLightboxProps) {
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
@@ -20,24 +23,47 @@ export function ImageLightbox({ src, alt, onClose }: ImageLightboxProps) {
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
const displayName = fileName || alt || "image";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm"
|
||||
className="fixed inset-0 z-[100] flex flex-col items-center justify-center bg-black/80 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 rounded-full bg-black/50 p-2 text-white hover:bg-black/70 transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
<div className="absolute top-4 right-4 flex items-center gap-2">
|
||||
<a
|
||||
href={src}
|
||||
download={displayName}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="rounded-full bg-black/50 p-2 text-white hover:bg-black/70 transition-colors cursor-pointer"
|
||||
title="Download"
|
||||
>
|
||||
<Download className="h-5 w-5" />
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-full bg-black/50 p-2 text-white hover:bg-black/70 transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<img
|
||||
src={src}
|
||||
alt={alt ?? "image"}
|
||||
className="max-h-[90vh] max-w-[90vw] rounded-lg object-contain"
|
||||
className="max-h-[85vh] max-w-[90vw] rounded-lg object-contain"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
{(fileName || (size != null && size > 0)) && (
|
||||
<div
|
||||
className="mt-3 flex items-center gap-2 rounded-full bg-black/60 px-4 py-1.5 text-sm text-white/90"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{fileName && <span className="max-w-[300px] truncate">{fileName}</span>}
|
||||
{fileName && size != null && size > 0 && <span className="text-white/50">·</span>}
|
||||
{size != null && size > 0 && <span className="text-white/60">{formatSize(size)}</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -166,18 +166,32 @@ export function MarkdownRenderer({ content, className }: MarkdownRendererProps)
|
||||
},
|
||||
img({ src, alt, ...props }) {
|
||||
const resolvedSrc = isFileLink(src) ? toFileUrl(src!, token) : src;
|
||||
const displayName = alt || fileNameFromHref(src ?? "");
|
||||
return (
|
||||
<img
|
||||
src={resolvedSrc}
|
||||
alt={alt ?? "image"}
|
||||
className="max-w-sm rounded-lg border shadow-sm cursor-pointer hover:opacity-90 transition-opacity"
|
||||
loading="lazy"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (resolvedSrc) openLightbox(resolvedSrc, alt ?? "image");
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
<span className="group/img relative inline-block overflow-hidden rounded-lg border shadow-sm">
|
||||
<img
|
||||
src={resolvedSrc}
|
||||
alt={alt ?? "image"}
|
||||
className="block max-w-sm cursor-pointer hover:opacity-90 transition-opacity"
|
||||
loading="lazy"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (resolvedSrc) openLightbox(resolvedSrc, alt ?? "image");
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
{resolvedSrc && (
|
||||
<a
|
||||
href={resolvedSrc}
|
||||
download={displayName}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="absolute top-1.5 right-1.5 flex items-center justify-center rounded-md bg-black/60 p-1 text-white shadow-md opacity-0 transition-opacity group-hover/img:opacity-100 hover:bg-black/80 cursor-pointer"
|
||||
title="Download"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
table({ children, ...props }) {
|
||||
|
||||
Reference in new issue
Block a user