@chai-ui/react 0.6.0 → 0.7.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 +19 -3
- package/dist/ChaiProvider.d.ts +3 -42
- package/dist/ChaiProvider.d.ts.map +1 -1
- package/dist/ChaiProvider.js +5 -43
- package/dist/ChaiProvider.js.map +1 -1
- package/dist/Composer.d.ts +4 -59
- package/dist/Composer.d.ts.map +1 -1
- package/dist/Composer.js +50 -169
- package/dist/Composer.js.map +1 -1
- package/dist/EditCard.d.ts +2 -16
- package/dist/EditCard.d.ts.map +1 -1
- package/dist/EditCard.js +16 -115
- package/dist/EditCard.js.map +1 -1
- package/dist/MediaAnalyzer.d.ts +3 -49
- package/dist/MediaAnalyzer.d.ts.map +1 -1
- package/dist/MediaAnalyzer.js +13 -141
- package/dist/MediaAnalyzer.js.map +1 -1
- package/dist/ResultCard.d.ts +13 -0
- package/dist/ResultCard.d.ts.map +1 -1
- package/dist/ResultCard.js +2 -48
- package/dist/ResultCard.js.map +1 -1
- package/dist/useComposer.d.ts +11 -86
- package/dist/useComposer.d.ts.map +1 -1
- package/dist/useComposer.js +26 -291
- package/dist/useComposer.js.map +1 -1
- package/dist/useMediaAnalyzer.d.ts +8 -28
- package/dist/useMediaAnalyzer.d.ts.map +1 -1
- package/dist/useMediaAnalyzer.js +9 -106
- package/dist/useMediaAnalyzer.js.map +1 -1
- package/package.json +3 -3
- package/src/ChaiProvider.tsx +12 -70
- package/src/Composer.tsx +73 -258
- package/src/EditCard.tsx +46 -139
- package/src/MediaAnalyzer.tsx +31 -176
- package/src/ResultCard.tsx +13 -38
- package/src/useComposer.ts +33 -411
- package/src/useMediaAnalyzer.ts +15 -146
- package/dist/media-actions.d.ts +0 -18
- package/dist/media-actions.d.ts.map +0 -1
- package/dist/media-actions.js +0 -56
- package/dist/media-actions.js.map +0 -1
- package/dist/regions.d.ts +0 -3
- package/dist/regions.d.ts.map +0 -1
- package/dist/regions.js +0 -7
- package/dist/regions.js.map +0 -1
- package/src/media-actions.ts +0 -54
- package/src/regions.ts +0 -7
package/DESIGN.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# CHAI UI — DESIGN.md
|
|
2
2
|
|
|
3
|
-
The rules for building with `@chai-ui/react`, written for
|
|
4
|
-
the coding agents who integrate
|
|
3
|
+
The rules for building with `@chai-ui/react` or `@chai-ui/vue`, written for
|
|
4
|
+
the people and the coding agents who integrate them. Both packages ship
|
|
5
|
+
this file. It uses React's names; "In Vue" below maps them. The API reference is the JSDoc on each
|
|
5
6
|
component's props and the docs site's `llms.txt`. This file covers how to
|
|
6
7
|
use the pieces correctly: which component to pick, which tokens to use,
|
|
7
8
|
which states you get for free, and what not to build.
|
|
@@ -28,7 +29,8 @@ use these names: `Results`, `Compare`, `ModelPicker`,
|
|
|
28
29
|
## 2. Integration rules
|
|
29
30
|
|
|
30
31
|
1. **Load the CSS in order:** `@chai-ui/tokens/css` first, then
|
|
31
|
-
`@chai-ui/react/style.css
|
|
32
|
+
`@chai-ui/react/style.css` (or `@chai-ui/vue/style.css`). Component
|
|
33
|
+
styles read the token variables.
|
|
32
34
|
2. **Everything is controlled.** Every value comes in as a prop and every
|
|
33
35
|
change comes out as a callback (`value` / `onChange`). Keep the state in
|
|
34
36
|
your app.
|
|
@@ -59,6 +61,20 @@ use these names: `Results`, `Compare`, `ModelPicker`,
|
|
|
59
61
|
Don't show model rankings or leaderboard positions you can't keep up to
|
|
60
62
|
date. Don't show a control the selected model can't honor.
|
|
61
63
|
|
|
64
|
+
### In Vue
|
|
65
|
+
|
|
66
|
+
The same components, rules and behavior, with Vue's names:
|
|
67
|
+
|
|
68
|
+
- `value` / `onChange` is `v-model`; every other `x` / `onXChange` pair is
|
|
69
|
+
`v-model:x` (`v-model:attachments`, `v-model:model-id`, `v-model:regions`, …).
|
|
70
|
+
- Other callbacks are events: `@submit`, `@action`, `@enhance`, `@abort`.
|
|
71
|
+
Rule 4 holds: a control that needs a callback shows only with its
|
|
72
|
+
listener (`@enhance` for the optimize button, `@abort` for stop,
|
|
73
|
+
`@update:model-id` for the Model menu).
|
|
74
|
+
- `useComposer` and `useMediaAnalyzer` are composables returning refs.
|
|
75
|
+
Pass a getter for options that change: `useComposer(() => ({ engine, editImage: image.value }))`.
|
|
76
|
+
- `ChaiProvider` is a component with the same props.
|
|
77
|
+
|
|
62
78
|
## 3. Material components, and where Chai uses its own
|
|
63
79
|
|
|
64
80
|
Chai builds on Material Design 3's web components where they fit, and
|
package/dist/ChaiProvider.d.ts
CHANGED
|
@@ -1,49 +1,10 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import { type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* auto-select routing and image analysis. A default, not a requirement —
|
|
6
|
-
* pass any model id `reasoningEngine` can run.
|
|
7
|
-
*/
|
|
8
|
-
export declare const DEFAULT_REASONING_MODEL = "anthropic/claude-opus-5";
|
|
9
|
-
/**
|
|
10
|
-
* Suggested per-kind overrides for media Claude can't read. Gemini takes
|
|
11
|
-
* video and audio directly. For a cheaper option, swap in
|
|
12
|
-
* `"qwen/qwen3.8-omni-flash"`, which takes both too.
|
|
13
|
-
*/
|
|
14
|
-
export declare const DEFAULT_REASONING_MODEL_BY_KIND: Partial<Record<MediaKind, string>>;
|
|
15
|
-
/** The reasoning engine and models every CHAI component shares. */
|
|
16
|
-
export interface Reasoning {
|
|
17
|
-
engine: GenerationEngine;
|
|
18
|
-
/** The model for text and any attachment kind without its own entry in `modelByKind`. */
|
|
19
|
-
model: string;
|
|
20
|
-
modelByKind: Partial<Record<MediaKind, string>>;
|
|
21
|
-
}
|
|
22
|
-
export interface ChaiProviderProps {
|
|
23
|
-
/** Text, image and routing model. Defaults to `DEFAULT_REASONING_MODEL`. */
|
|
24
|
-
reasoningModel?: string;
|
|
25
|
-
/**
|
|
26
|
-
* Per-attachment-kind models, merged over `DEFAULT_REASONING_MODEL_BY_KIND`
|
|
27
|
-
* one kind at a time, e.g. `{ video: "qwen/qwen3.8-omni-flash" }` keeps
|
|
28
|
-
* the default for audio. Map a kind to `reasoningModel`'s value to use
|
|
29
|
-
* one model for everything.
|
|
30
|
-
*/
|
|
31
|
-
reasoningModelByKind?: Partial<Record<MediaKind, string>>;
|
|
32
|
-
/**
|
|
33
|
-
* The engine the reasoning models run on. Defaults to OpenRouter,
|
|
34
|
-
* independent of the generation engine: Fal's own LLM access is an
|
|
35
|
-
* OpenRouter proxy, so defaulting to it would double-proxy.
|
|
36
|
-
*/
|
|
37
|
-
reasoningEngine?: GenerationEngine;
|
|
2
|
+
import { type Reasoning, type ReasoningConfig } from "@chai-ui/core";
|
|
3
|
+
export { DEFAULT_REASONING_MODEL, DEFAULT_REASONING_MODEL_BY_KIND, reasoningModelFor, type Reasoning, } from "@chai-ui/core";
|
|
4
|
+
export interface ChaiProviderProps extends ReasoningConfig {
|
|
38
5
|
children?: ReactNode;
|
|
39
6
|
}
|
|
40
7
|
export declare function ChaiProvider({ reasoningModel, reasoningModelByKind, reasoningEngine, children }: ChaiProviderProps): import("react").JSX.Element;
|
|
41
8
|
/** The nearest `ChaiProvider`'s reasoning engine and models, or `undefined` outside one. */
|
|
42
9
|
export declare function useReasoning(): Reasoning | undefined;
|
|
43
|
-
/**
|
|
44
|
-
* The reasoning model for a call with these attachments: the first
|
|
45
|
-
* attachment whose kind has its own model wins (a clip plus a still goes
|
|
46
|
-
* to the model that can read the clip), else `reasoning.model`.
|
|
47
|
-
*/
|
|
48
|
-
export declare function reasoningModelFor(reasoning: Reasoning, attachments?: DroppedMedia[]): string;
|
|
49
10
|
//# sourceMappingURL=ChaiProvider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChaiProvider.d.ts","sourceRoot":"","sources":["../src/ChaiProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC3E,OAAO,
|
|
1
|
+
{"version":3,"file":"ChaiProvider.d.ts","sourceRoot":"","sources":["../src/ChaiProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC3E,OAAO,EAAuC,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAE1G,OAAO,EACL,uBAAuB,EACvB,+BAA+B,EAC/B,iBAAiB,EACjB,KAAK,SAAS,GACf,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,YAAY,CAAC,EAAE,cAAc,EAAE,oBAAoB,EAAE,eAAe,EAAE,QAAQ,EAAE,EAAE,iBAAiB,+BAQlH;AAED,4FAA4F;AAC5F,wBAAgB,YAAY,IAAI,SAAS,GAAG,SAAS,CAEpD"}
|
package/dist/ChaiProvider.js
CHANGED
|
@@ -1,54 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useContext, useMemo } from "react";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
* The suggested reasoning model for text and images: prompt enhancement,
|
|
6
|
-
* auto-select routing and image analysis. A default, not a requirement —
|
|
7
|
-
* pass any model id `reasoningEngine` can run.
|
|
8
|
-
*/
|
|
9
|
-
export const DEFAULT_REASONING_MODEL = "anthropic/claude-opus-5";
|
|
10
|
-
/**
|
|
11
|
-
* Suggested per-kind overrides for media Claude can't read. Gemini takes
|
|
12
|
-
* video and audio directly. For a cheaper option, swap in
|
|
13
|
-
* `"qwen/qwen3.8-omni-flash"`, which takes both too.
|
|
14
|
-
*/
|
|
15
|
-
export const DEFAULT_REASONING_MODEL_BY_KIND = {
|
|
16
|
-
video: "google/gemini-3.8-flash",
|
|
17
|
-
audio: "google/gemini-3.8-flash",
|
|
18
|
-
};
|
|
3
|
+
import { createReasoning, reasoningEngineFor } from "@chai-ui/core";
|
|
4
|
+
export { DEFAULT_REASONING_MODEL, DEFAULT_REASONING_MODEL_BY_KIND, reasoningModelFor, } from "@chai-ui/core";
|
|
19
5
|
const ReasoningContext = createContext(undefined);
|
|
20
6
|
export function ChaiProvider({ reasoningModel, reasoningModelByKind, reasoningEngine, children }) {
|
|
21
|
-
const engine = useMemo(() => reasoningEngine
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const audio = reasoningModelByKind?.audio;
|
|
25
|
-
const image = reasoningModelByKind?.image;
|
|
26
|
-
const text = reasoningModelByKind?.text;
|
|
27
|
-
const value = useMemo(() => ({
|
|
28
|
-
engine,
|
|
29
|
-
model,
|
|
30
|
-
modelByKind: { ...DEFAULT_REASONING_MODEL_BY_KIND, ...dropUndefined({ video, audio, image, text }) },
|
|
31
|
-
}), [engine, model, video, audio, image, text]);
|
|
7
|
+
const engine = useMemo(() => reasoningEngineFor({ reasoningEngine }), [reasoningEngine]);
|
|
8
|
+
const { video, audio, image, text } = reasoningModelByKind ?? {};
|
|
9
|
+
const value = useMemo(() => createReasoning({ reasoningModel, reasoningModelByKind: { video, audio, image, text } }, engine), [engine, reasoningModel, video, audio, image, text]);
|
|
32
10
|
return _jsx(ReasoningContext.Provider, { value: value, children: children });
|
|
33
11
|
}
|
|
34
|
-
function dropUndefined(obj) {
|
|
35
|
-
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
|
|
36
|
-
}
|
|
37
12
|
/** The nearest `ChaiProvider`'s reasoning engine and models, or `undefined` outside one. */
|
|
38
13
|
export function useReasoning() {
|
|
39
14
|
return useContext(ReasoningContext);
|
|
40
15
|
}
|
|
41
|
-
/**
|
|
42
|
-
* The reasoning model for a call with these attachments: the first
|
|
43
|
-
* attachment whose kind has its own model wins (a clip plus a still goes
|
|
44
|
-
* to the model that can read the clip), else `reasoning.model`.
|
|
45
|
-
*/
|
|
46
|
-
export function reasoningModelFor(reasoning, attachments = []) {
|
|
47
|
-
for (const { kind } of attachments) {
|
|
48
|
-
const model = reasoning.modelByKind[kind];
|
|
49
|
-
if (model)
|
|
50
|
-
return model;
|
|
51
|
-
}
|
|
52
|
-
return reasoning.model;
|
|
53
|
-
}
|
|
54
16
|
//# sourceMappingURL=ChaiProvider.js.map
|
package/dist/ChaiProvider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChaiProvider.js","sourceRoot":"","sources":["../src/ChaiProvider.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAkB,MAAM,OAAO,CAAC;AAC3E,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"ChaiProvider.js","sourceRoot":"","sources":["../src/ChaiProvider.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAkB,MAAM,OAAO,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAwC,MAAM,eAAe,CAAC;AAE1G,OAAO,EACL,uBAAuB,EACvB,+BAA+B,EAC/B,iBAAiB,GAElB,MAAM,eAAe,CAAC;AAEvB,MAAM,gBAAgB,GAAG,aAAa,CAAwB,SAAS,CAAC,CAAC;AAMzE,MAAM,UAAU,YAAY,CAAC,EAAE,cAAc,EAAE,oBAAoB,EAAE,eAAe,EAAE,QAAQ,EAAqB;IACjH,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IACzF,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,oBAAoB,IAAI,EAAE,CAAC;IACjE,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,eAAe,CAAC,EAAE,cAAc,EAAE,oBAAoB,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,EACtG,CAAC,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,CACpD,CAAC;IACF,OAAO,KAAC,gBAAgB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA6B,CAAC;AACzF,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,YAAY;IAC1B,OAAO,UAAU,CAAC,gBAAgB,CAAC,CAAC;AACtC,CAAC"}
|
package/dist/Composer.d.ts
CHANGED
|
@@ -1,63 +1,8 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
export type ComposerAttachMenuAction = "add-media" | "create-image" | "create-video" | "edit-media";
|
|
7
|
-
/**
|
|
8
|
-
* The selected generation use case, shown as a removable chip (reference:
|
|
9
|
-
* the mock's "Video"/"Image" chip) — deliberately independent of
|
|
10
|
-
* `attachments`: a use case can be picked from the attach menu with nothing
|
|
11
|
-
* attached yet (e.g. "Create video"). Composer never derives this from
|
|
12
|
-
* `attachments` itself (that's app-level policy, not a Composer opinion —
|
|
13
|
-
* the design). With none picked, `defaultUseCase` applies.
|
|
14
|
-
*/
|
|
15
|
-
export interface ComposerUseCase {
|
|
16
|
-
kind: MediaKind;
|
|
17
|
-
label: string;
|
|
18
|
-
/**
|
|
19
|
-
* An edit of the attached image rather than a new one: no aspect ratio,
|
|
20
|
-
* region chips when `regions` is set, and a submit needs only an image and
|
|
21
|
-
* either a prompt or a region. Images only for now.
|
|
22
|
-
*/
|
|
23
|
-
edit?: boolean;
|
|
24
|
-
}
|
|
25
|
-
/** A plain text request — what a submit means when no use case is picked, unless the builder sets `defaultUseCase`. */
|
|
26
|
-
export declare const TEXT_USE_CASE: ComposerUseCase;
|
|
27
|
-
/** Editing the attached image, the attach menu's "Edit image". Pair it with `useComposer`'s `editImage` and an `EditCard`. */
|
|
28
|
-
export declare const EDIT_IMAGE_USE_CASE: ComposerUseCase;
|
|
29
|
-
/** One use case to run and the models picked for it. Empty `modelIds` means the engine's default model. */
|
|
30
|
-
export interface ComposerSelection {
|
|
31
|
-
useCase: ComposerUseCase;
|
|
32
|
-
modelIds: string[];
|
|
33
|
-
/** The models offered for this use case: what auto-select routes among. */
|
|
34
|
-
models: ModelOption[];
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Everything Composer itself tracks, assembled into one object at submit
|
|
38
|
-
* time — not the the design `Request` shape (no `mode`/`params`/
|
|
39
|
-
* `presetId`; those belong to the run store this package doesn't have yet,
|
|
40
|
-
* see the package README), just an honest snapshot of this component's own
|
|
41
|
-
* state. Handed to `onSubmit` so a builder doesn't have to re-derive it
|
|
42
|
-
* from five separately-controlled props they already hold — mainly a
|
|
43
|
-
* correctness/ergonomics win for a coding agent wiring this up from the
|
|
44
|
-
* type signature alone.
|
|
45
|
-
*/
|
|
46
|
-
export interface ComposerSubmitPayload {
|
|
47
|
-
value: string;
|
|
48
|
-
attachments: ComposerAttachment[];
|
|
49
|
-
/** The use case to run — the picked one, or `defaultUseCase` when none is picked. The first one with `multiSelectUseCases`. */
|
|
50
|
-
useCase: ComposerUseCase;
|
|
51
|
-
/** The first picked model, or null for the engine's default. */
|
|
52
|
-
modelId: string | null;
|
|
53
|
-
aspectRatio: string | null;
|
|
54
|
-
/** Everything to run: one entry per use case with its picked models. A single entry unless `multiSelectUseCases` is on. */
|
|
55
|
-
selections: ComposerSelection[];
|
|
56
|
-
/** When true, the model is chosen at submit (`useComposer` routes among each selection's `models`), not by the picks. */
|
|
57
|
-
autoSelectModel: boolean;
|
|
58
|
-
/** An edit's marked regions, each with its own instruction. Empty unless the use case is an edit. */
|
|
59
|
-
regions?: EditRegion[];
|
|
60
|
-
}
|
|
2
|
+
import { type ComposerAttachMenuAction } from "@chai-ui/core";
|
|
3
|
+
import type { EditRegion, ModelOption, ParameterOption, MediaKind } from "@chai-ui/core";
|
|
4
|
+
import type { ComposerAttachment, ComposerSubmitPayload, ComposerUseCase } from "@chai-ui/core";
|
|
5
|
+
export { EDIT_IMAGE_USE_CASE, TEXT_USE_CASE, type ComposerAttachMenuAction, type ComposerAttachment, type ComposerSelection, type ComposerSubmitPayload, type ComposerUseCase, } from "@chai-ui/core";
|
|
61
6
|
export interface ComposerProps {
|
|
62
7
|
/** The prompt text. Controlled — Composer owns no text state of its own. */
|
|
63
8
|
value: string;
|
package/dist/Composer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Composer.d.ts","sourceRoot":"","sources":["../src/Composer.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"Composer.d.ts","sourceRoot":"","sources":["../src/Composer.tsx"],"names":[],"mappings":"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"}
|
package/dist/Composer.js
CHANGED
|
@@ -4,9 +4,9 @@ import { useEffect, useRef, useState } from "react";
|
|
|
4
4
|
import { createComponent } from "@lit/react";
|
|
5
5
|
import * as React from "react";
|
|
6
6
|
import { MdFab } from "@material/web/fab/fab.js";
|
|
7
|
-
import {
|
|
7
|
+
import { composerAttachmentsFromFiles, composerAttachMenuItems, composerView, enhanceRevealDurationMs, MAX_ANIMATE_ENHANCE_LENGTH, regionColor, TEXT_USE_CASE, } from "@chai-ui/core";
|
|
8
8
|
import { SearchMenu } from "./primitives/SearchMenu.js";
|
|
9
|
-
|
|
9
|
+
export { EDIT_IMAGE_USE_CASE, TEXT_USE_CASE, } from "@chai-ui/core";
|
|
10
10
|
/** See primitives/Toggle.tsx for why `createComponent` is used instead of raw JSX on custom-element tags. */
|
|
11
11
|
const MdFabElement = createComponent({
|
|
12
12
|
react: React,
|
|
@@ -14,35 +14,6 @@ const MdFabElement = createComponent({
|
|
|
14
14
|
elementClass: MdFab,
|
|
15
15
|
events: { onClick: "click" },
|
|
16
16
|
});
|
|
17
|
-
const ACCEPTED_KINDS = ["image", "video", "audio"];
|
|
18
|
-
function detectKind(file) {
|
|
19
|
-
const prefix = file.type.split("/")[0];
|
|
20
|
-
return ACCEPTED_KINDS.find((k) => k === prefix);
|
|
21
|
-
}
|
|
22
|
-
// Past this length the rewritten prompt just appears instantly — a
|
|
23
|
-
// multi-hundred-character reveal reads as sluggish, not delightful, and a
|
|
24
|
-
// long prompt is exactly the case where a person wants the result now, not
|
|
25
|
-
// a show. Picked to comfortably cover a typical Enhance rewrite (usually a
|
|
26
|
-
// sentence or two of added detail) while bailing out before it doesn't.
|
|
27
|
-
const MAX_ANIMATE_ENHANCE_LENGTH = 260;
|
|
28
|
-
// Time-driven (see the `requestAnimationFrame` loop that uses this), not
|
|
29
|
-
// count-driven — a fixed ms-per-character reveal on a slow frame would
|
|
30
|
-
// drift or stall instead of just finishing a beat later. Capped on both
|
|
31
|
-
// ends: a two-word rewrite shouldn't take as long as a five-word one to
|
|
32
|
-
// feel intentional, and even a near-max-length one should still resolve
|
|
33
|
-
// under a second.
|
|
34
|
-
function enhanceRevealDurationMs(charCount) {
|
|
35
|
-
return Math.min(900, Math.max(300, charCount * 6));
|
|
36
|
-
}
|
|
37
|
-
function nextAttachmentId() {
|
|
38
|
-
return typeof crypto !== "undefined" && "randomUUID" in crypto
|
|
39
|
-
? crypto.randomUUID()
|
|
40
|
-
: `attachment-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
41
|
-
}
|
|
42
|
-
/** A plain text request — what a submit means when no use case is picked, unless the builder sets `defaultUseCase`. */
|
|
43
|
-
export const TEXT_USE_CASE = { kind: "text", label: "Text" };
|
|
44
|
-
/** Editing the attached image, the attach menu's "Edit image". Pair it with `useComposer`'s `editImage` and an `EditCard`. */
|
|
45
|
-
export const EDIT_IMAGE_USE_CASE = { kind: "image", label: "Edit image", edit: true };
|
|
46
17
|
/**
|
|
47
18
|
* The `bar`-layout Composer —
|
|
48
19
|
* reference: the "Compose Component" design, whose four states (minimal / model+aspect select / auto-select with
|
|
@@ -54,84 +25,36 @@ export const EDIT_IMAGE_USE_CASE = { kind: "image", label: "Edit image", edit: t
|
|
|
54
25
|
* `--chai-composer-*` custom properties in style.css.
|
|
55
26
|
*/
|
|
56
27
|
export function Composer({ value, onChange, placeholder, promptLabel, attachments = [], onAttachmentsChange, onUnsupportedFile, useCase = null, onClearUseCase, defaultUseCase = TEXT_USE_CASE, multiSelectUseCases = false, useCases = [], onUseCasesChange, modelsByKind, multiSelectModels = false, modelIds = [], onModelIdsChange, models, modelId = null, onModelChange, autoSelectModel = false, showAutoSelectToggle = false, onAutoSelectModelChange, aspectRatios, aspectRatio = null, onAspectRatioChange, onEnhance, enhancing = false, onSubmit, submitError = null, submitDisabled, submitting = false, onAbort, onAttachMenuSelect, attachMenuActions, regions = [], onRegionsChange, disabled = false, }) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const rosterFor = (u) => {
|
|
84
|
-
const given = multiSelectUseCases ? modelsByKind?.[u.kind] : models;
|
|
85
|
-
if (given)
|
|
86
|
-
return { options: given, suggested: false };
|
|
87
|
-
return { options: suggestedModels(u.edit ? "image-edit" : deriveGenerationUseCase(u, attachments)), suggested: true };
|
|
88
|
-
};
|
|
89
|
-
// One menu section per use case with multi-use-case select; otherwise one flat roster.
|
|
90
|
-
const modelSections = (multiSelectUseCases ? activeUseCases : activeUseCases.slice(0, 1))
|
|
91
|
-
.map((u) => ({ label: multiSelectUseCases ? u.label : "", kind: u.kind, ...rosterFor(u) }))
|
|
92
|
-
.filter((s) => s.options.length > 0);
|
|
93
|
-
const allModels = modelSections.flatMap((s) => s.options);
|
|
94
|
-
// A pick that isn't in the current list (e.g. the edit model after leaving
|
|
95
|
-
// edit mode) is shown as unpicked and never submitted. With no list at
|
|
96
|
-
// all, the app's pick is passed through as given. With nothing picked
|
|
97
|
-
// from a suggested list (and auto-select off), its first model is the pick.
|
|
98
|
-
const validPicks = allModels.length > 0 ? rawPickedIds.filter((id) => allModels.some((m) => m.id === id)) : rawPickedIds;
|
|
99
|
-
const pickedModelIds = validPicks.length > 0 || autoSelectModel
|
|
100
|
-
? validPicks
|
|
101
|
-
: modelSections.filter((s) => s.suggested).map((s) => s.options[0].id);
|
|
102
|
-
const pickedModels = allModels.filter((m) => pickedModelIds.includes(m.id));
|
|
103
|
-
// No menu that can't change anything: without its handler, the Model menu
|
|
104
|
-
// hides (the default pick still runs).
|
|
105
|
-
const canPickModel = multiModel ? Boolean(onModelIdsChange) : Boolean(onModelChange);
|
|
106
|
-
const modelTriggerLabel = pickedModels.length > 1
|
|
107
|
-
? `${pickedModels.length} models`
|
|
108
|
-
: pickedModels[0]?.label ?? (autoSelectModel ? "Auto-select" : "Select models");
|
|
109
|
-
// An edit keeps the source image's shape.
|
|
110
|
-
const showAspectRatio = activeUseCases.some((u) => !u.edit && (u.kind === "image" || u.kind === "video"));
|
|
111
|
-
// Only ratios every model that might run takes: the picked ones, or with
|
|
112
|
-
// none picked (or auto-select on) the whole list, so whichever runs can
|
|
113
|
-
// honor it. A model with no `aspectRatios` sets its own shape: no menu.
|
|
114
|
-
const ratioModels = pickedModels.length > 0 && !autoSelectModel ? pickedModels : allModels;
|
|
115
|
-
const sharedRatios = ratioModels.reduce((shared, m) => (shared ?? m.aspectRatios ?? []).filter((r) => m.aspectRatios?.includes(r)), null) ?? [];
|
|
116
|
-
const aspectOptions = aspectRatios
|
|
117
|
-
? aspectRatios.filter((o) => sharedRatios.includes(o.value))
|
|
118
|
-
: sharedRatios.map((r) => ({ value: r, label: r }));
|
|
119
|
-
// A pick the current models don't take is shown as unpicked and never sent.
|
|
120
|
-
const offeredAspect = showAspectRatio && aspectOptions.some((o) => o.value === aspectRatio) ? aspectRatio : null;
|
|
121
|
-
const selectedAspect = aspectOptions.find((o) => o.value === offeredAspect) ?? null;
|
|
28
|
+
const view = composerView({
|
|
29
|
+
value,
|
|
30
|
+
placeholder,
|
|
31
|
+
attachments,
|
|
32
|
+
useCase,
|
|
33
|
+
defaultUseCase,
|
|
34
|
+
multiSelectUseCases,
|
|
35
|
+
useCases,
|
|
36
|
+
modelsByKind,
|
|
37
|
+
multiSelectModels,
|
|
38
|
+
modelIds,
|
|
39
|
+
models,
|
|
40
|
+
modelId,
|
|
41
|
+
onModelChange,
|
|
42
|
+
onModelIdsChange,
|
|
43
|
+
autoSelectModel,
|
|
44
|
+
aspectRatios,
|
|
45
|
+
aspectRatio,
|
|
46
|
+
submitError,
|
|
47
|
+
submitDisabled,
|
|
48
|
+
submitting,
|
|
49
|
+
onAbort,
|
|
50
|
+
regions,
|
|
51
|
+
disabled,
|
|
52
|
+
});
|
|
53
|
+
const { editing, isSubmitDisabled, showStop, multiModel, modelSections, pickedModelIds, aspectOptions, offeredAspect } = view;
|
|
122
54
|
function toggleModel(id) {
|
|
123
55
|
if (!multiModel)
|
|
124
56
|
return onModelChange?.(id);
|
|
125
|
-
onModelIdsChange?.(
|
|
126
|
-
}
|
|
127
|
-
function buildSelections() {
|
|
128
|
-
return activeUseCases.map((u) => ({
|
|
129
|
-
useCase: u,
|
|
130
|
-
models: rosterFor(u).options,
|
|
131
|
-
modelIds: multiSelectUseCases
|
|
132
|
-
? pickedModelIds.filter((id) => rosterFor(u).options.some((m) => m.id === id))
|
|
133
|
-
: pickedModelIds,
|
|
134
|
-
}));
|
|
57
|
+
onModelIdsChange?.(view.toggledModelIds(id));
|
|
135
58
|
}
|
|
136
59
|
// Auto-select chooses at submit, from the prompt (`useComposer` routes
|
|
137
60
|
// with the reasoning model). Turning it on clears the current pick, so
|
|
@@ -218,57 +141,26 @@ export function Composer({ value, onChange, placeholder, promptLabel, attachment
|
|
|
218
141
|
setOriginalValue(value); // snapshot now — before the rewrite lands, for undo
|
|
219
142
|
onEnhance?.(value);
|
|
220
143
|
}
|
|
221
|
-
function handleFilesSelected(fileList) {
|
|
144
|
+
async function handleFilesSelected(fileList) {
|
|
222
145
|
if (!fileList || fileList.length === 0 || !onAttachmentsChange)
|
|
223
146
|
return;
|
|
224
147
|
// Edit mode works on one image: a new one replaces it, and the edit starts over.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
const reader = new FileReader();
|
|
232
|
-
reader.onload = () => onAttachmentsChange([{ id: nextAttachmentId(), src: String(reader.result), kind: "image", name: image.name, size: image.size }]);
|
|
233
|
-
reader.onerror = () => onUnsupportedFile?.(image.name);
|
|
234
|
-
reader.readAsDataURL(image);
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
const next = [...attachments];
|
|
238
|
-
let pending = fileList.length;
|
|
239
|
-
const settle = () => {
|
|
240
|
-
pending -= 1;
|
|
241
|
-
if (pending === 0)
|
|
242
|
-
onAttachmentsChange(next);
|
|
243
|
-
};
|
|
244
|
-
Array.from(fileList).forEach((file) => {
|
|
245
|
-
const kind = detectKind(file);
|
|
246
|
-
if (!kind) {
|
|
247
|
-
onUnsupportedFile?.(file.name);
|
|
248
|
-
settle();
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
const reader = new FileReader();
|
|
252
|
-
reader.onload = () => {
|
|
253
|
-
next.push({ id: nextAttachmentId(), src: String(reader.result), kind, name: file.name, size: file.size });
|
|
254
|
-
settle();
|
|
255
|
-
};
|
|
256
|
-
reader.onerror = () => {
|
|
257
|
-
onUnsupportedFile?.(file.name);
|
|
258
|
-
settle();
|
|
259
|
-
};
|
|
260
|
-
reader.readAsDataURL(file);
|
|
261
|
-
});
|
|
148
|
+
const { attachments: next, unsupported } = await composerAttachmentsFromFiles(fileList, attachments, editing);
|
|
149
|
+
unsupported.forEach((name) => onUnsupportedFile?.(name));
|
|
150
|
+
if (next)
|
|
151
|
+
onAttachmentsChange(next);
|
|
262
152
|
}
|
|
263
153
|
// Only items that can work: no menu item that silently does nothing.
|
|
264
|
-
const menuItems =
|
|
265
|
-
|
|
154
|
+
const menuItems = composerAttachMenuItems(attachMenuActions, {
|
|
155
|
+
canAddMedia: Boolean(onAttachmentsChange),
|
|
156
|
+
canSelect: Boolean(onAttachMenuSelect),
|
|
157
|
+
});
|
|
266
158
|
function handleRemoveAttachment(id) {
|
|
267
159
|
onAttachmentsChange?.(attachments.filter((a) => a.id !== id));
|
|
268
160
|
}
|
|
269
|
-
return (_jsxs("div", { className: "chai-composer", children: [attachments.length > 0 && (_jsx("div", { className: "chai-composer__thumbnails", children: attachments.map((a) => (_jsx(ComposerThumbnail, { attachment: a, onRemove: onAttachmentsChange ? () => handleRemoveAttachment(a.id) : undefined }, a.id))) })), editing && regions.length > 0 && (_jsx("ul", { className: "chai-composer__regions", "aria-label": "Edit regions", children: regions.map((r) => (_jsx("li", { children: _jsx(RegionChip, { region: r, onRemove: onRegionsChange ? () => onRegionsChange(regions.filter((x) => x.id !== r.id)) : undefined, disabled: disabled }) }, r.id))) })), _jsxs("div", { className: "chai-composer__field", children: [_jsx("textarea", { ref: textareaRef, className: `chai-composer__input chai-composer__type${animating ? " chai-composer__input--revealing" : ""}`, value: value, onChange: (e) => onChange(e.target.value), placeholder:
|
|
161
|
+
return (_jsxs("div", { className: "chai-composer", children: [attachments.length > 0 && (_jsx("div", { className: "chai-composer__thumbnails", children: attachments.map((a) => (_jsx(ComposerThumbnail, { attachment: a, onRemove: onAttachmentsChange ? () => handleRemoveAttachment(a.id) : undefined }, a.id))) })), editing && regions.length > 0 && (_jsx("ul", { className: "chai-composer__regions", "aria-label": "Edit regions", children: regions.map((r) => (_jsx("li", { children: _jsx(RegionChip, { region: r, onRemove: onRegionsChange ? () => onRegionsChange(regions.filter((x) => x.id !== r.id)) : undefined, disabled: disabled }) }, r.id))) })), _jsxs("div", { className: "chai-composer__field", children: [_jsx("textarea", { ref: textareaRef, className: `chai-composer__input chai-composer__type${animating ? " chai-composer__input--revealing" : ""}`, value: value, onChange: (e) => onChange(e.target.value), placeholder: view.placeholder, "aria-label": promptLabel ?? view.placeholder, rows: 1, disabled: disabled || animating }), animating && (_jsx("div", { className: "chai-composer__reveal chai-composer__type", "aria-hidden": "true", children: _jsx("span", { className: "chai-composer__reveal-text", children: animatingText.slice(0, revealedLength) }) })), onEnhance && (_jsx("button", { type: "button", className: `chai-composer__optimize${enhanceBusy ? " chai-composer__optimize--active" : ""}`, onClick: handleOptimizeClick, disabled: disabled || (isEnhanced ? false : !value.trim() || enhanceBusy), "aria-label": isEnhanced ? "Revert to original prompt" : "Optimize prompt", title: isEnhanced ? "Revert to original prompt" : "Optimize prompt", children: isEnhanced ? _jsx(UndoIcon, {}) : _jsx(OptimizeIcon, {}) }))] }), submitError && (_jsxs("p", { className: "chai-composer__error", role: "alert", children: [_jsx(ErrorIcon, {}), submitError] })), _jsxs("div", { className: "chai-composer__controls", children: [_jsxs("div", { className: "chai-composer__controls-start", children: [menuItems.length > 0 && (_jsx(ComposerAttachMenu, { items: menuItems, onSelect: onAttachMenuSelect, onAddMediaFiles: handleFilesSelected, imagesOnly: editing, disabled: disabled })), view.pickedUseCases.map((u) => (_jsx(UseCaseChip, { useCase: u, onRemove: multiSelectUseCases
|
|
270
162
|
? onUseCasesChange && (() => onUseCasesChange(useCases.filter((x) => x.kind !== u.kind)))
|
|
271
|
-
: onClearUseCase, disabled: disabled }, u.kind))), modelSections.length > 0 && canPickModel && (_jsx(SearchMenu, { ...(multiSelectUseCases
|
|
163
|
+
: onClearUseCase, disabled: disabled }, u.kind))), modelSections.length > 0 && view.canPickModel && (_jsx(SearchMenu, { ...(multiSelectUseCases
|
|
272
164
|
? { sections: modelSections.map((s) => ({ label: s.label, options: s.options })) }
|
|
273
165
|
: { options: modelSections[0].options }), value: multiModel ? pickedModelIds : modelId, multiple: multiModel, toggleHeader: showAutoSelectToggle
|
|
274
166
|
? {
|
|
@@ -276,33 +168,22 @@ export function Composer({ value, onChange, placeholder, promptLabel, attachment
|
|
|
276
168
|
value: autoSelectModel,
|
|
277
169
|
onChange: (v) => onAutoSelectModelChange?.(v),
|
|
278
170
|
}
|
|
279
|
-
: undefined, triggerLabel: modelTriggerLabel, menuLabel: multiModel ? "Select models" : "Select model", onSelect: (opt) => toggleModel(opt.id), searchable: false, triggerVariant: "text", disabled: disabled || (autoSelectModel && !showAutoSelectToggle), panelWidth: showAutoSelectToggle ? 320 : undefined })), showAspectRatio && aspectOptions.length > 0 && onAspectRatioChange && (_jsx(SearchMenu, { options: aspectOptions.map((o) => ({ id: o.value, label: o.label })), value: offeredAspect, triggerLabel: selectedAspect?.label ?? "Aspect ratio", menuLabel: "Aspect ratio", onSelect: (opt) => onAspectRatioChange?.(opt.id), searchable: false, triggerVariant: "text", disabled: disabled }))] }), _jsx(MdFabElement, { className: "chai-composer__submit", size: "small", variant: "secondary", label: "", "data-ready": showStop || !isSubmitDisabled || undefined, "data-error": Boolean(submitError) || undefined, "data-stop": showStop || undefined, ...{
|
|
171
|
+
: undefined, triggerLabel: view.modelTriggerLabel, menuLabel: multiModel ? "Select models" : "Select model", onSelect: (opt) => toggleModel(opt.id), searchable: false, triggerVariant: "text", disabled: disabled || (autoSelectModel && !showAutoSelectToggle), panelWidth: showAutoSelectToggle ? 320 : undefined })), view.showAspectRatio && aspectOptions.length > 0 && onAspectRatioChange && (_jsx(SearchMenu, { options: aspectOptions.map((o) => ({ id: o.value, label: o.label })), value: offeredAspect, triggerLabel: view.selectedAspect?.label ?? "Aspect ratio", menuLabel: "Aspect ratio", onSelect: (opt) => onAspectRatioChange?.(opt.id), searchable: false, triggerVariant: "text", disabled: disabled }))] }), _jsx(MdFabElement, { className: "chai-composer__submit", size: "small", variant: "secondary", label: "", "data-ready": showStop || !isSubmitDisabled || undefined, "data-error": Boolean(submitError) || undefined, "data-stop": showStop || undefined, ...{
|
|
280
172
|
ariaLabel: showStop ? "Stop" : submitError ? "Retry" : "Submit",
|
|
281
173
|
ariaDisabled: showStop ? "false" : submitting ? "true" : isSubmitDisabled ? "true" : "false",
|
|
282
174
|
}, onClick: () => {
|
|
283
175
|
if (showStop)
|
|
284
176
|
onAbort?.();
|
|
285
|
-
else if (!isSubmitDisabled && !submitting)
|
|
286
|
-
|
|
287
|
-
onSubmit({
|
|
288
|
-
value,
|
|
289
|
-
attachments,
|
|
290
|
-
useCase: activeUseCases[0],
|
|
291
|
-
modelId: pickedModelIds[0] ?? null,
|
|
292
|
-
aspectRatio: offeredAspect,
|
|
293
|
-
selections,
|
|
294
|
-
autoSelectModel,
|
|
295
|
-
regions: editing ? regions : [],
|
|
296
|
-
});
|
|
297
|
-
}
|
|
177
|
+
else if (!isSubmitDisabled && !submitting)
|
|
178
|
+
onSubmit(view.payload());
|
|
298
179
|
}, children: _jsx("span", { slot: "icon", children: showStop ? _jsx(StopIcon, {}) : submitError ? _jsx(RefreshIcon, {}) : _jsx(ArrowForwardIcon, {}) }) })] })] }));
|
|
299
180
|
}
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
181
|
+
const ATTACH_MENU_ICONS = {
|
|
182
|
+
"add-media": _jsx(AttachFileIcon, {}),
|
|
183
|
+
"create-image": _jsx(ImageIcon, {}),
|
|
184
|
+
"create-video": _jsx(VideocamIcon, {}),
|
|
185
|
+
"edit-media": _jsx(ContentCutIcon, {}),
|
|
186
|
+
};
|
|
306
187
|
/**
|
|
307
188
|
* The "+" / "×" attach control (reference: Composer_01 "Menu open" state,
|
|
308
189
|
* Figma "Menu - Icon button Example") — hand-rolled positioned
|
|
@@ -338,7 +219,7 @@ function ComposerAttachMenu({ items, onSelect, onAddMediaFiles, imagesOnly, disa
|
|
|
338
219
|
inputRef.current?.click();
|
|
339
220
|
onSelect?.(action);
|
|
340
221
|
}
|
|
341
|
-
return (_jsxs("div", { className: "chai-composer__attach", ref: rootRef, children: [_jsx("button", { type: "button", className: `chai-composer__attach-toggle${open ? " chai-composer__attach-toggle--open" : ""}`, onClick: () => setOpen((v) => !v), disabled: disabled, "aria-expanded": open, "aria-haspopup": "menu", "aria-label": open ? "Close attach menu" : "Add media", children: open ? _jsx(CloseIcon, {}) : _jsx(PlusIcon, {}) }), open && (_jsx("ul", { className: "chai-composer__attach-menu", role: "menu", children: items.map((item, i) => (_jsxs("li", { role: "none", children: [_jsxs("button", { type: "button", role: "menuitem", className: "chai-composer__attach-menu-item", onClick: () => handleItemClick(item.action), children: [_jsx("span", { className: "chai-composer__attach-menu-icon", children: item.
|
|
222
|
+
return (_jsxs("div", { className: "chai-composer__attach", ref: rootRef, children: [_jsx("button", { type: "button", className: `chai-composer__attach-toggle${open ? " chai-composer__attach-toggle--open" : ""}`, onClick: () => setOpen((v) => !v), disabled: disabled, "aria-expanded": open, "aria-haspopup": "menu", "aria-label": open ? "Close attach menu" : "Add media", children: open ? _jsx(CloseIcon, {}) : _jsx(PlusIcon, {}) }), open && (_jsx("ul", { className: "chai-composer__attach-menu", role: "menu", children: items.map((item, i) => (_jsxs("li", { role: "none", children: [_jsxs("button", { type: "button", role: "menuitem", className: "chai-composer__attach-menu-item", onClick: () => handleItemClick(item.action), children: [_jsx("span", { className: "chai-composer__attach-menu-icon", children: ATTACH_MENU_ICONS[item.action] }), item.label] }), item.divider && i < items.length - 1 && _jsx("hr", { className: "chai-composer__attach-menu-divider", role: "separator" })] }, item.action))) })), _jsx("input", { ref: inputRef, type: "file", accept: imagesOnly ? "image/*" : "image/*,video/*,audio/*", multiple: !imagesOnly, hidden: true, disabled: disabled, onChange: (e) => {
|
|
342
223
|
onAddMediaFiles(e.target.files);
|
|
343
224
|
e.target.value = "";
|
|
344
225
|
} })] }));
|