@chai-ui/vue 0.0.0-stage → 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 +235 -0
- package/LICENSE +21 -0
- package/README.md +122 -2
- package/dist/ChaiProvider.vue.d.ts +24 -0
- package/dist/ChaiProvider.vue.d.ts.map +1 -0
- package/dist/Composer.vue.d.ts +94 -0
- package/dist/Composer.vue.d.ts.map +1 -0
- package/dist/EditCard.vue.d.ts +38 -0
- package/dist/EditCard.vue.d.ts.map +1 -0
- package/dist/MediaAnalyzer.vue.d.ts +54 -0
- package/dist/MediaAnalyzer.vue.d.ts.map +1 -0
- package/dist/PageSlide.vue.d.ts +23 -0
- package/dist/PageSlide.vue.d.ts.map +1 -0
- package/dist/ResultCard.vue.d.ts +29 -0
- package/dist/ResultCard.vue.d.ts.map +1 -0
- package/dist/composer/AttachMenu.vue.d.ts +17 -0
- package/dist/composer/AttachMenu.vue.d.ts.map +1 -0
- package/dist/composer/Chip.vue.d.ts +14 -0
- package/dist/composer/Chip.vue.d.ts.map +1 -0
- package/dist/dismiss.d.ts +4 -0
- package/dist/dismiss.d.ts.map +1 -0
- package/dist/edit-card/ExpandedImage.vue.d.ts +12 -0
- package/dist/edit-card/ExpandedImage.vue.d.ts.map +1 -0
- package/dist/edit-card/RegionShape.vue.d.ts +20 -0
- package/dist/edit-card/RegionShape.vue.d.ts.map +1 -0
- package/dist/icons.d.ts +43 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2515 -0
- package/dist/index.js.map +1 -0
- package/dist/page-slide.d.ts +31 -0
- package/dist/page-slide.d.ts.map +1 -0
- package/dist/primitives/Pagination.vue.d.ts +26 -0
- package/dist/primitives/Pagination.vue.d.ts.map +1 -0
- package/dist/primitives/SearchMenu.vue.d.ts +51 -0
- package/dist/primitives/SearchMenu.vue.d.ts.map +1 -0
- package/dist/primitives/Toggle.vue.d.ts +16 -0
- package/dist/primitives/Toggle.vue.d.ts.map +1 -0
- package/dist/primitives/search-menu.d.ts +19 -0
- package/dist/primitives/search-menu.d.ts.map +1 -0
- package/dist/reasoning.d.ts +6 -0
- package/dist/reasoning.d.ts.map +1 -0
- package/dist/result-card/IconButton.vue.d.ts +21 -0
- package/dist/result-card/IconButton.vue.d.ts.map +1 -0
- package/dist/result-card/MoreMenu.vue.d.ts +13 -0
- package/dist/result-card/MoreMenu.vue.d.ts.map +1 -0
- package/dist/result-card/ResultMedia.vue.d.ts +28 -0
- package/dist/result-card/ResultMedia.vue.d.ts.map +1 -0
- package/dist/result-card/ResultText.vue.d.ts +8 -0
- package/dist/result-card/ResultText.vue.d.ts.map +1 -0
- package/dist/result-card/VideoPlayer.vue.d.ts +18 -0
- package/dist/result-card/VideoPlayer.vue.d.ts.map +1 -0
- package/dist/result-card/types.d.ts +9 -0
- package/dist/result-card/types.d.ts.map +1 -0
- package/dist/style.css +2378 -0
- package/dist/useComposer.d.ts +22 -0
- package/dist/useComposer.d.ts.map +1 -0
- package/dist/useMediaAnalyzer.d.ts +17 -0
- package/dist/useMediaAnalyzer.d.ts.map +1 -0
- package/package.json +79 -3
- package/src/ChaiProvider.vue +26 -0
- package/src/Composer.vue +362 -0
- package/src/EditCard.vue +520 -0
- package/src/MediaAnalyzer.vue +216 -0
- package/src/PageSlide.vue +36 -0
- package/src/ResultCard.vue +297 -0
- package/src/composer/AttachMenu.vue +66 -0
- package/src/composer/Chip.vue +26 -0
- package/src/dismiss.ts +22 -0
- package/src/edit-card/ExpandedImage.vue +39 -0
- package/src/edit-card/RegionShape.vue +39 -0
- package/src/icons.ts +55 -0
- package/src/index.ts +77 -0
- package/src/page-slide.ts +32 -0
- package/src/primitives/Pagination.vue +76 -0
- package/src/primitives/SearchMenu.vue +167 -0
- package/src/primitives/Toggle.vue +23 -0
- package/src/primitives/search-menu.ts +20 -0
- package/src/reasoning.ts +10 -0
- package/src/result-card/IconButton.vue +18 -0
- package/src/result-card/MoreMenu.vue +35 -0
- package/src/result-card/ResultMedia.vue +77 -0
- package/src/result-card/ResultText.vue +37 -0
- package/src/result-card/VideoPlayer.vue +90 -0
- package/src/result-card/types.ts +10 -0
- package/src/useComposer.ts +38 -0
- package/src/useMediaAnalyzer.ts +33 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type MaybeRefOrGetter } from "vue";
|
|
2
|
+
import { type ComposerOptions } from "@chai-ui/core";
|
|
3
|
+
export type UseComposerOptions = ComposerOptions;
|
|
4
|
+
/**
|
|
5
|
+
* The Vue binding for `createComposerStore` in `@chai-ui/core`: turns a
|
|
6
|
+
* `Composer`'s submit payload into a real `Request` → `Run` → `Result`.
|
|
7
|
+
* Pass a getter (or ref) for options that change, e.g. `() => ({ engine, editImage: image.value })`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useComposer(options?: MaybeRefOrGetter<UseComposerOptions>): {
|
|
10
|
+
run: import("vue").ComputedRef<import("@chai-ui/core").Run | null>;
|
|
11
|
+
runs: import("vue").ComputedRef<import("@chai-ui/core").Run[]>;
|
|
12
|
+
submit: (payload: import("@chai-ui/core").ComposerSubmitPayload) => void;
|
|
13
|
+
cancel: () => void;
|
|
14
|
+
/** `undefined` without reasoning, so the Composer hides its Enhance button. */
|
|
15
|
+
enhance: import("vue").ComputedRef<((value: string) => Promise<string>) | undefined>;
|
|
16
|
+
enhancing: import("vue").ComputedRef<boolean>;
|
|
17
|
+
routing: import("vue").ComputedRef<boolean>;
|
|
18
|
+
/** Props for an `EditCard`, and `regions` for `Composer`. `null` when there's no `editImage`. */
|
|
19
|
+
edit: import("vue").ComputedRef<import("@chai-ui/core").ComposerEdit | null>;
|
|
20
|
+
};
|
|
21
|
+
export type UseComposerReturn = ReturnType<typeof useComposer>;
|
|
22
|
+
//# sourceMappingURL=useComposer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useComposer.d.ts","sourceRoot":"","sources":["../src/useComposer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiD,KAAK,gBAAgB,EAAE,MAAM,KAAK,CAAC;AAC3F,OAAO,EAAsC,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAGzF,MAAM,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAEjD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,gBAAgB,CAAC,kBAAkB,CAAM;;;;;IAiB1E,+EAA+E;;;;IAI/E,iGAAiG;;EAGpG;AAED,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type MaybeRefOrGetter } from "vue";
|
|
2
|
+
import { type MediaAnalyzerOptions } from "@chai-ui/core";
|
|
3
|
+
export type UseMediaAnalyzerOptions = MediaAnalyzerOptions;
|
|
4
|
+
/**
|
|
5
|
+
* The Vue binding for `createMediaAnalyzerStore` in `@chai-ui/core`, behind
|
|
6
|
+
* `MediaAnalyzer`'s submit: `prompt` fills in as the text streams.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useMediaAnalyzer(options?: MaybeRefOrGetter<UseMediaAnalyzerOptions>): {
|
|
9
|
+
analyzing: import("vue").ComputedRef<boolean>;
|
|
10
|
+
error: import("vue").ComputedRef<string | null>;
|
|
11
|
+
prompt: import("vue").ComputedRef<string | null>;
|
|
12
|
+
submit: (payload: import("@chai-ui/core").MediaAnalyzerSubmitPayload) => void;
|
|
13
|
+
cancel: () => void;
|
|
14
|
+
reset: () => void;
|
|
15
|
+
};
|
|
16
|
+
export type UseMediaAnalyzerReturn = ReturnType<typeof useMediaAnalyzer>;
|
|
17
|
+
//# sourceMappingURL=useMediaAnalyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMediaAnalyzer.d.ts","sourceRoot":"","sources":["../src/useMediaAnalyzer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiD,KAAK,gBAAgB,EAAE,MAAM,KAAK,CAAC;AAC3F,OAAO,EAA4B,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAGpF,MAAM,MAAM,uBAAuB,GAAG,oBAAoB,CAAC;AAE3D;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,gBAAgB,CAAC,uBAAuB,CAAM;;;;;;;EAoBvF;AAED,MAAM,MAAM,sBAAsB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,82 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chai-ui/vue",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "CHAI UI Vue components — smart, drop-in UI for AI apps: Composer, Media Analyzer, Result Card, Edit Card, and the useComposer composable.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "kazarazat",
|
|
7
|
+
"homepage": "https://chai-ui.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kazarazat/chai-ui.git",
|
|
11
|
+
"directory": "packages/vue"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/kazarazat/chai-ui/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ai",
|
|
18
|
+
"generative-ai",
|
|
19
|
+
"design-system",
|
|
20
|
+
"ai-ui",
|
|
21
|
+
"vue",
|
|
22
|
+
"components",
|
|
23
|
+
"prompt",
|
|
24
|
+
"composer"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./style.css": "./dist/style.css"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src",
|
|
39
|
+
"!src/**/*.test.*",
|
|
40
|
+
"!src/__tests__",
|
|
41
|
+
"DESIGN.md"
|
|
42
|
+
],
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"vue": "^3.5.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@fontsource/roboto": "^5.3.0",
|
|
48
|
+
"@material/web": "^2.5.0",
|
|
49
|
+
"@chai-ui/core": "0.7.0",
|
|
50
|
+
"@chai-ui/tokens": "0.7.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@vitejs/plugin-vue": "^6.0.9",
|
|
54
|
+
"@vitest/browser": "3.2.7",
|
|
55
|
+
"@vitest/coverage-v8": "3.2.7",
|
|
56
|
+
"@vue/test-utils": "^2.5.1",
|
|
57
|
+
"axe-core": "^4.13.0",
|
|
58
|
+
"jsdom": "^25.0.1",
|
|
59
|
+
"playwright": "^1.63.0",
|
|
60
|
+
"rimraf": "^6.0.1",
|
|
61
|
+
"typescript": "^5.6.3",
|
|
62
|
+
"vite": "^7.3.6",
|
|
63
|
+
"vitest": "^3.2.7",
|
|
64
|
+
"vitest-browser-vue": "^1.1.0",
|
|
65
|
+
"vue": "^3.5.0",
|
|
66
|
+
"vue-tsc": "^3.3.12"
|
|
67
|
+
},
|
|
68
|
+
"publishConfig": {
|
|
69
|
+
"access": "public"
|
|
70
|
+
},
|
|
71
|
+
"scripts": {
|
|
72
|
+
"build": "rimraf dist && vite build && vue-tsc -p tsconfig.json --declaration --emitDeclarationOnly --outDir dist && node -e \"require('fs').copyFileSync('../react/src/style.css','dist/style.css');require('fs').copyFileSync('../react/DESIGN.md','DESIGN.md')\"",
|
|
73
|
+
"dev": "vite build --watch",
|
|
74
|
+
"typecheck": "vue-tsc --noEmit -p tsconfig.json",
|
|
75
|
+
"lint": "echo \"(no lint configured yet)\"",
|
|
76
|
+
"clean": "rimraf dist",
|
|
77
|
+
"test": "vitest run && vitest run --config vitest.browser.config.ts",
|
|
78
|
+
"test:coverage": "vitest run --coverage --coverage.reporter=lcov --coverage.reporter=text-summary --coverage.include=src/** --coverage.reportsDirectory=coverage/unit && vitest run --config vitest.browser.config.ts --coverage --coverage.reporter=lcov --coverage.reporter=text-summary --coverage.include=src/** --coverage.reportsDirectory=coverage/browser",
|
|
79
|
+
"test:unit": "vitest run",
|
|
80
|
+
"test:browser": "vitest run --config vitest.browser.config.ts"
|
|
81
|
+
}
|
|
6
82
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, provide } from "vue";
|
|
3
|
+
import { createReasoning, reasoningEngineFor, type GenerationEngine, type MediaKind } from "@chai-ui/core";
|
|
4
|
+
import { reasoningKey } from "./reasoning.js";
|
|
5
|
+
|
|
6
|
+
/** The reasoning engine and models every CHAI component inside shares. Same props as React's `ChaiProvider`. */
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
/** Text, image and routing model. Defaults to `DEFAULT_REASONING_MODEL`. */
|
|
9
|
+
reasoningModel?: string;
|
|
10
|
+
/** Per-attachment-kind models, merged over `DEFAULT_REASONING_MODEL_BY_KIND` one kind at a time. */
|
|
11
|
+
reasoningModelByKind?: Partial<Record<MediaKind, string>>;
|
|
12
|
+
/** The engine the reasoning models run on. Defaults to OpenRouter, independent of the generation engine. */
|
|
13
|
+
reasoningEngine?: GenerationEngine;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
// Created once, and again only when a different engine is passed.
|
|
17
|
+
const engine = computed(() => reasoningEngineFor({ reasoningEngine: props.reasoningEngine }));
|
|
18
|
+
provide(
|
|
19
|
+
reasoningKey,
|
|
20
|
+
computed(() => createReasoning(props, engine.value))
|
|
21
|
+
);
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<slot />
|
|
26
|
+
</template>
|
package/src/Composer.vue
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The compose bar for generative AI input: prompt field, attachments, use
|
|
4
|
+
* case, model and aspect-ratio menus, Enhance, and submit, all driven by
|
|
5
|
+
* which props are given. Same props as React's `Composer`, with `v-model`
|
|
6
|
+
* for the prompt and `v-model:…` for attachments, use cases, models, aspect
|
|
7
|
+
* ratio, auto-select and regions.
|
|
8
|
+
*
|
|
9
|
+
* Some controls show only with somewhere for them to go, so these listeners
|
|
10
|
+
* are props, checked for: `@update:attachments` (add and remove media),
|
|
11
|
+
* `@update:model-id` / `@update:model-ids` (the Model menu),
|
|
12
|
+
* `@update:aspect-ratio`, `@update:use-cases` and `@clear-use-case` (chip
|
|
13
|
+
* remove), `@update:regions`, `@enhance` (the Enhance button), `@abort` (stop
|
|
14
|
+
* while submitting) and `@attach-menu-select` (the + menu's use cases).
|
|
15
|
+
*/
|
|
16
|
+
import "@material/web/fab/fab.js";
|
|
17
|
+
import { computed, onScopeDispose, ref, watch } from "vue";
|
|
18
|
+
import {
|
|
19
|
+
composerAttachmentsFromFiles,
|
|
20
|
+
composerAttachMenuItems,
|
|
21
|
+
composerView,
|
|
22
|
+
enhanceRevealDurationMs,
|
|
23
|
+
MAX_ANIMATE_ENHANCE_LENGTH,
|
|
24
|
+
type ComposerAttachment,
|
|
25
|
+
type ComposerAttachMenuAction,
|
|
26
|
+
type ComposerSubmitPayload,
|
|
27
|
+
type ComposerUseCase,
|
|
28
|
+
type EditRegion,
|
|
29
|
+
type MediaKind,
|
|
30
|
+
type ModelOption,
|
|
31
|
+
type ParameterOption,
|
|
32
|
+
} from "@chai-ui/core";
|
|
33
|
+
import AttachMenu from "./composer/AttachMenu.vue";
|
|
34
|
+
import Chip from "./composer/Chip.vue";
|
|
35
|
+
import { ArrowForwardIcon, CloseSymbolIcon, ErrorIcon, ImageIcon, MusicNoteIcon, OptimizeIcon, RetryIcon, StopIcon, UndoIcon, VideocamIcon } from "./icons.js";
|
|
36
|
+
import SearchMenu from "./primitives/SearchMenu.vue";
|
|
37
|
+
|
|
38
|
+
const props = withDefaults(
|
|
39
|
+
defineProps<{
|
|
40
|
+
/** The prompt text. Controlled: use `v-model`. */
|
|
41
|
+
modelValue: string;
|
|
42
|
+
/** Defaults to "Describe media to create", or edit wording when the use case is an edit. */
|
|
43
|
+
placeholder?: string;
|
|
44
|
+
/** The prompt field's accessible name. Defaults to `placeholder`, which disappears once the person types. */
|
|
45
|
+
promptLabel?: string;
|
|
46
|
+
attachments?: ComposerAttachment[];
|
|
47
|
+
"onUpdate:attachments"?: (next: ComposerAttachment[]) => void;
|
|
48
|
+
/** The use-case chip. Aspect ratio only renders for an image or video use case. */
|
|
49
|
+
useCase?: ComposerUseCase | null;
|
|
50
|
+
onClearUseCase?: () => void;
|
|
51
|
+
/** What a submit means with no use case picked. Defaults to `TEXT_USE_CASE`. Never shown as a chip. */
|
|
52
|
+
defaultUseCase?: ComposerUseCase;
|
|
53
|
+
/** Several use cases at once, at most one per kind, with `useCases` and `modelsByKind`. */
|
|
54
|
+
multiSelectUseCases?: boolean;
|
|
55
|
+
useCases?: ComposerUseCase[];
|
|
56
|
+
"onUpdate:useCases"?: (next: ComposerUseCase[]) => void;
|
|
57
|
+
/** Models per use-case kind, for `multiSelectUseCases`. A kind left out gets Chai's suggested models. */
|
|
58
|
+
modelsByKind?: Partial<Record<MediaKind, ModelOption[]>>;
|
|
59
|
+
/** The end user can pick several models, each producing its own result, with `modelIds`. */
|
|
60
|
+
multiSelectModels?: boolean;
|
|
61
|
+
modelIds?: string[];
|
|
62
|
+
"onUpdate:modelIds"?: (ids: string[]) => void;
|
|
63
|
+
/** Models for the use case. Leave out for Chai's suggested Fal models; `[]` for no Model menu. */
|
|
64
|
+
models?: ModelOption[];
|
|
65
|
+
modelId?: string | null;
|
|
66
|
+
"onUpdate:modelId"?: (id: string | null) => void;
|
|
67
|
+
/** The model is chosen at submit: `useComposer` asks the reasoning model. Turning it on clears the pick. */
|
|
68
|
+
autoSelectModel?: boolean;
|
|
69
|
+
/** An "Auto-select model" switch at the top of the Model menu, for the end user. */
|
|
70
|
+
showAutoSelectToggle?: boolean;
|
|
71
|
+
"onUpdate:autoSelectModel"?: (value: boolean) => void;
|
|
72
|
+
/** Your own labels or a shorter list; ratios the models don't take are left out either way. */
|
|
73
|
+
aspectRatios?: ParameterOption<string>[];
|
|
74
|
+
aspectRatio?: string | null;
|
|
75
|
+
"onUpdate:aspectRatio"?: (value: string) => void;
|
|
76
|
+
/** Shows the Enhance button; your handler runs the rewrite and updates the prompt. Gets the current prompt. */
|
|
77
|
+
onEnhance?: (value: string) => void;
|
|
78
|
+
/** True while your Enhance handler is running. */
|
|
79
|
+
enhancing?: boolean;
|
|
80
|
+
/** The last submit's failure: submit becomes retry, and the message shows inline. */
|
|
81
|
+
submitError?: string | null;
|
|
82
|
+
/** Overrides the built-in "empty prompt and no attachments" check. Ignored while `submitError` is set. */
|
|
83
|
+
submitDisabled?: boolean;
|
|
84
|
+
/** True while a request is in flight: with `@abort`, submit becomes stop; without, it's disabled. */
|
|
85
|
+
submitting?: boolean;
|
|
86
|
+
onAbort?: () => void;
|
|
87
|
+
onAttachMenuSelect?: (action: ComposerAttachMenuAction) => void;
|
|
88
|
+
/** Which + menu items to show. Each still shows only when it can work. */
|
|
89
|
+
attachMenuActions?: ComposerAttachMenuAction[];
|
|
90
|
+
/** An edit's regions (from `useComposer`'s `edit`), shown as colored chips while editing. */
|
|
91
|
+
regions?: EditRegion[];
|
|
92
|
+
"onUpdate:regions"?: (next: EditRegion[]) => void;
|
|
93
|
+
disabled?: boolean;
|
|
94
|
+
}>(),
|
|
95
|
+
{
|
|
96
|
+
attachments: () => [],
|
|
97
|
+
useCase: null,
|
|
98
|
+
useCases: () => [],
|
|
99
|
+
modelIds: () => [],
|
|
100
|
+
modelId: null,
|
|
101
|
+
aspectRatio: null,
|
|
102
|
+
submitError: null,
|
|
103
|
+
// Left undefined unless set: Vue would make an absent boolean `false`,
|
|
104
|
+
// which reads as "never disable".
|
|
105
|
+
submitDisabled: undefined,
|
|
106
|
+
regions: () => [],
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
const emit = defineEmits<{
|
|
110
|
+
"update:modelValue": [value: string];
|
|
111
|
+
/** Everything Composer tracks. Also what retry sends, with the same payload, when `submitError` is set. */
|
|
112
|
+
submit: [payload: ComposerSubmitPayload];
|
|
113
|
+
unsupportedFile: [fileName: string];
|
|
114
|
+
}>();
|
|
115
|
+
|
|
116
|
+
const view = computed(() =>
|
|
117
|
+
composerView({
|
|
118
|
+
...props,
|
|
119
|
+
value: props.modelValue,
|
|
120
|
+
onModelChange: props["onUpdate:modelId"],
|
|
121
|
+
onModelIdsChange: props["onUpdate:modelIds"],
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
const menuItems = computed(() =>
|
|
125
|
+
composerAttachMenuItems(props.attachMenuActions, {
|
|
126
|
+
canAddMedia: Boolean(props["onUpdate:attachments"]),
|
|
127
|
+
canSelect: Boolean(props.onAttachMenuSelect),
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
const modelMenuOptions = computed(() =>
|
|
131
|
+
props.multiSelectUseCases
|
|
132
|
+
? { sections: view.value.modelSections.map((s) => ({ label: s.label, options: s.options })) }
|
|
133
|
+
: { options: view.value.modelSections[0]?.options }
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Auto-select chooses at submit, so turning it on clears the current pick
|
|
137
|
+
// and the trigger reads "Auto-select" until routing fills in the model.
|
|
138
|
+
watch(
|
|
139
|
+
() => props.autoSelectModel,
|
|
140
|
+
(on, was) => {
|
|
141
|
+
if (!on || was) return;
|
|
142
|
+
if (view.value.multiModel) props["onUpdate:modelIds"]?.([]);
|
|
143
|
+
else props["onUpdate:modelId"]?.(null);
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
function toggleModel(id: string) {
|
|
148
|
+
if (view.value.multiModel) props["onUpdate:modelIds"]?.(view.value.toggledModelIds(id));
|
|
149
|
+
else props["onUpdate:modelId"]?.(id);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Grows with wrapped text from one line, instead of a fixed row count.
|
|
153
|
+
const textarea = ref<HTMLTextAreaElement>();
|
|
154
|
+
watch(
|
|
155
|
+
() => props.modelValue,
|
|
156
|
+
() => {
|
|
157
|
+
const el = textarea.value;
|
|
158
|
+
if (!el) return;
|
|
159
|
+
el.style.height = "auto";
|
|
160
|
+
el.style.height = `${el.scrollHeight}px`;
|
|
161
|
+
},
|
|
162
|
+
{ flush: "post", immediate: true }
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Enhance and undo. The prompt already holds the rewrite once `enhancing`
|
|
166
|
+
// drops; the reveal is a decorative overlay painting it in, then gone.
|
|
167
|
+
const originalValue = ref<string | null>(null);
|
|
168
|
+
const enhancedValue = ref<string | null>(null);
|
|
169
|
+
const animatingText = ref<string | null>(null);
|
|
170
|
+
const revealedLength = ref(0);
|
|
171
|
+
let raf = 0;
|
|
172
|
+
|
|
173
|
+
watch(
|
|
174
|
+
() => props.enhancing,
|
|
175
|
+
(now, was) => {
|
|
176
|
+
if (!was || now || originalValue.value == null || props.modelValue === originalValue.value) return;
|
|
177
|
+
const value = props.modelValue;
|
|
178
|
+
enhancedValue.value = value;
|
|
179
|
+
// Long rewrites and reduced-motion requests appear at once.
|
|
180
|
+
if (value.length > MAX_ANIMATE_ENHANCE_LENGTH || window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
|
|
181
|
+
animatingText.value = value;
|
|
182
|
+
revealedLength.value = 0;
|
|
183
|
+
const duration = enhanceRevealDurationMs(value.length);
|
|
184
|
+
const start = performance.now();
|
|
185
|
+
const tick = (time: number) => {
|
|
186
|
+
revealedLength.value = Math.min(value.length, Math.floor(((time - start) / duration) * value.length));
|
|
187
|
+
if (revealedLength.value < value.length) raf = requestAnimationFrame(tick);
|
|
188
|
+
else animatingText.value = null;
|
|
189
|
+
};
|
|
190
|
+
raf = requestAnimationFrame(tick);
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
onScopeDispose(() => cancelAnimationFrame(raf));
|
|
194
|
+
|
|
195
|
+
const animating = computed(() => animatingText.value != null);
|
|
196
|
+
const isEnhanced = computed(() => !animating.value && enhancedValue.value != null && props.modelValue === enhancedValue.value);
|
|
197
|
+
const enhanceBusy = computed(() => Boolean(props.enhancing) || animating.value);
|
|
198
|
+
|
|
199
|
+
function onOptimizeClick() {
|
|
200
|
+
if (isEnhanced.value) {
|
|
201
|
+
const original = originalValue.value;
|
|
202
|
+
enhancedValue.value = null;
|
|
203
|
+
originalValue.value = null;
|
|
204
|
+
if (original != null) emit("update:modelValue", original);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
originalValue.value = props.modelValue; // for undo, before the rewrite lands
|
|
208
|
+
props.onEnhance?.(props.modelValue);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function onFiles(files: FileList | null) {
|
|
212
|
+
const onAttachments = props["onUpdate:attachments"];
|
|
213
|
+
if (!files || files.length === 0 || !onAttachments) return;
|
|
214
|
+
// Edit mode works on one image: a new one replaces it, and the edit starts over.
|
|
215
|
+
const { attachments, unsupported } = await composerAttachmentsFromFiles(files, props.attachments, view.value.editing);
|
|
216
|
+
unsupported.forEach((name) => emit("unsupportedFile", name));
|
|
217
|
+
if (attachments) onAttachments(attachments);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function removeUseCase(useCase: ComposerUseCase) {
|
|
221
|
+
if (props.multiSelectUseCases) props["onUpdate:useCases"]?.(props.useCases.filter((u) => u.kind !== useCase.kind));
|
|
222
|
+
else props.onClearUseCase?.();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function onSubmitClick() {
|
|
226
|
+
if (view.value.showStop) props.onAbort?.();
|
|
227
|
+
else if (!view.value.isSubmitDisabled && !props.submitting) emit("submit", view.value.payload());
|
|
228
|
+
}
|
|
229
|
+
</script>
|
|
230
|
+
|
|
231
|
+
<template>
|
|
232
|
+
<div class="chai-composer">
|
|
233
|
+
<div v-if="attachments.length > 0" class="chai-composer__thumbnails">
|
|
234
|
+
<div v-for="a in attachments" :key="a.id" class="chai-composer__thumb">
|
|
235
|
+
<img v-if="a.kind === 'image'" class="chai-composer__thumb-img" :src="a.src" alt="" />
|
|
236
|
+
<span v-else class="chai-composer__thumb-icon"><VideocamIcon v-if="a.kind === 'video'" /><MusicNoteIcon v-else-if="a.kind === 'audio'" /><ImageIcon v-else /></span>
|
|
237
|
+
<button
|
|
238
|
+
v-if="props['onUpdate:attachments']"
|
|
239
|
+
type="button"
|
|
240
|
+
class="chai-composer__thumb-remove"
|
|
241
|
+
aria-label="Remove attachment"
|
|
242
|
+
@click="props['onUpdate:attachments']?.(attachments.filter((x) => x.id !== a.id))"
|
|
243
|
+
>
|
|
244
|
+
<CloseSymbolIcon />
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<ul v-if="view.editing && regions.length > 0" class="chai-composer__regions" aria-label="Edit regions">
|
|
250
|
+
<li v-for="r in regions" :key="r.id">
|
|
251
|
+
<Chip
|
|
252
|
+
:label="`Region ${r.number}`"
|
|
253
|
+
:region-number="r.number"
|
|
254
|
+
:removable="Boolean(props['onUpdate:regions'])"
|
|
255
|
+
:disabled="disabled"
|
|
256
|
+
@remove="props['onUpdate:regions']?.(regions.filter((x) => x.id !== r.id))"
|
|
257
|
+
/>
|
|
258
|
+
</li>
|
|
259
|
+
</ul>
|
|
260
|
+
|
|
261
|
+
<div class="chai-composer__field">
|
|
262
|
+
<textarea
|
|
263
|
+
ref="textarea"
|
|
264
|
+
:class="['chai-composer__input chai-composer__type', { 'chai-composer__input--revealing': animating }]"
|
|
265
|
+
:value="modelValue"
|
|
266
|
+
:placeholder="view.placeholder"
|
|
267
|
+
:aria-label="promptLabel ?? view.placeholder"
|
|
268
|
+
rows="1"
|
|
269
|
+
:disabled="disabled || animating"
|
|
270
|
+
@input="emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
|
|
271
|
+
/>
|
|
272
|
+
<!-- The Enhance reveal: decorative, so hidden from screen readers, which read the textarea's final text. -->
|
|
273
|
+
<div v-if="animating" class="chai-composer__reveal chai-composer__type" aria-hidden="true">
|
|
274
|
+
<span class="chai-composer__reveal-text">{{ animatingText!.slice(0, revealedLength) }}</span>
|
|
275
|
+
</div>
|
|
276
|
+
<!-- Optimize, or once a rewrite lands, undo it: until the prompt is edited by hand. -->
|
|
277
|
+
<button
|
|
278
|
+
v-if="onEnhance"
|
|
279
|
+
type="button"
|
|
280
|
+
:class="['chai-composer__optimize', { 'chai-composer__optimize--active': enhanceBusy }]"
|
|
281
|
+
:disabled="disabled || (isEnhanced ? false : !modelValue.trim() || enhanceBusy)"
|
|
282
|
+
:aria-label="isEnhanced ? 'Revert to original prompt' : 'Optimize prompt'"
|
|
283
|
+
:title="isEnhanced ? 'Revert to original prompt' : 'Optimize prompt'"
|
|
284
|
+
@click="onOptimizeClick"
|
|
285
|
+
>
|
|
286
|
+
<UndoIcon v-if="isEnhanced" /><OptimizeIcon v-else />
|
|
287
|
+
</button>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<p v-if="submitError" class="chai-composer__error" role="alert"><ErrorIcon />{{ submitError }}</p>
|
|
291
|
+
|
|
292
|
+
<div class="chai-composer__controls">
|
|
293
|
+
<div class="chai-composer__controls-start">
|
|
294
|
+
<AttachMenu
|
|
295
|
+
v-if="menuItems.length > 0"
|
|
296
|
+
:items="menuItems"
|
|
297
|
+
:images-only="view.editing"
|
|
298
|
+
:disabled="disabled"
|
|
299
|
+
@select="onAttachMenuSelect?.($event)"
|
|
300
|
+
@files="onFiles"
|
|
301
|
+
/>
|
|
302
|
+
|
|
303
|
+
<Chip
|
|
304
|
+
v-for="u in view.pickedUseCases"
|
|
305
|
+
:key="u.kind"
|
|
306
|
+
:label="u.label"
|
|
307
|
+
:removable="Boolean(multiSelectUseCases ? props['onUpdate:useCases'] : onClearUseCase)"
|
|
308
|
+
:disabled="disabled"
|
|
309
|
+
@remove="removeUseCase(u)"
|
|
310
|
+
/>
|
|
311
|
+
|
|
312
|
+
<!-- The auto-select switch lives inside the Model menu, so with it shown the trigger stays enabled: it's the only way back to the switch. -->
|
|
313
|
+
<SearchMenu
|
|
314
|
+
v-if="view.modelSections.length > 0 && view.canPickModel"
|
|
315
|
+
v-bind="modelMenuOptions"
|
|
316
|
+
:value="view.multiModel ? view.pickedModelIds : modelId"
|
|
317
|
+
:multiple="view.multiModel"
|
|
318
|
+
:toggle-header="
|
|
319
|
+
showAutoSelectToggle
|
|
320
|
+
? { label: 'Auto-select model', value: autoSelectModel, onChange: (v: boolean) => props['onUpdate:autoSelectModel']?.(v) }
|
|
321
|
+
: undefined
|
|
322
|
+
"
|
|
323
|
+
:trigger-label="view.modelTriggerLabel"
|
|
324
|
+
:menu-label="view.multiModel ? 'Select models' : 'Select model'"
|
|
325
|
+
:searchable="false"
|
|
326
|
+
trigger-variant="text"
|
|
327
|
+
:disabled="disabled || (autoSelectModel && !showAutoSelectToggle)"
|
|
328
|
+
:panel-width="showAutoSelectToggle ? 320 : undefined"
|
|
329
|
+
@select="toggleModel($event.id)"
|
|
330
|
+
/>
|
|
331
|
+
|
|
332
|
+
<SearchMenu
|
|
333
|
+
v-if="view.showAspectRatio && view.aspectOptions.length > 0 && props['onUpdate:aspectRatio']"
|
|
334
|
+
:options="view.aspectOptions.map((o) => ({ id: o.value, label: o.label }))"
|
|
335
|
+
:value="view.offeredAspect"
|
|
336
|
+
:trigger-label="view.selectedAspect?.label ?? 'Aspect ratio'"
|
|
337
|
+
menu-label="Aspect ratio"
|
|
338
|
+
:searchable="false"
|
|
339
|
+
trigger-variant="text"
|
|
340
|
+
:disabled="disabled"
|
|
341
|
+
@select="props['onUpdate:aspectRatio']?.($event.id)"
|
|
342
|
+
/>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<!-- MD3's md-fab has no disabled state of its own; Chai's unfilled-until-ready look comes from `data-ready` in style.css. `label=""` keeps it a plain icon FAB. -->
|
|
346
|
+
<md-fab
|
|
347
|
+
class="chai-composer__submit"
|
|
348
|
+
size="small"
|
|
349
|
+
variant="secondary"
|
|
350
|
+
label=""
|
|
351
|
+
:data-ready="view.showStop || !view.isSubmitDisabled || undefined"
|
|
352
|
+
:data-error="Boolean(submitError) || undefined"
|
|
353
|
+
:data-stop="view.showStop || undefined"
|
|
354
|
+
:aria-label="view.showStop ? 'Stop' : submitError ? 'Retry' : 'Submit'"
|
|
355
|
+
:aria-disabled="view.showStop ? 'false' : submitting || view.isSubmitDisabled ? 'true' : 'false'"
|
|
356
|
+
@click="onSubmitClick"
|
|
357
|
+
>
|
|
358
|
+
<span slot="icon"><StopIcon v-if="view.showStop" /><RetryIcon v-else-if="submitError" /><ArrowForwardIcon v-else /></span>
|
|
359
|
+
</md-fab>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</template>
|