@chai-ui/vue 0.0.0-stage → 0.7.1
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 +2521 -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 +372 -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,77 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** The card's front: the result's image, video, audio or text, or its pending, stopped or failed state, with Download, Share and Expand over it. */
|
|
3
|
+
import { computed } from "vue";
|
|
4
|
+
import type { Result } from "@chai-ui/core";
|
|
5
|
+
import { CheckSymbolIcon, DownloadIcon, ExpandIcon, ShareIcon } from "../icons.js";
|
|
6
|
+
import IconButton from "./IconButton.vue";
|
|
7
|
+
import ResultText from "./ResultText.vue";
|
|
8
|
+
import VideoPlayer from "./VideoPlayer.vue";
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
result: Result;
|
|
12
|
+
alt: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
mediaExpanded: boolean;
|
|
15
|
+
linkCopied: boolean;
|
|
16
|
+
videoTime: { current: number };
|
|
17
|
+
videoWasPlaying: boolean;
|
|
18
|
+
}>();
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
download: [];
|
|
21
|
+
share: [];
|
|
22
|
+
expand: [];
|
|
23
|
+
dimensions: [width: number, height: number];
|
|
24
|
+
videoPlayingChange: [playing: boolean];
|
|
25
|
+
}>();
|
|
26
|
+
|
|
27
|
+
const kind = computed(() => props.result.output?.kind);
|
|
28
|
+
// "Running with output" is a text result streaming in: show the partial text, not the spinner.
|
|
29
|
+
const streaming = computed(() => props.result.status === "running" && kind.value === "text");
|
|
30
|
+
const pending = computed(() => (props.result.status === "queued" || props.result.status === "running") && !streaming.value);
|
|
31
|
+
|
|
32
|
+
function onImageLoad(e: Event) {
|
|
33
|
+
const img = e.currentTarget as HTMLImageElement;
|
|
34
|
+
emit("dimensions", img.naturalWidth, img.naturalHeight);
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<div v-if="pending" class="chai-result-card__media chai-result-card__media--pending">
|
|
40
|
+
<span class="chai-result-card__spinner" role="img" :aria-label="result.status === 'running' ? 'Running' : 'Queued'" />
|
|
41
|
+
</div>
|
|
42
|
+
<!-- Stopped before any output. A stopped stream keeps its partial text and renders below like any other. -->
|
|
43
|
+
<div v-else-if="result.status === 'cancelled' && !result.output" class="chai-result-card__media chai-result-card__media--cancelled">
|
|
44
|
+
<p class="chai-result-card__cancelled-text">Stopped</p>
|
|
45
|
+
</div>
|
|
46
|
+
<div v-else-if="result.status === 'error'" class="chai-result-card__media chai-result-card__media--error">
|
|
47
|
+
<p class="chai-result-card__error-text">{{ result.error?.message ?? "Something went wrong." }}</p>
|
|
48
|
+
</div>
|
|
49
|
+
<div v-else-if="!result.output" class="chai-result-card__media" />
|
|
50
|
+
<div v-else :class="['chai-result-card__media', { 'chai-result-card__media--fixed-height': kind === 'audio' || kind === 'text' }]">
|
|
51
|
+
<template v-if="kind === 'video'">
|
|
52
|
+
<!-- While expanded, the playing video is in the overlay; a still frame holds the layout here. -->
|
|
53
|
+
<img v-if="mediaExpanded" class="chai-result-card__image" :src="result.output.src" alt="" />
|
|
54
|
+
<VideoPlayer
|
|
55
|
+
v-else
|
|
56
|
+
:src="result.output.src"
|
|
57
|
+
:time="videoTime"
|
|
58
|
+
:was-playing="videoWasPlaying"
|
|
59
|
+
@playing-change="emit('videoPlayingChange', $event)"
|
|
60
|
+
@dimensions="(w, h) => emit('dimensions', w, h)"
|
|
61
|
+
/>
|
|
62
|
+
</template>
|
|
63
|
+
<audio v-else-if="kind === 'audio'" class="chai-result-card__audio" :src="result.output.src" controls />
|
|
64
|
+
<ResultText v-else-if="kind === 'text'" :text="result.output.src" :streaming="streaming" />
|
|
65
|
+
<img v-else class="chai-result-card__image" :src="result.output.src" :alt="alt" @load="onImageLoad" />
|
|
66
|
+
|
|
67
|
+
<div v-if="!mediaExpanded && !streaming" class="chai-card-top chai-card-top--end">
|
|
68
|
+
<IconButton variant="top" label="Download" :disabled="disabled" @click="emit('download')"><DownloadIcon /></IconButton>
|
|
69
|
+
<IconButton variant="top" :label="linkCopied ? 'Link copied' : 'Share'" :disabled="disabled" @click="emit('share')">
|
|
70
|
+
<CheckSymbolIcon v-if="linkCopied" /><ShareIcon v-else />
|
|
71
|
+
</IconButton>
|
|
72
|
+
<IconButton v-if="kind === 'video' || kind === 'image'" variant="top" label="Expand" :disabled="disabled" @click="emit('expand')">
|
|
73
|
+
<ExpandIcon />
|
|
74
|
+
</IconButton>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A text result, streaming or finished. One element for both states, so the
|
|
4
|
+
* scroll position survives the moment a stream completes. Past its max
|
|
5
|
+
* height it scrolls; while streaming it follows the newest text, unless the
|
|
6
|
+
* person has scrolled up to read, in which case it stays where they are.
|
|
7
|
+
*/
|
|
8
|
+
import { onMounted, ref, watch } from "vue";
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{ text: string; streaming: boolean }>();
|
|
11
|
+
const el = ref<HTMLElement>();
|
|
12
|
+
let follow = true;
|
|
13
|
+
|
|
14
|
+
function followNewest() {
|
|
15
|
+
if (el.value && props.streaming && follow) el.value.scrollTop = el.value.scrollHeight;
|
|
16
|
+
}
|
|
17
|
+
// A frame later on mount: set during mount, before the card's first paint,
|
|
18
|
+
// the scroll position didn't stick in Chromium (React's effect runs after paint).
|
|
19
|
+
onMounted(() => requestAnimationFrame(followNewest));
|
|
20
|
+
watch(() => [props.text, props.streaming], followNewest, { flush: "post" });
|
|
21
|
+
|
|
22
|
+
function onScroll(e: Event) {
|
|
23
|
+
const p = e.currentTarget as HTMLElement;
|
|
24
|
+
follow = p.scrollTop + p.clientHeight >= p.scrollHeight - 24;
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<!-- aria-busy: screen readers wait for the finished text instead of announcing every chunk. tabindex: keyboard users can scroll a long answer. -->
|
|
30
|
+
<p
|
|
31
|
+
ref="el"
|
|
32
|
+
:class="['chai-result-card__text-output', { 'chai-result-card__text-output--streaming': streaming }]"
|
|
33
|
+
:aria-busy="streaming"
|
|
34
|
+
tabindex="0"
|
|
35
|
+
@scroll="onScroll"
|
|
36
|
+
>{{ text }}</p>
|
|
37
|
+
</template>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A real `<video>` with slim play/pause and scrubber chrome built on its own
|
|
4
|
+
* playback API, not the browser's heavier native controls. Mounted twice
|
|
5
|
+
* (inline, and in the expand overlay), so `time` and `wasPlaying` carry the
|
|
6
|
+
* position and play state across the swap.
|
|
7
|
+
*/
|
|
8
|
+
import { ref } from "vue";
|
|
9
|
+
import { PlayIcon } from "../icons.js";
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{ src: string; expanded?: boolean; time: { current: number }; wasPlaying: boolean }>();
|
|
12
|
+
const emit = defineEmits<{ playingChange: [playing: boolean]; dimensions: [width: number, height: number] }>();
|
|
13
|
+
const video = ref<HTMLVideoElement>();
|
|
14
|
+
const playing = ref(props.wasPlaying);
|
|
15
|
+
const progress = ref(0);
|
|
16
|
+
|
|
17
|
+
function togglePlay() {
|
|
18
|
+
const el = video.value!;
|
|
19
|
+
if (el.paused) el.play();
|
|
20
|
+
else el.pause();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function onLoadedMetadata() {
|
|
24
|
+
const el = video.value!;
|
|
25
|
+
el.currentTime = props.time.current;
|
|
26
|
+
if (props.wasPlaying) el.play().catch(() => {});
|
|
27
|
+
emit("dimensions", el.videoWidth, el.videoHeight);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function setPlaying(value: boolean) {
|
|
31
|
+
playing.value = value;
|
|
32
|
+
emit("playingChange", value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function onTimeUpdate() {
|
|
36
|
+
const el = video.value!;
|
|
37
|
+
props.time.current = el.currentTime;
|
|
38
|
+
if (el.duration) progress.value = el.currentTime / el.duration;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function scrub(e: MouseEvent) {
|
|
42
|
+
const el = video.value!;
|
|
43
|
+
if (!el.duration) return;
|
|
44
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
45
|
+
el.currentTime = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width)) * el.duration;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Keyboard seeking, the slider pattern: arrows move 5s, Home/End jump.
|
|
49
|
+
const STEPS: Record<string, number> = { ArrowRight: 5, ArrowUp: 5, ArrowLeft: -5, ArrowDown: -5 };
|
|
50
|
+
function onScrubKey(e: KeyboardEvent) {
|
|
51
|
+
const el = video.value!;
|
|
52
|
+
if (!el.duration) return;
|
|
53
|
+
const step = STEPS[e.key];
|
|
54
|
+
if (step != null) el.currentTime = Math.min(el.duration, Math.max(0, el.currentTime + step));
|
|
55
|
+
else if (e.key === "Home") el.currentTime = 0;
|
|
56
|
+
else if (e.key === "End") el.currentTime = el.duration;
|
|
57
|
+
else return;
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<video
|
|
64
|
+
ref="video"
|
|
65
|
+
class="chai-result-card__video"
|
|
66
|
+
:src="src"
|
|
67
|
+
@loadedmetadata="onLoadedMetadata"
|
|
68
|
+
@play="setPlaying(true)"
|
|
69
|
+
@pause="setPlaying(false)"
|
|
70
|
+
@timeupdate="onTimeUpdate"
|
|
71
|
+
/>
|
|
72
|
+
<!-- One control over the whole video: click, or Tab then Space/Enter, to play or pause. The round Play badge is just its paused look. -->
|
|
73
|
+
<button type="button" class="chai-result-card__video-toggle" :aria-label="playing ? 'Pause' : 'Play'" @click="togglePlay">
|
|
74
|
+
<span v-if="!playing" class="chai-result-card__play"><PlayIcon /></span>
|
|
75
|
+
</button>
|
|
76
|
+
<div
|
|
77
|
+
:class="['chai-result-card__scrubber', { 'chai-result-card__scrubber--expanded': expanded }]"
|
|
78
|
+
role="slider"
|
|
79
|
+
tabindex="0"
|
|
80
|
+
aria-label="Seek"
|
|
81
|
+
:aria-valuemin="0"
|
|
82
|
+
:aria-valuemax="100"
|
|
83
|
+
:aria-valuenow="Math.round(progress * 100)"
|
|
84
|
+
:aria-valuetext="`${Math.round(progress * 100)}%`"
|
|
85
|
+
@click="scrub"
|
|
86
|
+
@keydown="onScrubKey"
|
|
87
|
+
>
|
|
88
|
+
<div class="chai-result-card__scrubber-fill" :style="{ width: `${progress * 100}%` }" />
|
|
89
|
+
</div>
|
|
90
|
+
</template>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Component } from "vue";
|
|
2
|
+
|
|
3
|
+
/** One row in the "more" (⋯) menu, for app-specific actions (e.g. "Flag content"). Each one just emits `action` with this `id`. */
|
|
4
|
+
export interface ResultCardMoreAction {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: Component;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ResultCardVote = "like" | "dislike" | null;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { computed, onScopeDispose, shallowRef, toValue, type MaybeRefOrGetter } from "vue";
|
|
2
|
+
import { createComposerStore, pickReasoning, type ComposerOptions } from "@chai-ui/core";
|
|
3
|
+
import { useReasoning } from "./reasoning.js";
|
|
4
|
+
|
|
5
|
+
export type UseComposerOptions = ComposerOptions;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The Vue binding for `createComposerStore` in `@chai-ui/core`: turns a
|
|
9
|
+
* `Composer`'s submit payload into a real `Request` → `Run` → `Result`.
|
|
10
|
+
* Pass a getter (or ref) for options that change, e.g. `() => ({ engine, editImage: image.value })`.
|
|
11
|
+
*/
|
|
12
|
+
export function useComposer(options: MaybeRefOrGetter<UseComposerOptions> = {}) {
|
|
13
|
+
const provided = useReasoning();
|
|
14
|
+
const store = createComposerStore(
|
|
15
|
+
() => toValue(options),
|
|
16
|
+
() => provided.value
|
|
17
|
+
);
|
|
18
|
+
const state = shallowRef(store.getState());
|
|
19
|
+
onScopeDispose(
|
|
20
|
+
store.subscribe(() => (state.value = store.getState())),
|
|
21
|
+
true
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
run: computed(() => state.value.runs[0] ?? null),
|
|
26
|
+
runs: computed(() => state.value.runs),
|
|
27
|
+
submit: store.submit,
|
|
28
|
+
cancel: store.cancel,
|
|
29
|
+
/** `undefined` without reasoning, so the Composer hides its Enhance button. */
|
|
30
|
+
enhance: computed(() => (pickReasoning(toValue(options), provided.value) ? store.enhance : undefined)),
|
|
31
|
+
enhancing: computed(() => state.value.enhancing),
|
|
32
|
+
routing: computed(() => state.value.routing),
|
|
33
|
+
/** Props for an `EditCard`, and `regions` for `Composer`. `null` when there's no `editImage`. */
|
|
34
|
+
edit: computed(() => store.edit(state.value, toValue(options).editImage)),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type UseComposerReturn = ReturnType<typeof useComposer>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { computed, onScopeDispose, shallowRef, toValue, type MaybeRefOrGetter } from "vue";
|
|
2
|
+
import { createMediaAnalyzerStore, type MediaAnalyzerOptions } from "@chai-ui/core";
|
|
3
|
+
import { useReasoning } from "./reasoning.js";
|
|
4
|
+
|
|
5
|
+
export type UseMediaAnalyzerOptions = MediaAnalyzerOptions;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The Vue binding for `createMediaAnalyzerStore` in `@chai-ui/core`, behind
|
|
9
|
+
* `MediaAnalyzer`'s submit: `prompt` fills in as the text streams.
|
|
10
|
+
*/
|
|
11
|
+
export function useMediaAnalyzer(options: MaybeRefOrGetter<UseMediaAnalyzerOptions> = {}) {
|
|
12
|
+
const provided = useReasoning();
|
|
13
|
+
const store = createMediaAnalyzerStore(
|
|
14
|
+
() => toValue(options),
|
|
15
|
+
() => provided.value
|
|
16
|
+
);
|
|
17
|
+
const state = shallowRef(store.getState());
|
|
18
|
+
onScopeDispose(
|
|
19
|
+
store.subscribe(() => (state.value = store.getState())),
|
|
20
|
+
true
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
analyzing: computed(() => state.value.analyzing),
|
|
25
|
+
error: computed(() => state.value.error),
|
|
26
|
+
prompt: computed(() => state.value.prompt),
|
|
27
|
+
submit: store.submit,
|
|
28
|
+
cancel: store.cancel,
|
|
29
|
+
reset: store.reset,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type UseMediaAnalyzerReturn = ReturnType<typeof useMediaAnalyzer>;
|