Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

126 lines
4.7 KiB
TypeScript

import { useState, type CSSProperties } from "react";
import { Spinner } from "@tutti-os/ui-system";
import { ImageIcon, X } from "lucide-react";
import { ZoomableImage } from "../../../app/renderer/components/ZoomableImage";
import { cn } from "../../../app/renderer/lib/utils";
import type { AgentComposerDraftImage } from "../model/agentGuiNodeTypes";
import {
DRAFT_IMAGE_PREVIEW_BASE_HEIGHT_PX,
DRAFT_IMAGE_PREVIEW_MAX_RATIO,
DRAFT_IMAGE_PREVIEW_MAX_WIDTH_PX,
DRAFT_IMAGE_PREVIEW_MIN_RATIO,
DRAFT_IMAGE_PREVIEW_MIN_WIDTH_PX
} from "./AgentComposerChrome";
export function AgentComposerDraftImagePreview({
image,
removeLabel,
onRemove
}: {
image: AgentComposerDraftImage;
removeLabel: string;
onRemove: (id: string) => void;
}): React.JSX.Element {
const [aspectRatio, setAspectRatio] = useState(1);
const previewWidth = Math.round(
Math.min(
DRAFT_IMAGE_PREVIEW_MAX_WIDTH_PX,
Math.max(
DRAFT_IMAGE_PREVIEW_MIN_WIDTH_PX,
aspectRatio * DRAFT_IMAGE_PREVIEW_BASE_HEIGHT_PX
)
)
);
const previewStyle = {
aspectRatio: String(aspectRatio),
width: `${previewWidth}px`
} satisfies CSSProperties;
return (
<div
className={cn(
"group relative min-w-0 overflow-hidden rounded-[6px] border border-[var(--line-1)] bg-[var(--background-fronted)]",
"[&>[data-rmiz]]:block [&>[data-rmiz]]:size-full",
"[&>[data-rmiz]>[data-rmiz-content]]:block [&>[data-rmiz]>[data-rmiz-content]]:size-full",
image.uploadError &&
"border-[color:color-mix(in_srgb,var(--danger)_55%,var(--line-1))]"
)}
data-testid="agent-gui-composer-image-draft"
data-uploading={image.uploading ? "true" : undefined}
data-upload-error={image.uploadError ? "true" : undefined}
style={previewStyle}
>
{image.previewUrl ? (
<ZoomableImage
src={image.previewUrl}
alt={image.name}
className="size-full object-contain"
draggable={false}
downloadName={image.name || "image.png"}
onLoad={(event) => {
const element = event.currentTarget;
const width = element.naturalWidth;
const height = element.naturalHeight;
if (width <= 0 || height <= 0) {
return;
}
const nextRatio = Math.min(
DRAFT_IMAGE_PREVIEW_MAX_RATIO,
Math.max(DRAFT_IMAGE_PREVIEW_MIN_RATIO, width / height)
);
setAspectRatio(nextRatio);
}}
/>
) : (
<div
className="grid size-full place-items-center text-[var(--text-tertiary)]"
data-testid="agent-gui-composer-image-preview-pending"
>
<ImageIcon aria-hidden size={22} strokeWidth={1.8} />
</div>
)}
{image.uploading ? (
<div
className="absolute inset-0 grid place-items-center bg-[color-mix(in_srgb,var(--background-fronted)_62%,transparent)]"
data-testid="agent-gui-composer-image-uploading"
>
<Spinner
className="text-[var(--text-primary)]"
size={18}
strokeWidth={2.4}
trackColor="var(--transparency-hover)"
testId="agent-gui-composer-image-upload-spinner"
/>
</div>
) : null}
<button
type="button"
className="absolute right-1 top-1 z-[2] inline-flex size-5 items-center justify-center rounded-full border border-[color-mix(in_srgb,var(--text-primary)_16%,transparent)] bg-[color-mix(in_srgb,var(--background-fronted)_88%,transparent)] text-[var(--text-primary)] opacity-90 shadow-sm transition hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:color-mix(in_srgb,var(--text-primary)_34%,transparent)]"
aria-label={removeLabel}
title={removeLabel}
onClick={() => onRemove(image.id)}
>
<X size={12} strokeWidth={2.4} aria-hidden />
</button>
</div>
);
}
export function SendFilledIcon(): React.JSX.Element {
"use memo";
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<path
d="M2.74311 8.80587C2.84592 8.40096 3.14571 8.08844 3.54551 7.97033L18.5197 3.51569C18.9336 3.39383 19.3809 3.5054 19.6881 3.81262C19.9951 4.11984 20.1076 4.56798 19.9857 4.9817L15.5311 19.9559C15.413 20.3557 15.1005 20.6555 14.6956 20.7583C14.2895 20.8597 13.869 20.7438 13.5721 20.4469L10.455 15.1823C10.8585 14.6483 12.1563 12.9094 14.3475 9.96528C14.6086 9.70419 14.6382 9.31168 14.4138 9.08692C14.1891 8.86221 13.796 8.8913 13.5348 9.15252L8.31088 13.0423L3.05316 9.92799C2.7562 9.63104 2.64049 9.21071 2.74311 8.80587Z"
fill="currentColor"
/>
</svg>
);
}