@chai-ui/react 0.5.0 → 0.6.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.
Files changed (54) hide show
  1. package/DESIGN.md +6 -1
  2. package/README.md +79 -17
  3. package/dist/ChaiProvider.d.ts +3 -42
  4. package/dist/ChaiProvider.d.ts.map +1 -1
  5. package/dist/ChaiProvider.js +5 -43
  6. package/dist/ChaiProvider.js.map +1 -1
  7. package/dist/Composer.d.ts +10 -60
  8. package/dist/Composer.d.ts.map +1 -1
  9. package/dist/Composer.js +26 -24
  10. package/dist/Composer.js.map +1 -1
  11. package/dist/EditCard.d.ts +2 -16
  12. package/dist/EditCard.d.ts.map +1 -1
  13. package/dist/EditCard.js +7 -2
  14. package/dist/EditCard.js.map +1 -1
  15. package/dist/MediaAnalyzer.d.ts +14 -18
  16. package/dist/MediaAnalyzer.d.ts.map +1 -1
  17. package/dist/MediaAnalyzer.js +26 -8
  18. package/dist/MediaAnalyzer.js.map +1 -1
  19. package/dist/ResultCard.d.ts.map +1 -1
  20. package/dist/ResultCard.js +10 -6
  21. package/dist/ResultCard.js.map +1 -1
  22. package/dist/icons.d.ts +1 -0
  23. package/dist/icons.d.ts.map +1 -1
  24. package/dist/icons.js +9 -0
  25. package/dist/icons.js.map +1 -1
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/media-actions.d.ts +5 -2
  31. package/dist/media-actions.d.ts.map +1 -1
  32. package/dist/media-actions.js +12 -3
  33. package/dist/media-actions.js.map +1 -1
  34. package/dist/style.css +15 -0
  35. package/dist/useComposer.d.ts +11 -86
  36. package/dist/useComposer.d.ts.map +1 -1
  37. package/dist/useComposer.js +26 -291
  38. package/dist/useComposer.js.map +1 -1
  39. package/dist/useMediaAnalyzer.d.ts +8 -28
  40. package/dist/useMediaAnalyzer.d.ts.map +1 -1
  41. package/dist/useMediaAnalyzer.js +9 -106
  42. package/dist/useMediaAnalyzer.js.map +1 -1
  43. package/package.json +3 -3
  44. package/src/ChaiProvider.tsx +12 -70
  45. package/src/Composer.tsx +42 -90
  46. package/src/EditCard.tsx +18 -20
  47. package/src/MediaAnalyzer.tsx +51 -30
  48. package/src/ResultCard.tsx +12 -4
  49. package/src/icons.tsx +14 -0
  50. package/src/index.ts +10 -0
  51. package/src/media-actions.ts +13 -4
  52. package/src/style.css +15 -0
  53. package/src/useComposer.ts +33 -411
  54. package/src/useMediaAnalyzer.ts +15 -146
@@ -1,157 +1,26 @@
1
- import { useCallback, useRef, useState } from "react";
2
- import {
3
- createMockEngine,
4
- DEFAULT_MEDIA_ANALYSIS_PROMPT_LENGTH,
5
- isAbortError,
6
- MEDIA_ANALYSIS_PROMPTS,
7
- normalizeGenerationError,
8
- routeModel,
9
- type GenerationEngine,
10
- type ModelRoutingInput,
11
- type ModelRoutingResult,
12
- } from "@chai-ui/core";
13
- import type { MediaAnalyzerSubmitPayload } from "./MediaAnalyzer.js";
14
- import { reasoningModelFor, useReasoning } from "./ChaiProvider.js";
1
+ import { useState, useSyncExternalStore } from "react";
2
+ import { createMediaAnalyzerStore, type MediaAnalyzerOptions } from "@chai-ui/core";
3
+ import { useReasoning } from "./ChaiProvider.js";
15
4
  import { useLatest } from "./use-latest.js";
16
5
 
17
- const mockTextEngine = createMockEngine({ outputKind: "text" });
18
-
19
- export interface UseMediaAnalyzerOptions {
20
- /**
21
- * Overrides the nearest `ChaiProvider`'s reasoning engine. With neither,
22
- * a mock text engine answers, so a first render works with no key.
23
- */
24
- engine?: GenerationEngine;
25
- /** The model when the person hasn't picked one. Defaults to the provider's reasoning model for the media's kind. */
26
- model?: string;
27
- onResult?: (prompt: string) => void;
28
- onError?: (message: string) => void;
29
- /** Auto-select: fires with the routed model so the trigger shows it. Wire it to `MediaAnalyzer`'s `onModelChange` state. */
30
- onModelChange?: (modelId: string) => void;
31
- /** Replaces the routing instruction (`buildModelRoutingPrompt` in `@chai-ui/core`). */
32
- routingPrompt?: (input: ModelRoutingInput) => string;
33
- /** Fires when auto-select fell back to the first listed model (no reasoning model, a failed call, or an unlisted reply). Analysis still runs. */
34
- onRoutingFallback?: (result: Extract<ModelRoutingResult, { status: "fallback" }>) => void;
35
- }
6
+ export type UseMediaAnalyzerOptions = MediaAnalyzerOptions;
36
7
 
