@chai-ui/react 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +6 -1
- package/README.md +79 -17
- package/dist/Composer.d.ts +14 -3
- package/dist/Composer.d.ts.map +1 -1
- package/dist/Composer.js +50 -25
- package/dist/Composer.js.map +1 -1
- package/dist/EditCard.d.ts +14 -2
- package/dist/EditCard.d.ts.map +1 -1
- package/dist/EditCard.js +18 -4
- package/dist/EditCard.js.map +1 -1
- package/dist/MediaAnalyzer.d.ts +12 -5
- package/dist/MediaAnalyzer.d.ts.map +1 -1
- package/dist/MediaAnalyzer.js +26 -8
- package/dist/MediaAnalyzer.js.map +1 -1
- package/dist/ResultCard.d.ts.map +1 -1
- package/dist/ResultCard.js +10 -6
- package/dist/ResultCard.js.map +1 -1
- package/dist/icons.d.ts +1 -0
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +9 -0
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/media-actions.d.ts +5 -2
- package/dist/media-actions.d.ts.map +1 -1
- package/dist/media-actions.js +12 -3
- package/dist/media-actions.js.map +1 -1
- package/dist/style.css +16 -1
- package/dist/useComposer.d.ts.map +1 -1
- package/dist/useComposer.js +6 -2
- package/dist/useComposer.js.map +1 -1
- package/package.json +3 -3
- package/src/Composer.tsx +94 -41
- package/src/EditCard.tsx +49 -6
- package/src/MediaAnalyzer.tsx +48 -15
- package/src/ResultCard.tsx +12 -4
- package/src/icons.tsx +14 -0
- package/src/index.ts +10 -0
- package/src/media-actions.ts +13 -4
- package/src/style.css +16 -1
- package/src/useComposer.ts +6 -2
package/src/EditCard.tsx
CHANGED
|
@@ -44,8 +44,20 @@ export interface EditCardProps {
|
|
|
44
44
|
onRegionsChange: (next: EditRegion[]) => void;
|
|
45
45
|
/** The most regions the chosen model takes (`ModelOption.maxRegions`). "New region" is disabled at this many. Defaults to 6. */
|
|
46
46
|
maxRegions?: number;
|
|
47
|
-
/**
|
|
47
|
+
/** A fixed starting width in px, fitted within `maxWidth` and `maxHeight`. Without it (and without `scale`), the card is as large as fits in that box. */
|
|
48
48
|
width?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Sizes the card from the image's own pixel width: `0.3` makes it 30% as
|
|
51
|
+
* wide as the image (a 5000px photo gives a 1500px card). The height
|
|
52
|
+
* follows the image's shape, and the card is never wider than its
|
|
53
|
+
* container. Each version is measured on its own, since an edit can come
|
|
54
|
+
* back a different size.
|
|
55
|
+
*/
|
|
56
|
+
scale?: number;
|
|
57
|
+
/** The widest the card gets, in px. Defaults to 630. With no `width` or `scale`, the card fills `maxWidth` × `maxHeight`, keeping the image's shape. */
|
|
58
|
+
maxWidth?: number;
|
|
59
|
+
/** The tallest the card gets, in px. Defaults to 630. A tall image narrows the card to fit, keeping its shape. */
|
|
60
|
+
maxHeight?: number;
|
|
49
61
|
/** Alt text for the image. Defaults to "Image being edited". */
|
|
50
62
|
alt?: string;
|
|
51
63
|
/** Fires after the card downloads or shares the version showing. */
|
|
@@ -131,6 +143,9 @@ export function EditCard({
|
|
|
131
143
|
onRegionsChange,
|
|
132
144
|
maxRegions = DEFAULT_MAX_REGIONS,
|
|
133
145
|
width,
|
|
146
|
+
scale,
|
|
147
|
+
maxWidth = 630,
|
|
148
|
+
maxHeight = 630,
|
|
134
149
|
alt = "Image being edited",
|
|
135
150
|
onAction,
|
|
136
151
|
disabled = false,
|
|
@@ -149,7 +164,11 @@ export function EditCard({
|
|
|
149
164
|
const [zoom, setZoom] = useState(1);
|
|
150
165
|
const [pan, setPan] = useState<Pan>({ x: 0, y: 0 });
|
|
151
166
|
const [size, setSize] = useState<Size>({ width: 0, height: 0 });
|
|
167
|
+
// The shown image's own pixel size, for `scale` and the max height. Kept
|
|
168
|
+
// until the next version's image loads, so the card doesn't jump between.
|
|
169
|
+
const [natural, setNatural] = useState<{ width: number; height: number } | null>(null);
|
|
152
170
|
const [expanded, setExpanded] = useState(false);
|
|
171
|
+
const [linkCopied, setLinkCopied] = useState(false);
|
|
153
172
|
const drag = useRef<Drag | null>(null);
|
|
154
173
|
const viewportRef = useRef<HTMLDivElement>(null);
|
|
155
174
|
const fieldRef = useRef<HTMLTextAreaElement>(null);
|
|
@@ -353,7 +372,11 @@ export function EditCard({
|
|
|
353
372
|
|
|
354
373
|
const handleShare = async () => {
|
|
355
374
|
if (!version?.src) return;
|
|
356
|
-
|
|
375
|
+
// With no share sheet the link is copied; say so, or the click looks like nothing happened.
|
|
376
|
+
if ((await shareMedia(version.src)) === "copied") {
|
|
377
|
+
setLinkCopied(true);
|
|
378
|
+
setTimeout(() => setLinkCopied(false), 1500);
|
|
379
|
+
}
|
|
357
380
|
onAction?.("share", version);
|
|
358
381
|
};
|
|
359
382
|
|
|
@@ -379,11 +402,18 @@ export function EditCard({
|
|
|
379
402
|
|
|
380
403
|
if (!version) return null;
|
|
381
404
|
|
|
405
|
+
// The starting width (from `scale`, else `width`), then fitted inside
|
|
406
|
+
// maxWidth × maxHeight with the image's shape kept.
|
|
407
|
+
// With neither, the card fills the box.
|
|
408
|
+
const baseWidth = scale != null && natural ? natural.width * scale : (width ?? maxWidth);
|
|
409
|
+
const heightCap = natural && natural.height > 0 ? (maxHeight * natural.width) / natural.height : Infinity;
|
|
410
|
+
const cardWidth = Math.round(Math.min(baseWidth, maxWidth, heightCap));
|
|
411
|
+
|
|
382
412
|
return (
|
|
383
413
|
<>
|
|
384
414
|
<div
|
|
385
415
|
className="chai-edit-card"
|
|
386
|
-
style={
|
|
416
|
+
style={{ "--chai-edit-card-width": `${cardWidth}px` } as CSSProperties}
|
|
387
417
|
>
|
|
388
418
|
<div
|
|
389
419
|
ref={viewportRef}
|
|
@@ -403,7 +433,13 @@ export function EditCard({
|
|
|
403
433
|
style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`, "--chai-edit-zoom": zoom } as CSSProperties}
|
|
404
434
|
>
|
|
405
435
|
{version.src || version.from ? (
|
|
406
|
-
<img
|
|
436
|
+
<img
|
|
437
|
+
className="chai-edit-card__image"
|
|
438
|
+
src={version.src ?? version.from}
|
|
439
|
+
alt={alt}
|
|
440
|
+
draggable={false}
|
|
441
|
+
onLoad={(e) => setNatural({ width: e.currentTarget.naturalWidth, height: e.currentTarget.naturalHeight })}
|
|
442
|
+
/>
|
|
407
443
|
) : (
|
|
408
444
|
<div className="chai-edit-card__placeholder" />
|
|
409
445
|
)}
|
|
@@ -478,8 +514,15 @@ export function EditCard({
|
|
|
478
514
|
<button type="button" className="chai-card-top-btn" aria-label="Download" title="Download" disabled={disabled || !ready} onClick={handleDownload}>
|
|
479
515
|
<DownloadIcon />
|
|
480
516
|
</button>
|
|
481
|
-
<button
|
|
482
|
-
|
|
517
|
+
<button
|
|
518
|
+
type="button"
|
|
519
|
+
className="chai-card-top-btn"
|
|
520
|
+
aria-label={linkCopied ? "Link copied" : "Share"}
|
|
521
|
+
title={linkCopied ? "Link copied" : "Share"}
|
|
522
|
+
disabled={disabled || !ready}
|
|
523
|
+
onClick={handleShare}
|
|
524
|
+
>
|
|
525
|
+
{linkCopied ? <CheckSymbolIcon /> : <ShareIcon />}
|
|
483
526
|
</button>
|
|
484
527
|
<button type="button" className="chai-card-top-btn" aria-label="Expand" title="Expand" disabled={!ready} onClick={() => setExpanded(true)}>
|
|
485
528
|
<ExpandIcon />
|
package/src/MediaAnalyzer.tsx
CHANGED
|
@@ -2,9 +2,11 @@ import { createComponent } from "@lit/react";
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { useEffect, useRef, useState, type ChangeEvent, type DragEvent } from "react";
|
|
4
4
|
import { MdFab } from "@material/web/fab/fab.js";
|
|
5
|
+
import { SUGGESTED_ANALYSIS_MODELS } from "@chai-ui/core";
|
|
5
6
|
import type { DroppedMedia, ModelOption, MediaKind } from "@chai-ui/core";
|
|
6
7
|
import type { MediaAnalysisPromptLength } from "@chai-ui/core";
|
|
7
8
|
import { SearchMenu, type SearchMenuSection } from "./primitives/SearchMenu.js";
|
|
9
|
+
import { StopIcon } from "./icons.js";
|
|
8
10
|
|
|
9
11
|
/** See primitives/Toggle.tsx for why `createComponent` is used instead of raw JSX on custom-element tags. */
|
|
10
12
|
const MdFabElement = createComponent({
|
|
@@ -113,10 +115,15 @@ export interface MediaAnalyzerProps {
|
|
|
113
115
|
onAttachmentsChange: (next: MediaAnalyzerAttachment[]) => void;
|
|
114
116
|
onUnsupportedFile?: (fileName: string) => void;
|
|
115
117
|
maxAttachmentsByKind?: Record<MediaKind, number>;
|
|
116
|
-
/**
|
|
117
|
-
models
|
|
118
|
-
/**
|
|
119
|
-
|
|
118
|
+
/** Fallback roster for a kind `modelsByKind` doesn't list. Optional. */
|
|
119
|
+
models?: ModelOption[];
|
|
120
|
+
/**
|
|
121
|
+
* Per-kind rosters — once a kind is known, only that kind's models show.
|
|
122
|
+
* Before that, all three show as separate sections (reference: Figma "06
|
|
123
|
+
* Composer from Media Analysis"). Defaults to Chai's suggested OpenRouter
|
|
124
|
+
* models (`SUGGESTED_ANALYSIS_MODELS`); pass your own to replace them.
|
|
125
|
+
*/
|
|
126
|
+
modelsByKind?: Partial<Record<MediaKind, ModelOption[]>>;
|
|
120
127
|
/**
|
|
121
128
|
* The picked model. While `autoSelectModel` is on it's `null` until a
|
|
122
129
|
* submit routes, then the routed model, shown by name on the trigger.
|
|
@@ -154,6 +161,8 @@ export interface MediaAnalyzerProps {
|
|
|
154
161
|
submitting?: boolean;
|
|
155
162
|
submitError?: string | null;
|
|
156
163
|
onSubmit: (payload: MediaAnalyzerSubmitPayload) => void;
|
|
164
|
+
/** Fires when the stop button is pressed while `submitting`. With it, submit turns into stop during an analysis (wire it to `useMediaAnalyzer`'s `cancel`); without it, submit is just disabled. */
|
|
165
|
+
onAbort?: () => void;
|
|
157
166
|
}
|
|
158
167
|
|
|
159
168
|
const KIND_LABEL: Record<MediaKind, string> = {
|
|
@@ -175,8 +184,8 @@ export function MediaAnalyzer({
|
|
|
175
184
|
onAttachmentsChange,
|
|
176
185
|
onUnsupportedFile,
|
|
177
186
|
maxAttachmentsByKind = DEFAULT_MAX_ATTACHMENTS_BY_KIND,
|
|
178
|
-
models,
|
|
179
|
-
modelsByKind,
|
|
187
|
+
models = [],
|
|
188
|
+
modelsByKind = SUGGESTED_ANALYSIS_MODELS,
|
|
180
189
|
modelId,
|
|
181
190
|
onModelChange,
|
|
182
191
|
autoSelectModel,
|
|
@@ -188,6 +197,7 @@ export function MediaAnalyzer({
|
|
|
188
197
|
submitting,
|
|
189
198
|
submitError,
|
|
190
199
|
onSubmit,
|
|
200
|
+
onAbort,
|
|
191
201
|
}: MediaAnalyzerProps) {
|
|
192
202
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
193
203
|
const kind = attachments[0]?.kind;
|
|
@@ -199,6 +209,9 @@ export function MediaAnalyzer({
|
|
|
199
209
|
// children. Active for as long as this is > 0; both drag handlers below
|
|
200
210
|
// clamp at 0 so a stray extra `dragleave` can't go negative.
|
|
201
211
|
const [dragDepth, setDragDepth] = useState(0);
|
|
212
|
+
// What the last attach replaced (one kind per analysis), shown until the
|
|
213
|
+
// attachments change some other way.
|
|
214
|
+
const [replaced, setReplaced] = useState<{ note: string; for: MediaAnalyzerAttachment[] } | null>(null);
|
|
202
215
|
const isDragActive = dragDepth > 0;
|
|
203
216
|
|
|
204
217
|
/**
|
|
@@ -221,9 +234,14 @@ export function MediaAnalyzer({
|
|
|
221
234
|
const settle = () => {
|
|
222
235
|
pending -= 1;
|
|
223
236
|
if (pending === 0 && resolved.length > 0) {
|
|
224
|
-
|
|
225
|
-
|
|
237
|
+
const next = resolved.reduce(
|
|
238
|
+
(acc, media) => nextMediaAnalyzerAttachments(acc, media, maxAttachmentsByKind),
|
|
239
|
+
attachments
|
|
226
240
|
);
|
|
241
|
+
const removed = attachments.filter((a) => !next.some((n) => n.id === a.id));
|
|
242
|
+
const added = next.filter((n) => !attachments.some((a) => a.id === n.id));
|
|
243
|
+
setReplaced(removed.length > 0 && added.length > 0 ? { note: replacedNote(removed, added), for: next } : null);
|
|
244
|
+
onAttachmentsChange(next);
|
|
227
245
|
}
|
|
228
246
|
};
|
|
229
247
|
files.forEach((file) => {
|
|
@@ -278,6 +296,7 @@ export function MediaAnalyzer({
|
|
|
278
296
|
const selectedLength = promptLengthOptions.find((o) => o.value === promptLength);
|
|
279
297
|
const hasContent = attachments.length > 0;
|
|
280
298
|
const isSubmitDisabled = disabled || !hasContent;
|
|
299
|
+
const showStop = Boolean(submitting && onAbort);
|
|
281
300
|
|
|
282
301
|
return (
|
|
283
302
|
<div className="chai-media-analyzer">
|
|
@@ -338,6 +357,12 @@ export function MediaAnalyzer({
|
|
|
338
357
|
</div>
|
|
339
358
|
)}
|
|
340
359
|
|
|
360
|
+
{replaced && replaced.for === attachments && (
|
|
361
|
+
<p className="chai-media-analyzer__note" role="status">
|
|
362
|
+
{replaced.note}
|
|
363
|
+
</p>
|
|
364
|
+
)}
|
|
365
|
+
|
|
341
366
|
{submitError && (
|
|
342
367
|
<p className="chai-media-analyzer__error" role="alert">
|
|
343
368
|
{submitError}
|
|
@@ -390,14 +415,16 @@ export function MediaAnalyzer({
|
|
|
390
415
|
size="small"
|
|
391
416
|
variant="secondary"
|
|
392
417
|
label=""
|
|
393
|
-
data-ready={!isSubmitDisabled || undefined}
|
|
418
|
+
data-ready={showStop || !isSubmitDisabled || undefined}
|
|
394
419
|
data-error={Boolean(submitError) || undefined}
|
|
420
|
+
data-stop={showStop || undefined}
|
|
395
421
|
{...({
|
|
396
|
-
ariaLabel: submitError ? "Retry" : "Analyze media",
|
|
397
|
-
ariaDisabled: submitting ? "true" : isSubmitDisabled ? "true" : "false",
|
|
422
|
+
ariaLabel: showStop ? "Stop" : submitError ? "Retry" : "Analyze media",
|
|
423
|
+
ariaDisabled: showStop ? "false" : submitting ? "true" : isSubmitDisabled ? "true" : "false",
|
|
398
424
|
} as Record<string, unknown>)}
|
|
399
425
|
onClick={() => {
|
|
400
|
-
if (
|
|
426
|
+
if (showStop) onAbort?.();
|
|
427
|
+
else if (!isSubmitDisabled && !submitting) onSubmit({
|
|
401
428
|
attachments,
|
|
402
429
|
modelId,
|
|
403
430
|
autoSelectModel,
|
|
@@ -406,9 +433,7 @@ export function MediaAnalyzer({
|
|
|
406
433
|
});
|
|
407
434
|
}}
|
|
408
435
|
>
|
|
409
|
-
<span slot="icon">
|
|
410
|
-
<ArrowForwardIcon />
|
|
411
|
-
</span>
|
|
436
|
+
<span slot="icon">{showStop ? <StopIcon /> : <ArrowForwardIcon />}</span>
|
|
412
437
|
</MdFabElement>
|
|
413
438
|
</div>
|
|
414
439
|
</div>
|
|
@@ -447,6 +472,14 @@ function MediaAnalyzerThumbnail({
|
|
|
447
472
|
// Same "trace and inline as a currentColor component" convention Composer.tsx
|
|
448
473
|
// documents — icons aren't shared across files yet in this codebase.
|
|
449
474
|
|
|
475
|
+
/** "Replaced 2 images with clip.wav." */
|
|
476
|
+
function replacedNote(removed: MediaAnalyzerAttachment[], added: MediaAnalyzerAttachment[]): string {
|
|
477
|
+
const name = (a: MediaAnalyzerAttachment) => a.name ?? KIND_LABEL[a.kind].toLowerCase();
|
|
478
|
+
const describe = (list: MediaAnalyzerAttachment[]) =>
|
|
479
|
+
list.length === 1 ? name(list[0]!) : `${list.length} ${KIND_LABEL[list[0]!.kind].toLowerCase()}s`;
|
|
480
|
+
return `Replaced ${describe(removed)} with ${describe(added)}.`;
|
|
481
|
+
}
|
|
482
|
+
|
|
450
483
|
function ArrowForwardIcon() {
|
|
451
484
|
return (
|
|
452
485
|
<svg viewBox="0 -960 960 960" aria-hidden="true">
|
package/src/ResultCard.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
|
|
2
2
|
import { ROUTING_MODEL_ID, type ModelOption, type Result } from "@chai-ui/core";
|
|
3
3
|
import { Pagination } from "./primitives/Pagination.js";
|
|
4
|
-
import { DownloadIcon, ExpandIcon, ShareIcon } from "./icons.js";
|
|
4
|
+
import { CheckSymbolIcon, DownloadIcon, ExpandIcon, ShareIcon } from "./icons.js";
|
|
5
5
|
import { downloadFilename, downloadMedia, shareMedia } from "./media-actions.js";
|
|
6
6
|
import { PageSlide, usePageSlide } from "./page-slide.js";
|
|
7
7
|
|
|
@@ -125,6 +125,7 @@ export function ResultCard({
|
|
|
125
125
|
const [flipped, setFlipped] = useState(false);
|
|
126
126
|
const [moreOpen, setMoreOpen] = useState(false);
|
|
127
127
|
const [copied, setCopied] = useState(false);
|
|
128
|
+
const [linkCopied, setLinkCopied] = useState(false);
|
|
128
129
|
const [mediaExpanded, setMediaExpanded] = useState(false);
|
|
129
130
|
// Not part of `Result` (no engine reports generated-media dimensions
|
|
130
131
|
// today) — measured client-side off the loaded `<img>`/`<video>` itself
|
|
@@ -238,7 +239,11 @@ export function ResultCard({
|
|
|
238
239
|
};
|
|
239
240
|
|
|
240
241
|
const handleShare = async () => {
|
|
241
|
-
|
|
242
|
+
// With no share sheet the link is copied; say so, or the click looks like nothing happened.
|
|
243
|
+
if (result.output && (await shareMedia(result.output.src)) === "copied") {
|
|
244
|
+
setLinkCopied(true);
|
|
245
|
+
setTimeout(() => setLinkCopied(false), 1500);
|
|
246
|
+
}
|
|
242
247
|
onAction("share", result);
|
|
243
248
|
};
|
|
244
249
|
|
|
@@ -274,6 +279,7 @@ export function ResultCard({
|
|
|
274
279
|
mediaExpanded={mediaExpanded}
|
|
275
280
|
onDownload={handleDownload}
|
|
276
281
|
onShare={handleShare}
|
|
282
|
+
linkCopied={linkCopied}
|
|
277
283
|
onExpand={() => setMediaExpanded(true)}
|
|
278
284
|
onDimensions={(w, h) => setMeasured({ id: result.id, width: w, height: h })}
|
|
279
285
|
videoTimeRef={videoTimeRef}
|
|
@@ -458,6 +464,7 @@ function ResultMedia({
|
|
|
458
464
|
mediaExpanded,
|
|
459
465
|
onDownload,
|
|
460
466
|
onShare,
|
|
467
|
+
linkCopied,
|
|
461
468
|
onExpand,
|
|
462
469
|
onDimensions,
|
|
463
470
|
videoTimeRef,
|
|
@@ -470,6 +477,7 @@ function ResultMedia({
|
|
|
470
477
|
mediaExpanded: boolean;
|
|
471
478
|
onDownload: () => void;
|
|
472
479
|
onShare: () => void;
|
|
480
|
+
linkCopied: boolean;
|
|
473
481
|
onExpand: () => void;
|
|
474
482
|
onDimensions: (width: number, height: number) => void;
|
|
475
483
|
videoTimeRef: React.MutableRefObject<number>;
|
|
@@ -555,8 +563,8 @@ function ResultMedia({
|
|
|
555
563
|
<ResultIconButton variant="top" label="Download" disabled={disabled} onClick={onDownload}>
|
|
556
564
|
<DownloadIcon />
|
|
557
565
|
</ResultIconButton>
|
|
558
|
-
<ResultIconButton variant="top" label="Share" disabled={disabled} onClick={onShare}>
|
|
559
|
-
<ShareIcon />
|
|
566
|
+
<ResultIconButton variant="top" label={linkCopied ? "Link copied" : "Share"} disabled={disabled} onClick={onShare}>
|
|
567
|
+
{linkCopied ? <CheckSymbolIcon /> : <ShareIcon />}
|
|
560
568
|
</ResultIconButton>
|
|
561
569
|
{(isVideo || isImage) && (
|
|
562
570
|
<ResultIconButton variant="top" label="Expand" disabled={disabled} onClick={onExpand}>
|
package/src/icons.tsx
CHANGED
|
@@ -113,3 +113,17 @@ export function CloseSymbolIcon() {
|
|
|
113
113
|
</svg>
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
|
+
|
|
117
|
+
// Real Material Symbols "stop" glyph, the filled variant specifically
|
|
118
|
+
// (`stop-fill`, not the base `stop` — that one traces a hollow square-
|
|
119
|
+
// outline shape even filled with `currentColor`, which read as a thin
|
|
120
|
+
// frame rather than a real stop icon at 24px; design review caught
|
|
121
|
+
// this live). Viewbox `0 -960 960 960` — the submit FAB's in-flight state,
|
|
122
|
+
// its own doc comment above where it's rendered.
|
|
123
|
+
export function StopIcon() {
|
|
124
|
+
return (
|
|
125
|
+
<svg viewBox="0 -960 960 960" aria-hidden="true">
|
|
126
|
+
<path d="M240-240v-480h480v480H240Z" fill="currentColor" />
|
|
127
|
+
</svg>
|
|
128
|
+
);
|
|
129
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,13 @@ export {
|
|
|
22
22
|
DEFAULT_MEDIA_ANALYSIS_PROMPT_LENGTH,
|
|
23
23
|
MEDIA_ANALYSIS_PROMPTS,
|
|
24
24
|
MEDIA_ANALYSIS_PROMPT_LENGTHS,
|
|
25
|
+
SUGGESTED_IMAGE_MODELS,
|
|
26
|
+
SUGGESTED_TEXT_TO_VIDEO_MODELS,
|
|
27
|
+
SUGGESTED_IMAGE_TO_VIDEO_MODELS,
|
|
28
|
+
SUGGESTED_EDIT_MODELS,
|
|
29
|
+
SUGGESTED_ANALYSIS_MODELS,
|
|
30
|
+
suggestedModels,
|
|
31
|
+
checkSuggestedModels,
|
|
25
32
|
} from "@chai-ui/core";
|
|
26
33
|
export type {
|
|
27
34
|
DroppedMedia,
|
|
@@ -40,5 +47,8 @@ export type {
|
|
|
40
47
|
Result,
|
|
41
48
|
ResultStatus,
|
|
42
49
|
Run,
|
|
50
|
+
SuggestedModelProblem,
|
|
51
|
+
SuggestedModelsCheck,
|
|
52
|
+
FalInputFormat,
|
|
43
53
|
Usage,
|
|
44
54
|
} from "@chai-ui/core";
|
package/src/media-actions.ts
CHANGED
|
@@ -24,16 +24,25 @@ export async function downloadMedia(src: string, filename: string): Promise<void
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The native share sheet where there is one, otherwise the media's URL
|
|
29
|
+
* copied to the clipboard. Says which, so the button can confirm a copy.
|
|
30
|
+
*/
|
|
31
|
+
export async function shareMedia(src: string): Promise<"shared" | "copied" | "none"> {
|
|
29
32
|
if (navigator.share) {
|
|
30
33
|
try {
|
|
31
34
|
await navigator.share({ url: src });
|
|
35
|
+
return "shared";
|
|
32
36
|
} catch {
|
|
33
37
|
// Dismissed, or not shareable: same as closing a native share sheet.
|
|
38
|
+
return "none";
|
|
34
39
|
}
|
|
35
|
-
}
|
|
36
|
-
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
await navigator.clipboard.writeText(src);
|
|
43
|
+
return "copied";
|
|
44
|
+
} catch {
|
|
45
|
+
return "none";
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
48
|
|
package/src/style.css
CHANGED
|
@@ -1178,11 +1178,26 @@
|
|
|
1178
1178
|
--md-fab-secondary-icon-color: var(--chai-media-analyzer-text);
|
|
1179
1179
|
}
|
|
1180
1180
|
|
|
1181
|
+
/* While analyzing, with `onAbort`: submit becomes stop, same as the Composer's.
|
|
1182
|
+
Before `[data-error]`, so an error still wins. */
|
|
1183
|
+
.chai-media-analyzer__submit[data-stop] {
|
|
1184
|
+
--md-fab-secondary-container-color: var(--chai-color-semantic-warning-subtle);
|
|
1185
|
+
--md-fab-secondary-icon-color: var(--chai-color-semantic-warning);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1181
1188
|
.chai-media-analyzer__submit[data-error] {
|
|
1182
1189
|
--md-fab-secondary-container-color: var(--chai-color-semantic-danger-subtle);
|
|
1183
1190
|
--md-fab-secondary-icon-color: var(--chai-color-semantic-danger);
|
|
1184
1191
|
}
|
|
1185
1192
|
|
|
1193
|
+
/* What the last attach replaced (one kind per analysis). */
|
|
1194
|
+
.chai-media-analyzer__note {
|
|
1195
|
+
margin: 0;
|
|
1196
|
+
padding: 2px 2px 0;
|
|
1197
|
+
color: var(--chai-media-analyzer-text);
|
|
1198
|
+
font-size: var(--chai-type-size-label-large);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1186
1201
|
.chai-media-analyzer__error {
|
|
1187
1202
|
margin: 0;
|
|
1188
1203
|
padding: 2px 2px 0;
|
|
@@ -1825,7 +1840,7 @@
|
|
|
1825
1840
|
regions, so both move and scale together. */
|
|
1826
1841
|
|
|
1827
1842
|
.chai-edit-card {
|
|
1828
|
-
width: var(--chai-edit-card-width,
|
|
1843
|
+
width: var(--chai-edit-card-width, 630px);
|
|
1829
1844
|
max-width: 100%;
|
|
1830
1845
|
display: flex;
|
|
1831
1846
|
flex-direction: column;
|
package/src/useComposer.ts
CHANGED
|
@@ -128,8 +128,12 @@ export function useComposer(options: UseComposerOptions = {}) {
|
|
|
128
128
|
const [activeVersion, setActiveVersion] = useState(0);
|
|
129
129
|
// Another image starts over. Reset while rendering, not in an effect (the rules of React).
|
|
130
130
|
const [editSource, setEditSource] = useState<string | null>(editImage?.src ?? null);
|
|
131
|
+
// Numbers each image, so its original version has its own id and EditCard
|
|
132
|
+
// starts it clean (no half-drawn region from the last image).
|
|
133
|
+
const [editImageCount, setEditImageCount] = useState(0);
|
|
131
134
|
if ((editImage?.src ?? null) !== editSource) {
|
|
132
135
|
setEditSource(editImage?.src ?? null);
|
|
136
|
+
setEditImageCount((n) => n + 1);
|
|
133
137
|
setEditRuns([]);
|
|
134
138
|
setRegions([]);
|
|
135
139
|
setActiveVersion(0);
|
|
@@ -138,7 +142,7 @@ export function useComposer(options: UseComposerOptions = {}) {
|
|
|
138
142
|
() =>
|
|
139
143
|
editImage
|
|
140
144
|
? [
|
|
141
|
-
{ id:
|
|
145
|
+
{ id: `original-${editImageCount}`, src: editImage.src, status: "done", regions: [] },
|
|
142
146
|
...editRuns.flatMap((run) =>
|
|
143
147
|
run.results.map((r) => ({
|
|
144
148
|
id: r.id,
|
|
@@ -151,7 +155,7 @@ export function useComposer(options: UseComposerOptions = {}) {
|
|
|
151
155
|
),
|
|
152
156
|
]
|
|
153
157
|
: [],
|
|
154
|
-
[editImage, editRuns]
|
|
158
|
+
[editImage, editRuns, editImageCount]
|
|
155
159
|
);
|
|
156
160
|
const editRef = useLatest({ versions, activeVersion, editRuns });
|
|
157
161
|
// Each submit gets an id, so routing that settles after a newer submit is dropped.
|