@theoremjs/react 0.3.4-next.423.g9c79ac8 → 0.3.4-next.427.g3ab5770

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/README.md CHANGED
@@ -431,6 +431,8 @@ function MyChat() {
431
431
 
432
432
  `chat.phase` is `'idle'`, `'streaming'`, or `'gated'`. The actions are `handleSubmit`, `handleStop`, `handleSendNow`, and `handleToolDecision`.
433
433
 
434
+ `handlePendingMove(id, direction)` moves a pending message among those of its kind. `ChatComposerBar` draws a move only when `composerPendingMoves` from `@theoremjs/agents/interface` says the message can go that way: a message alone in its kind has none.
435
+
434
436
  To show the transcript and the composer, use `ChatTranscript` and `ChatComposerBar` from `/ui`. You can also draw your own from `chat.blocks`.
435
437
 
436
438
  The hooks and the client write no English text. They return kinds, codes, and states. You write the words.
@@ -24,6 +24,7 @@ export declare function useTheoremChat(options: UseTheoremChatOptions): {
24
24
  handleStop: () => void;
25
25
  handleMenuAction: (action: import("../../../src/interface/composer-actions").ComposerMenuAction) => void;
26
26
  handlePendingQueue: (id: string) => void;
27
+ handlePendingMove: (id: string, direction: "up" | "down") => void;
27
28
  handlePendingRestore: (id: string) => Promise<void>;
28
29
  handleSendNow: (source?: import("../../../src/interface/pending").ComposerPendingMessage) => Promise<void>;
29
30
  handleToolDecision: (_index: number, action: ToolDecisionAction) => Promise<void>;
@@ -30,6 +30,7 @@ export function useTheoremChat(options) {
30
30
  handleStop: chat.stop,
31
31
  handleMenuAction: chat.menuAction,
32
32
  handlePendingQueue: chat.queuePending,
33
+ handlePendingMove: chat.movePending,
33
34
  handlePendingRestore: chat.restorePending,
34
35
  handleSendNow: chat.sendNow,
35
36
  handleToolDecision,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theoremjs/react",
3
- "version": "0.3.4-next.423.g9c79ac8",
3
+ "version": "0.3.4-next.427.g3ab5770",
4
4
  "description": "React bindings for Theorem: headless hooks + transport, an Astryx chat UI, and a Web-standard server handler.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -60,8 +60,8 @@
60
60
  "@stylexjs/stylex": "0.19.0",
61
61
  "react": "^19.0.0",
62
62
  "react-dom": "^19.0.0",
63
- "@theoremjs/agents": "^0.3.4-next.423.g9c79ac8",
64
- "@theoremjs/web": "^0.3.4-next.423.g9c79ac8"
63
+ "@theoremjs/agents": "^0.3.4-next.427.g3ab5770",
64
+ "@theoremjs/web": "^0.3.4-next.427.g3ab5770"
65
65
  },
66
66
  "peerDependenciesMeta": {
67
67
  "@astryxdesign/core": {
@@ -14,7 +14,7 @@ import { Tooltip as HoverTip } from '@astryxdesign/core/Tooltip';
14
14
  import { VStack } from '@astryxdesign/core/VStack';
15
15
  import { IconAdjustmentsHorizontal, IconArchive, IconArrowBigRightLines, IconArrowDown, IconArrowUp, IconBrain, IconCornerDownLeft, IconInputAi, IconListNumbers, IconMicrophone, IconPaperclip, IconPencil, IconPlayerRecordFilled, IconSend, IconStack2, IconSteeringWheel, IconX, } from '@tabler/icons-react';
16
16
  import { attachmentIssueText, labelText } from '@theoremjs/agents';
17
- import { composerPendingPreview, effortSelectEnabled, interfaceEffortOptions, interfaceModelOptions, modelSelectEnabled, } from '@theoremjs/agents/interface';
17
+ import { composerPendingMoves, composerPendingPreview, effortSelectEnabled, interfaceEffortOptions, interfaceModelOptions, modelSelectEnabled, } from '@theoremjs/agents/interface';
18
18
  import { composerActionState, composerDrawerSummary, isStashShortcut, resolveComposerHint, STASH_SHORTCUT, } from '@theoremjs/web/client';
19
19
  import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
20
20
  import { stageComposerFiles } from "../client/composer-attachments.js";
@@ -29,15 +29,23 @@ const NO_FOCUS_RING = { '--focus-outline-width': '0px' };
29
29
  export const EFFORT_ICON = IconBrain;
30
30
  const isImage = (file) => file.type.startsWith('image/');
31
31
  const anyFile = () => true;
32
- /** Object URLs for the files `keep` (a module-level predicate) accepts, revoked when the files change. */
32
+ /**
33
+ * Object URLs for the files `keep` (a module-level predicate) accepts, revoked when the files change.
34
+ * why: Each URL is made in the effect that revokes it, so a remount (strict mode, a moved subtree)
35
+ * makes fresh ones instead of keeping revoked ones; until the effect runs the files have none.
36
+ */
33
37
  function useObjectUrls(files, keep) {
34
- const urls = useMemo(() => files.map((file) => (keep(file) ? URL.createObjectURL(file) : undefined)), [files, keep]);
35
- useEffect(() => () => {
36
- for (const url of urls)
37
- if (url)
38
- URL.revokeObjectURL(url);
39
- }, [urls]);
40
- return urls;
38
+ const [made, setMade] = useState();
39
+ useEffect(() => {
40
+ const urls = files.map((file) => (keep(file) ? URL.createObjectURL(file) : undefined));
41
+ setMade({ files, urls });
42
+ return () => {
43
+ for (const url of urls)
44
+ if (url)
45
+ URL.revokeObjectURL(url);
46
+ };
47
+ }, [files, keep]);
48
+ return made?.files === files ? made.urls : [];
41
49
  }
42
50
  /** Each way to send, by the same icon in the send menu and on a waiting message. */
43
51
  const SEND_ICON = {
@@ -57,7 +65,7 @@ function PendingRow(props) {
57
65
  const moveDown = t('@theorem.composer.pending.move_down');
58
66
  const remove = t('@theorem.composer.pending.remove');
59
67
  const Kind = SEND_ICON[message.kind];
60
- return (_jsxs(HStack, { gap: 1, align: "center", width: "100%", children: [_jsx(HoverTip, { content: t(`@theorem.composer.pending.${message.kind}.description`), children: _jsx(Token, { label: t(`@theorem.composer.pending.${message.kind}`), isLabelHidden: true, color: message.kind === 'steer' ? 'blue' : 'default', icon: _jsx(Kind, { size: 14 }) }) }), _jsx(StackItem, { size: "fill", style: { width: 0 }, children: _jsx(Text, { size: "sm", maxLines: 1, hasTruncateTooltip: true, children: composerPendingPreview(message) }) }), _jsxs(HStack, { gap: 0.5, children: [_jsx(IconButton, { label: edit, tooltip: edit, size: "sm", variant: "ghost", icon: icon(IconPencil), onClick: () => props.onRestore(message.id) }), message.kind === 'stash' ? (_jsx(IconButton, { label: queue, tooltip: queue, size: "sm", variant: "ghost", icon: icon(IconCornerDownLeft), onClick: () => props.onQueue(message.id) })) : null, _jsx(IconButton, { label: sendNow, tooltip: sendNow, size: "sm", variant: "ghost", icon: icon(IconSend), onClick: () => props.onSendNow(message.id) }), _jsx(IconButton, { label: moveUp, tooltip: moveUp, size: "sm", variant: "ghost", icon: icon(IconArrowUp), onClick: () => props.onMove(message.id, 'up') }), _jsx(IconButton, { label: moveDown, tooltip: moveDown, size: "sm", variant: "ghost", icon: icon(IconArrowDown), onClick: () => props.onMove(message.id, 'down') }), _jsx(IconButton, { label: remove, tooltip: remove, size: "sm", variant: "ghost", icon: icon(IconX), onClick: () => props.onRemove(message.id) })] })] }));
68
+ return (_jsxs(HStack, { gap: 1, align: "center", width: "100%", children: [_jsx(HoverTip, { content: t(`@theorem.composer.pending.${message.kind}.description`), children: _jsx(Token, { label: t(`@theorem.composer.pending.${message.kind}`), isLabelHidden: true, color: message.kind === 'steer' ? 'blue' : 'default', icon: _jsx(Kind, { size: 14 }) }) }), _jsx(StackItem, { size: "fill", style: { width: 0 }, children: _jsx(Text, { size: "sm", maxLines: 1, hasTruncateTooltip: true, children: composerPendingPreview(message) }) }), _jsxs(HStack, { gap: 0.5, children: [_jsx(IconButton, { label: edit, tooltip: edit, size: "sm", variant: "ghost", icon: icon(IconPencil), onClick: () => props.onRestore(message.id) }), message.kind === 'stash' ? (_jsx(IconButton, { label: queue, tooltip: queue, size: "sm", variant: "ghost", icon: icon(IconCornerDownLeft), onClick: () => props.onQueue(message.id) })) : null, _jsx(IconButton, { label: sendNow, tooltip: sendNow, size: "sm", variant: "ghost", icon: icon(IconSend), onClick: () => props.onSendNow(message.id) }), props.moves.up ? (_jsx(IconButton, { label: moveUp, tooltip: moveUp, size: "sm", variant: "ghost", icon: icon(IconArrowUp), onClick: () => props.onMove(message.id, 'up') })) : null, props.moves.down ? (_jsx(IconButton, { label: moveDown, tooltip: moveDown, size: "sm", variant: "ghost", icon: icon(IconArrowDown), onClick: () => props.onMove(message.id, 'down') })) : null, _jsx(IconButton, { label: remove, tooltip: remove, size: "sm", variant: "ghost", icon: icon(IconX), onClick: () => props.onRemove(message.id) })] })] }));
61
69
  }
62
70
  /** The model and effort choices this profile offers; efforts only for a chosen model. */
63
71
  function generationOptions(iface, selectedModel) {
@@ -111,7 +119,11 @@ function composerStatus(args) {
111
119
  /** Pending messages, then two uniform rows: 64px tiles (images, voice notes), then file tokens. */
112
120
  function PendingDrawer(props) {
113
121
  const t = useLabels();
114
- return (_jsx(ChatComposerDrawer, { count: props.summary.count, label: composerDrawerLabel(t, props.summary), children: _jsxs(VStack, { gap: 2, width: "100%", children: [props.messages.map((message) => (_jsx(PendingRow, { message: message, ...props.pendingActions }, message.id))), props.imageFiles.length + props.voiceFiles.length > 0 ? (_jsxs(HStack, { gap: 2, wrap: "wrap", vAlign: "center", children: [props.imageFiles.map(({ file, index, preview }) => (_jsx(Thumbnail, { src: preview, alt: file.name, label: file.name, onRemove: () => props.onAttachmentRemove(index) }, `${file.name}:${String(index)}`))), props.voiceFiles.map((file, index) => (_jsx(VoiceNote, { src: props.voiceUrls[index] ?? '', mimeType: file.type || undefined, onRemove: props.onVoiceRemove }, `voice:${file.name}`)))] })) : null, props.otherFiles.length > 0 ? (_jsx(HStack, { gap: 2, wrap: "wrap", children: props.otherFiles.map(({ file, index }) => (_jsx(Token, { label: file.name, onRemove: () => props.onAttachmentRemove(index) }, `${file.name}:${String(index)}`))) })) : null] }) }));
122
+ return (_jsx(ChatComposerDrawer, { count: props.summary.count, label: composerDrawerLabel(t, props.summary), children: _jsxs(VStack, { gap: 2, width: "100%", children: [props.messages.map((message) => (_jsx(PendingRow, { message: message, moves: composerPendingMoves(props.messages, message.id), ...props.pendingActions }, message.id))), props.imageFiles.length + props.voiceFiles.length > 0 ? (_jsxs(HStack, { gap: 2, wrap: "wrap", vAlign: "center", children: [props.imageFiles.map(({ file, index, preview }) => (_jsx(Thumbnail, { src: preview, alt: file.name, label: file.name, onRemove: () => props.onAttachmentRemove(index) }, `${file.name}:${String(index)}`))), props.voiceFiles.map((file, index) => {
123
+ const src = props.voiceUrls[index];
124
+ // why: An empty src makes the browser fetch the page again; a note draws once it has its URL.
125
+ return src === undefined ? null : (_jsx(VoiceNote, { src: src, mimeType: file.type || undefined, onRemove: props.onVoiceRemove }, `voice:${file.name}`));
126
+ })] })) : null, props.otherFiles.length > 0 ? (_jsx(HStack, { gap: 2, wrap: "wrap", children: props.otherFiles.map(({ file, index }) => (_jsx(Token, { label: file.name, onRemove: () => props.onAttachmentRemove(index) }, `${file.name}:${String(index)}`))) })) : null] }) }));
115
127
  }
116
128
  /** A hidden file input behind the paperclip button. */
117
129
  function AttachFilesButton({ accept, onFiles, }) {
@@ -168,8 +180,8 @@ function useStagedFiles(pendingFiles, pendingVoice) {
168
180
  preview: previews[index],
169
181
  }));
170
182
  return {
171
- imageFiles: staged.filter((entry) => entry.preview !== undefined),
172
- otherFiles: staged.filter((entry) => entry.preview === undefined),
183
+ imageFiles: staged.filter((entry) => isImage(entry.file)),
184
+ otherFiles: staged.filter((entry) => !isImage(entry.file)),
173
185
  voiceUrls,
174
186
  };
175
187
  }
package/ui/TheoremChat.js CHANGED
@@ -5,7 +5,7 @@ import { ChatLayout } from '@astryxdesign/core/Chat';
5
5
  import { Layout, LayoutContent } from '@astryxdesign/core/Layout';
6
6
  import { Text } from '@astryxdesign/core/Text';
7
7
  import { VStack } from '@astryxdesign/core/VStack';
8
- import { moveComposerPendingWithinKind, removeComposerPendingMessage, } from '@theoremjs/agents/interface';
8
+ import { removeComposerPendingMessage, } from '@theoremjs/agents/interface';
9
9
  import { createHttpTransport, } from '@theoremjs/web/client';
10
10
  import { useImperativeHandle, useMemo, useRef, } from 'react';
11
11
  import { parseAspectRatio } from "../client/image-output.js";
@@ -41,9 +41,7 @@ function ChatComposerForChat({ chat, iface, placeholder, inputRef, }) {
41
41
  chat.setIssues([]);
42
42
  }, onVoiceClear: () => chat.setPendingVoice([]), onSubmit: () => {
43
43
  void chat.handleSubmit();
44
- }, onStop: chat.handleStop, onMenuAction: chat.handleMenuAction, onGenerationChange: chat.handleGenerationChange, slotChoices: chat.slotChoices, onPendingMove: (id, direction) => {
45
- chat.setPendingMessages((prev) => moveComposerPendingWithinKind(prev, id, direction));
46
- }, onPendingRemove: (id) => {
44
+ }, onStop: chat.handleStop, onMenuAction: chat.handleMenuAction, onGenerationChange: chat.handleGenerationChange, slotChoices: chat.slotChoices, onPendingMove: chat.handlePendingMove, onPendingRemove: (id) => {
47
45
  chat.setPendingMessages((prev) => removeComposerPendingMessage(prev, id));
48
46
  }, onPendingQueue: chat.handlePendingQueue, onPendingRestore: (id) => {
49
47
  void chat.handlePendingRestore(id);