37
8
  /**
38
- * The binding behind `MediaAnalyzer`'s `onSubmit`: sends the attached
39
- * media and the instruction for its kind and prompt length
40
- * (`MEDIA_ANALYSIS_PROMPTS`) to the reasoning engine, and returns the
41
- * generated prompt. `prompt` fills in as the text streams.
9
+ * The React binding for `createMediaAnalyzerStore` in `@chai-ui/core`, behind
10
+ * `MediaAnalyzer`'s `onSubmit`: returns the generated prompt, which fills in
11
+ * as the text streams.
42
12
  */
43
13
  export function useMediaAnalyzer(options: UseMediaAnalyzerOptions = {}) {
44
- const [analyzing, setAnalyzing] = useState(false);
45
- const [error, setError] = useState<string | null>(null);
46
- const [prompt, setPrompt] = useState<string | null>(null);
47
-
48
14
  const provided = useReasoning();
49
15
  const latest = useLatest({ options, provided });
50
- // Each submit gets an id; a late reply from an earlier submit is dropped,
51
- // and its call aborted.
52
- const callId = useRef(0);
53
- const controller = useRef<AbortController | null>(null);
54
-
55
- const submit = useCallback((payload: MediaAnalyzerSubmitPayload) => {
56
- const { options: opts, provided: reasoning } = latest.current;
57
- const id = ++callId.current;
58
- controller.current?.abort();
59
- const { signal } = (controller.current = new AbortController());
60
- setError(null);
61
- setPrompt(null);
62
-
63
- const kind = payload.attachments[0]?.kind;
64
- if (kind !== "image" && kind !== "video" && kind !== "audio") {
65
- const message = "Attach an image, video, or audio file to analyze.";
66
- setError(message);
67
- opts.onError?.(message);
68
- return;
69
- }
70
-
71
- const engine = opts.engine ?? reasoning?.engine ?? mockTextEngine;
72
- const defaultModel = opts.model ?? (reasoning ? reasoningModelFor(reasoning, payload.attachments) : undefined);
73
- const instruction = MEDIA_ANALYSIS_PROMPTS[kind][payload.promptLength ?? DEFAULT_MEDIA_ANALYSIS_PROMPT_LENGTH];
74
- const current = () => id === callId.current;
75
- const autoSelect = payload.autoSelectModel && payload.models.length > 0;
76
-
77
- const analyze = (modelId: string | undefined) =>
78
- engine
79
- .generate({
80
- modelId,
81
- prompt: instruction,
82
- attachments: payload.attachments,
83
- onText: (text) => current() && setPrompt(text),
84
- signal,
85
- })
86
- .then(
87
- ({ src }) => {
88
- if (!current()) return;
89
- setPrompt(src);
90
- latest.current.options.onResult?.(src);
91
- },
92
- (err) => {
93
- if (!current() || isAbortError(err)) return;
94
- const { message } = normalizeGenerationError(err);
95
- setError(message);
96
- latest.current.options.onError?.(message);
97
- }
98
- )
99
- .finally(() => current() && setAnalyzing(false));
100
-
101
- setAnalyzing(true);
102
- if (!autoSelect) {
103
- void analyze(payload.modelId ?? defaultModel);
104
- return;
105
- }
106
- // Auto-select: the reasoning model looks at the media and picks the
107
- // analysis model first. A routing problem falls back, never blocks.
108
- void routeModel({
109
- prompt: "",
110
- outputKind: "text",
111
- attachmentKinds: payload.attachments.map((a) => a.kind),
112
- attachments: payload.attachments,
113
- models: payload.models,
114
- engine: opts.engine ?? reasoning?.engine,
115
- model: defaultModel,
116
- routingPrompt: opts.routingPrompt,
117
- signal,
118
- }).then((routed) => {
119
- if (!current() || signal.aborted) return;
120
- const latestOpts = latest.current.options;
121
- if (routed.status === "fallback") {
122
- if (routed.reason === "no-reasoning") warnNoReasoningOnce();
123
- latestOpts.onRoutingFallback?.(routed);
124
- }
125
- latestOpts.onModelChange?.(routed.modelId);
126
- return analyze(routed.modelId);
127
- });
128
- }, [latest]);
129
-
130
- /** Stops the analysis in flight (cancelling at the provider where it can), keeping any text that already streamed in. */
131
- const cancel = useCallback(() => {
132
- callId.current += 1;
133
- controller.current?.abort();
134
- controller.current = null;
135
- setAnalyzing(false);
136
- }, []);
137
-
138
- /** Stops anything in flight and clears the prompt and error, e.g. when the person changes their media. */
139
- const reset = useCallback(() => {
140
- cancel();
141
- setError(null);
142
- setPrompt(null);
143
- }, [cancel]);
144
-
145
- return { submit, analyzing, error, prompt, cancel, reset };
16
+ const [store] = useState(() =>
17
+ createMediaAnalyzerStore(
18
+ () => latest.current.options,
19
+ () => latest.current.provided
20
+ )
21
+ );
22
+ const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
23
+ return { ...state, submit: store.submit, cancel: store.cancel, reset: store.reset };
146
24
  }
147
25
 
148
26
  export type UseMediaAnalyzerReturn = ReturnType<typeof useMediaAnalyzer>;
149
-
150
- let warnedNoReasoning = false;
151
- function warnNoReasoningOnce() {
152
- if (warnedNoReasoning) return;
153
- warnedNoReasoning = true;
154
- console.warn(
155
- "[chai-ui] Auto-select model is on, but there's no reasoning model, so analysis runs on the first listed model. Wrap your app in <ChaiProvider> to route with one."
156
- );
157
- }