@theoremjs/react 0.3.0 → 0.3.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/README.md +728 -202
- package/client/composer-hints.js +1 -1
- package/client/decision-images.d.ts +76 -0
- package/client/decision-images.js +139 -0
- package/client/decision-transport.d.ts +26 -4
- package/client/decision-transport.js +79 -6
- package/client/embedding-input.d.ts +84 -0
- package/client/embedding-input.js +194 -0
- package/client/embedding-space.d.ts +82 -0
- package/client/embedding-space.js +389 -0
- package/client/embedding-transport.d.ts +40 -0
- package/client/embedding-transport.js +71 -0
- package/client/failure.d.ts +7 -10
- package/client/failure.js +9 -9
- package/client/host-call.d.ts +1 -1
- package/client/host-call.js +7 -2
- package/client/host-transport.d.ts +2 -0
- package/client/host-transport.js +3 -2
- package/client/index.d.ts +19 -11
- package/client/index.js +14 -10
- package/client/ink-waveform.d.ts +1 -1
- package/client/ink-waveform.js +1 -2
- package/client/live/apply-live-turn-tool-event.js +1 -1
- package/client/live/live-captions.d.ts +30 -0
- package/client/live/live-captions.js +105 -1
- package/client/live/live-mic-forward.d.ts +1 -0
- package/client/live/live-mic-forward.js +11 -2
- package/client/live/live-page-tool.d.ts +34 -0
- package/client/live/live-page-tool.js +11 -0
- package/client/live/live-state.d.ts +4 -2
- package/client/live/live-state.js +2 -0
- package/client/live/live-video.js +1 -1
- package/client/live/run-live-tool-call.d.ts +7 -2
- package/client/live/run-live-tool-call.js +48 -10
- package/client/live/use-live-caption-log.d.ts +17 -0
- package/client/live/use-live-caption-log.js +51 -0
- package/client/live-client.d.ts +52 -4
- package/client/live-client.js +250 -59
- package/client/live-messages.d.ts +51 -13
- package/client/live-messages.js +42 -9
- package/client/page-dispatch.d.ts +15 -0
- package/client/page-dispatch.js +63 -0
- package/client/run-commit.d.ts +16 -43
- package/client/run-commit.js +26 -101
- package/client/run-session.d.ts +40 -41
- package/client/run-session.js +104 -112
- package/client/schema-fields.js +27 -6
- package/client/shaped-data.js +37 -8
- package/client/source-citations.js +1 -1
- package/client/space-geometry.d.ts +39 -0
- package/client/space-geometry.js +183 -0
- package/client/state-fields.js +1 -1
- package/client/tool-result.js +30 -12
- package/client/tool-resume.d.ts +18 -1
- package/client/tool-resume.js +4 -1
- package/client/trace-feed.d.ts +1 -1
- package/client/trace-story.d.ts +2 -2
- package/client/trace-story.js +43 -19
- package/client/trace-view.d.ts +2 -2
- package/client/trace-view.js +56 -16
- package/client/transcript-groups.d.ts +28 -2
- package/client/transcript-groups.js +63 -13
- package/client/transcript-schema.d.ts +10 -0
- package/client/transcript-schema.js +120 -0
- package/client/transport.d.ts +114 -26
- package/client/transport.js +106 -32
- package/client/turn-client.d.ts +17 -8
- package/client/turn-client.js +26 -31
- package/client/voice-recorder.d.ts +1 -1
- package/client/voice-recorder.js +2 -5
- package/client/wire-line.js +5 -5
- package/client/worklets/mic-capture.js +9 -9
- package/components/InkWaveform.d.ts +12 -1
- package/components/InkWaveform.js +65 -42
- package/components/live/use-live-runner-controls.d.ts +3 -4
- package/components/live/use-live-runner-controls.js +6 -10
- package/components/live/use-live-runner-model.d.ts +10 -6
- package/components/live/use-live-runner-model.js +29 -14
- package/components/live/use-live-runner-ui.d.ts +10 -8
- package/components/live/use-live-runner-ui.js +19 -11
- package/components/live/use-live-session-client.d.ts +4 -4
- package/components/live/use-live-session-client.js +11 -8
- package/components/use-composer-voice.d.ts +4 -4
- package/components/use-composer-voice.js +8 -8
- package/components/use-editor-selection.js +4 -1
- package/components/use-voice-playback.d.ts +2 -1
- package/components/use-voice-playback.js +4 -4
- package/hooks/use-described.d.ts +5 -1
- package/hooks/use-described.js +11 -9
- package/hooks/use-embedding-index.d.ts +27 -0
- package/hooks/use-embedding-index.js +43 -0
- package/hooks/use-one-call.d.ts +31 -0
- package/hooks/use-one-call.js +76 -0
- package/hooks/use-page-tool-warnings.d.ts +6 -0
- package/hooks/use-page-tool-warnings.js +25 -0
- package/hooks/use-second-ticker.d.ts +2 -0
- package/hooks/use-second-ticker.js +13 -0
- package/hooks/use-theorem-chat-actions.d.ts +25 -21
- package/hooks/use-theorem-chat-actions.js +67 -45
- package/hooks/use-theorem-chat-state.d.ts +6 -6
- package/hooks/use-theorem-chat-state.js +13 -9
- package/hooks/use-theorem-chat.d.ts +48 -14
- package/hooks/use-theorem-chat.js +329 -141
- package/hooks/use-theorem-decision.d.ts +5 -10
- package/hooks/use-theorem-decision.js +4 -67
- package/hooks/use-theorem-embedding.d.ts +18 -0
- package/hooks/use-theorem-embedding.js +12 -0
- package/hooks/use-theorem-host.d.ts +5 -4
- package/hooks/use-theorem-host.js +26 -8
- package/hooks/use-theorem-interface.d.ts +6 -3
- package/hooks/use-theorem-interface.js +12 -22
- package/index.d.ts +6 -3
- package/index.js +5 -2
- package/live.d.ts +1 -0
- package/live.js +1 -1
- package/package.json +6 -5
- package/server/checkpoint-ownership.d.ts +6 -0
- package/server/checkpoint-ownership.js +42 -0
- package/server/conversation-store.d.ts +92 -0
- package/server/conversation-store.js +131 -0
- package/server/conversation.d.ts +193 -0
- package/server/conversation.js +552 -0
- package/server/decision-handler.d.ts +12 -4
- package/server/decision-handler.js +29 -38
- package/server/embedding-handler.d.ts +33 -0
- package/server/embedding-handler.js +45 -0
- package/server/handler.d.ts +58 -20
- package/server/handler.js +441 -238
- package/server/mod.d.ts +15 -6
- package/server/mod.js +13 -5
- package/server/page-dispatch.d.ts +22 -0
- package/server/page-dispatch.js +127 -0
- package/server/request-body.d.ts +2 -0
- package/server/request-body.js +42 -0
- package/server/request-check.d.ts +6 -1
- package/server/request-check.js +27 -12
- package/server/session-authority.d.ts +8 -0
- package/server/session-authority.js +78 -0
- package/server/session-boundary.d.ts +26 -0
- package/server/session-boundary.js +64 -0
- package/server/session-identity.d.ts +16 -0
- package/server/session-identity.js +41 -0
- package/server/session-limits.d.ts +12 -0
- package/server/session-limits.js +59 -0
- package/server/session-store.d.ts +69 -20
- package/server/session-store.js +220 -5
- package/server/single-action.d.ts +21 -0
- package/server/single-action.js +30 -0
- package/server/steer-inbox.js +1 -1
- package/server/tool-operations.d.ts +6 -0
- package/server/tool-operations.js +132 -0
- package/server/turn-input.d.ts +10 -0
- package/server/turn-input.js +29 -0
- package/styles/ink-controls.css +78 -78
- package/ui/CardWall.d.ts +10 -0
- package/ui/CardWall.js +61 -0
- package/ui/ChatComposerBar.d.ts +28 -5
- package/ui/ChatComposerBar.js +85 -30
- package/ui/ChatTranscript.d.ts +14 -1
- package/ui/ChatTranscript.js +110 -57
- package/ui/ConsoleFrame.d.ts +61 -4
- package/ui/ConsoleFrame.js +109 -9
- package/ui/DecisionAnswers.d.ts +40 -2
- package/ui/DecisionAnswers.js +166 -35
- package/ui/DecisionImages.d.ts +16 -0
- package/ui/DecisionImages.js +46 -0
- package/ui/EmbeddingDocuments.d.ts +78 -0
- package/ui/EmbeddingDocuments.js +278 -0
- package/ui/EmbeddingQuery.d.ts +27 -0
- package/ui/EmbeddingQuery.js +95 -0
- package/ui/EmbeddingResults.d.ts +41 -0
- package/ui/EmbeddingResults.js +165 -0
- package/ui/EmbeddingSpace.d.ts +26 -0
- package/ui/EmbeddingSpace.js +106 -0
- package/ui/LiveCaptionsPanel.d.ts +22 -0
- package/ui/LiveCaptionsPanel.js +27 -0
- package/ui/LiveRunner.d.ts +6 -4
- package/ui/LiveRunner.js +42 -55
- package/ui/PaneState.d.ts +33 -0
- package/ui/PaneState.js +50 -0
- package/ui/SchemaFields.d.ts +1 -1
- package/ui/SchemaFields.js +54 -26
- package/ui/ShapedData.js +73 -28
- package/ui/SidePanel.d.ts +74 -5
- package/ui/SidePanel.js +82 -9
- package/ui/StateFields.d.ts +10 -2
- package/ui/StateFields.js +56 -13
- package/ui/TheoremChat.d.ts +65 -14
- package/ui/TheoremChat.js +66 -78
- package/ui/TheoremDecision.d.ts +12 -20
- package/ui/TheoremDecision.js +78 -35
- package/ui/TheoremEmbedding.d.ts +27 -0
- package/ui/TheoremEmbedding.js +122 -0
- package/ui/TheoremHost.d.ts +22 -5
- package/ui/TheoremHost.js +127 -37
- package/ui/ToolGateCard.d.ts +5 -1
- package/ui/ToolGateCard.js +30 -23
- package/ui/ToolResult.js +12 -10
- package/ui/ToolResultCharts.js +29 -12
- package/ui/TraceCharts.d.ts +2 -2
- package/ui/TraceCharts.js +48 -17
- package/ui/TraceGuardrails.d.ts +3 -2
- package/ui/TraceGuardrails.js +24 -6
- package/ui/TraceInspector.d.ts +11 -1
- package/ui/TraceInspector.js +45 -28
- package/ui/TraceInspectorPanel.d.ts +31 -9
- package/ui/TraceInspectorPanel.js +77 -39
- package/ui/TraceOverview.d.ts +7 -3
- package/ui/TraceOverview.js +44 -14
- package/ui/TraceSpanDetail.js +24 -22
- package/ui/TraceStory.js +9 -8
- package/ui/TraceValues.js +19 -9
- package/ui/TraceWaterfall.js +12 -9
- package/ui/VoiceNote.js +4 -4
- package/ui/approval-heading.d.ts +4 -0
- package/ui/approval-heading.js +9 -0
- package/ui/arrive.d.ts +18 -0
- package/ui/arrive.js +46 -0
- package/ui/built/theme.css +86 -68
- package/ui/built/theme.css.d.ts +9 -0
- package/ui/built/theorem.d.ts +3 -3
- package/ui/built/theorem.js +34 -5
- package/ui/built/theorem.variants.d.ts +8 -2
- package/ui/code-language.d.ts +12 -0
- package/ui/code-language.js +50 -0
- package/ui/disclosure-motion.js +53 -9
- package/ui/embedding-space-gl.d.ts +37 -0
- package/ui/embedding-space-gl.js +663 -0
- package/ui/embedding-space-view.d.ts +8 -0
- package/ui/embedding-space-view.js +8 -0
- package/ui/glide.d.ts +7 -0
- package/ui/glide.js +44 -0
- package/ui/icons.js +2 -1
- package/ui/index.d.ts +20 -9
- package/ui/index.js +20 -10
- package/ui/labels-provider.js +5 -2
- package/ui/labels.d.ts +374 -85
- package/ui/labels.js +1028 -188
- package/ui/markdown-plugins.d.ts +9 -0
- package/ui/markdown-plugins.js +13 -0
- package/ui/mcp-icon.d.ts +6 -0
- package/ui/mcp-icon.js +8 -0
- package/ui/row-keys.d.ts +19 -0
- package/ui/row-keys.js +33 -0
- package/ui/theme.d.ts +1 -1
- package/ui/theme.js +2 -2
- package/ui/theorem-theme.js +32 -10
- package/ui/trace-palette.d.ts +2 -16
- package/ui/trace-palette.js +2 -16
- package/ui/trace-patterns.d.ts +11 -3
- package/ui/trace-patterns.js +16 -3
- package/ui/transcript-copy-text.js +1 -0
- package/ui/wall-columns.d.ts +8 -0
- package/ui/wall-columns.js +19 -0
- package/client/walk-away.d.ts +0 -34
- package/client/walk-away.js +0 -48
- package/server/walk-away.d.ts +0 -22
- package/server/walk-away.js +0 -41
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { FileInput } from '@astryxdesign/core/FileInput';
|
|
3
|
+
import { Grid, GridSpan } from '@astryxdesign/core/Grid';
|
|
4
|
+
import { HStack } from '@astryxdesign/core/HStack';
|
|
5
|
+
import { IconButton } from '@astryxdesign/core/IconButton';
|
|
6
|
+
import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core/SegmentedControl';
|
|
7
|
+
import { StackItem } from '@astryxdesign/core/Stack';
|
|
8
|
+
import { Text } from '@astryxdesign/core/Text';
|
|
9
|
+
import { TextArea } from '@astryxdesign/core/TextArea';
|
|
10
|
+
import { TextInput } from '@astryxdesign/core/TextInput';
|
|
11
|
+
import { Thumbnail } from '@astryxdesign/core/Thumbnail';
|
|
12
|
+
import { Tooltip } from '@astryxdesign/core/Tooltip';
|
|
13
|
+
import { VStack } from '@astryxdesign/core/VStack';
|
|
14
|
+
import { IconBaselineDensityMedium, IconCsv, IconFileText, IconPilcrow, IconScissors, IconX, } from '@tabler/icons-react';
|
|
15
|
+
import { attachmentIssueText, base64Bytes } from '@theoremjs/agents';
|
|
16
|
+
import { useMemo, useState } from 'react';
|
|
17
|
+
import { checkInputs, embeddingBatches, readEmbeddingInputs, readsAsText, splitText, } from "../client/embedding-input.js";
|
|
18
|
+
import { encodeFiles } from "../client/encode-files.js";
|
|
19
|
+
import { JsonRequest, RequestCard, RunBar, useBytes } from "./ConsoleFrame.js";
|
|
20
|
+
import { useLabels } from "./labels-provider.js";
|
|
21
|
+
import { textRows } from "./StateFields.js";
|
|
22
|
+
/** The ways to split, coarsest first, each with its icon. */
|
|
23
|
+
const SPLITS = [
|
|
24
|
+
{ value: 'whole', icon: _jsx(IconFileText, { size: 16 }) },
|
|
25
|
+
{ value: 'paragraphs', icon: _jsx(IconPilcrow, { size: 16 }) },
|
|
26
|
+
{ value: 'lines', icon: _jsx(IconBaselineDensityMedium, { size: 16 }) },
|
|
27
|
+
{ value: 'commas', icon: _jsx(IconCsv, { size: 16 }) },
|
|
28
|
+
{ value: 'custom', icon: _jsx(IconScissors, { size: 16 }) },
|
|
29
|
+
];
|
|
30
|
+
const REASON_LABELS = {
|
|
31
|
+
invalid_json: '@theorem.embedding.empty',
|
|
32
|
+
no_inputs: '@theorem.embedding.empty',
|
|
33
|
+
empty_input: '@theorem.embedding.empty',
|
|
34
|
+
text_off: '@theorem.embedding.text_off',
|
|
35
|
+
};
|
|
36
|
+
/** What the check says is wrong, about the input it names. */
|
|
37
|
+
export function inputIssue(t, check, name) {
|
|
38
|
+
if (check.ok)
|
|
39
|
+
return null;
|
|
40
|
+
if (check.reason === 'attachments')
|
|
41
|
+
return `${name}: ${check.issues.map((issue) => attachmentIssueText(issue)).join(' ')}`;
|
|
42
|
+
if (check.reason === 'too_many_inputs')
|
|
43
|
+
return t('@theorem.embedding.too_many', { max: check.max });
|
|
44
|
+
return t(REASON_LABELS[check.reason], { name });
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The documents: pasted text split as chosen, then files and JSON, in that
|
|
48
|
+
* order. Text files are split as the text is; any other file is one document.
|
|
49
|
+
*/
|
|
50
|
+
export function useEmbeddingDocuments(defaultText, rules, defaultDocuments = []) {
|
|
51
|
+
const t = useLabels();
|
|
52
|
+
const [text, setText] = useState(defaultText);
|
|
53
|
+
const [choice, setChoice] = useState('paragraphs');
|
|
54
|
+
const [separator, setSeparator] = useState('');
|
|
55
|
+
const [added, setAdded] = useState(() => [...defaultDocuments]);
|
|
56
|
+
const [view, setView] = useState('fields');
|
|
57
|
+
const [json, setJson] = useState(null);
|
|
58
|
+
const split = choice === 'custom' ? { separator } : choice;
|
|
59
|
+
const documents = useMemo(() => [
|
|
60
|
+
...splitText(text, choice === 'custom' ? { separator } : choice).map((input) => ({ input })),
|
|
61
|
+
...added,
|
|
62
|
+
], [text, choice, separator, added]);
|
|
63
|
+
const inputs = useMemo(() => documents.map((document) => document.input), [documents]);
|
|
64
|
+
// why: The inputs hold every attachment as base64; writing them out on each render blocks the page.
|
|
65
|
+
const shownJson = useMemo(() => (view === 'json' ? (json ?? JSON.stringify(inputs, null, 2)) : ''), [view, json, inputs]);
|
|
66
|
+
const jsonInvalid = json !== null && readEmbeddingInputs(json) === undefined;
|
|
67
|
+
const issue = useMemo(() => {
|
|
68
|
+
for (const [at, input] of inputs.entries()) {
|
|
69
|
+
const found = inputIssue(t, checkInputs([input], rules), t('@theorem.embedding.document', { number: at + 1 }));
|
|
70
|
+
if (found)
|
|
71
|
+
return found;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}, [inputs, rules, t]);
|
|
75
|
+
const requests = useMemo(() => embeddingBatches(inputs, rules).length, [inputs, rules]);
|
|
76
|
+
const addFiles = async (files) => {
|
|
77
|
+
const words = files.filter(readsAsText);
|
|
78
|
+
const media = files.filter((file) => !readsAsText(file));
|
|
79
|
+
const pieces = await Promise.all(words.map(async (file) => splitText(await file.text(), split).map((input) => ({ input, from: file.name }))));
|
|
80
|
+
const encoded = await encodeFiles(media);
|
|
81
|
+
setAdded((previous) => [
|
|
82
|
+
...previous,
|
|
83
|
+
...pieces.flat(),
|
|
84
|
+
...encoded.map(({ name, mimeType, data }) => ({
|
|
85
|
+
input: { parts: [{ type: 'media', mimeType, data }] },
|
|
86
|
+
from: name,
|
|
87
|
+
})),
|
|
88
|
+
]);
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
text,
|
|
92
|
+
setText,
|
|
93
|
+
choice,
|
|
94
|
+
setChoice,
|
|
95
|
+
separator,
|
|
96
|
+
setSeparator,
|
|
97
|
+
documents,
|
|
98
|
+
inputs,
|
|
99
|
+
issue,
|
|
100
|
+
requests,
|
|
101
|
+
view,
|
|
102
|
+
setView: (next) => {
|
|
103
|
+
setJson(null);
|
|
104
|
+
setView(next);
|
|
105
|
+
},
|
|
106
|
+
json: shownJson,
|
|
107
|
+
jsonInvalid,
|
|
108
|
+
setJson: (next) => {
|
|
109
|
+
setJson(next);
|
|
110
|
+
const parsed = readEmbeddingInputs(next);
|
|
111
|
+
if (parsed === undefined)
|
|
112
|
+
return;
|
|
113
|
+
setText('');
|
|
114
|
+
setAdded(parsed.map((input) => ({ input })));
|
|
115
|
+
},
|
|
116
|
+
addFiles,
|
|
117
|
+
remove: (at) => {
|
|
118
|
+
const shift = documents.length - added.length;
|
|
119
|
+
setAdded((previous) => previous.filter((_, other) => !at.includes(other + shift)));
|
|
120
|
+
},
|
|
121
|
+
/** Where the pasted text's documents end and the removable ones begin. */
|
|
122
|
+
fromText: documents.length - added.length,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function mediaOf(input) {
|
|
126
|
+
return typeof input === 'string' ? undefined : input.parts.find((part) => part.type !== 'text');
|
|
127
|
+
}
|
|
128
|
+
function textOf(input) {
|
|
129
|
+
if (typeof input === 'string')
|
|
130
|
+
return input;
|
|
131
|
+
return input.parts.flatMap((part) => (part.type === 'text' ? [part.text] : [])).join(' ');
|
|
132
|
+
}
|
|
133
|
+
/** What a document reads as in a list: its text, or its file's name, type, and size. */
|
|
134
|
+
export function documentLabel(t, bytes, document, number) {
|
|
135
|
+
const media = mediaOf(document.input);
|
|
136
|
+
const words = textOf(document.input);
|
|
137
|
+
if (!media)
|
|
138
|
+
return { label: words, detail: null };
|
|
139
|
+
const size = media.type === 'media' ? bytes(base64Bytes(media.data)) : t('@theorem.embedding.held_file');
|
|
140
|
+
return {
|
|
141
|
+
label: words || (document.from ?? t('@theorem.embedding.document', { number })),
|
|
142
|
+
detail: `${media.mimeType} · ${size}`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** Where the text is cut, and the separator when it is one of the user's own. */
|
|
146
|
+
/** A file field that reads what is dropped or chosen, and says which files it could not read. */
|
|
147
|
+
export function EmbeddingFileDrop({ label, placeholder, accept, isMultiple = false, onFiles, }) {
|
|
148
|
+
const t = useLabels();
|
|
149
|
+
const [unreadable, setUnreadable] = useState(null);
|
|
150
|
+
return (_jsx(FileInput, { label: label, isLabelHidden: true, mode: "dropzone", isMultiple: isMultiple, accept: accept.join(','), value: null, placeholder: placeholder, status: unreadable ? { type: 'error', message: unreadable } : undefined, onChange: (files) => {
|
|
151
|
+
const list = files === null ? [] : Array.isArray(files) ? files : [files];
|
|
152
|
+
if (list.length === 0)
|
|
153
|
+
return;
|
|
154
|
+
setUnreadable(null);
|
|
155
|
+
onFiles(list).catch(() => {
|
|
156
|
+
setUnreadable(t('@theorem.embedding.unreadable', { name: list.map((file) => file.name).join(', ') }));
|
|
157
|
+
});
|
|
158
|
+
} }));
|
|
159
|
+
}
|
|
160
|
+
function SplitFields({ form }) {
|
|
161
|
+
const t = useLabels();
|
|
162
|
+
return (_jsxs(HStack, { gap: 2, vAlign: "center", wrap: "wrap", children: [_jsx(Text, { type: "supporting", color: "secondary", children: t('@theorem.embedding.split') }), _jsx(SegmentedControl, { label: t('@theorem.embedding.split'), size: "sm", value: form.choice, onChange: (next) => {
|
|
163
|
+
form.setChoice(next);
|
|
164
|
+
}, children: SPLITS.map(({ value, icon }) => {
|
|
165
|
+
const label = t(`@theorem.embedding.split_${value}`);
|
|
166
|
+
return (_jsx(Tooltip, { content: label, children: _jsx(SegmentedControlItem, { value: value, label: label, icon: icon, isLabelHidden: true }) }, value));
|
|
167
|
+
}) }), form.choice === 'custom' ? (_jsx(StackItem, { size: "fill", children: _jsx(TextInput, { label: t('@theorem.embedding.separator'), isLabelHidden: true, placeholder: t('@theorem.embedding.separator'), size: "sm", value: form.separator, onChange: form.setSeparator }) })) : null] }));
|
|
168
|
+
}
|
|
169
|
+
/** How many pieces of a text a group previews; the rest are counted. */
|
|
170
|
+
const PREVIEWED = 3;
|
|
171
|
+
/** The added documents that are words, grouped by where they came from; the pasted text is already on screen. */
|
|
172
|
+
function groupDocuments(form, t) {
|
|
173
|
+
const groups = new Map();
|
|
174
|
+
for (const [at, document] of form.documents.entries()) {
|
|
175
|
+
if (mediaOf(document.input))
|
|
176
|
+
continue;
|
|
177
|
+
if (at < form.fromText)
|
|
178
|
+
continue;
|
|
179
|
+
const source = document.from
|
|
180
|
+
? t('@theorem.embedding.from_file', { name: document.from })
|
|
181
|
+
: t('@theorem.embedding.from_json');
|
|
182
|
+
const group = groups.get(source) ?? { source, at: [] };
|
|
183
|
+
group.at.push(at);
|
|
184
|
+
groups.set(source, group);
|
|
185
|
+
}
|
|
186
|
+
return [...groups.values()];
|
|
187
|
+
}
|
|
188
|
+
/** The first few pieces of one source, with the source and how many it made. */
|
|
189
|
+
function TextGroup({ group, form }) {
|
|
190
|
+
const t = useLabels();
|
|
191
|
+
return (_jsxs(VStack, { gap: 1, children: [_jsxs(HStack, { gap: 1, vAlign: "center", children: [_jsx(StackItem, { size: "fill", children: _jsx(Text, { type: "supporting", color: "secondary", children: t('@theorem.embedding.group', { source: group.source, count: group.at.length }) }) }), _jsx(IconButton, { label: t('@theorem.embedding.remove', { name: group.source }), size: "sm", variant: "ghost", icon: _jsx(IconX, { size: 14 }), onClick: () => {
|
|
192
|
+
form.remove(group.at);
|
|
193
|
+
} })] }), group.at.slice(0, PREVIEWED).map((at) => (_jsx(Text, { maxLines: 2, children: textOf(form.documents[at]?.input ?? '') }, String(at)))), group.at.length > PREVIEWED ? (_jsx(Text, { type: "supporting", color: "secondary", children: t('@theorem.embedding.more', { count: group.at.length - PREVIEWED }) })) : null] }));
|
|
194
|
+
}
|
|
195
|
+
/** One file: its picture above its type, with its name and size on hover and Remove. */
|
|
196
|
+
export function FileTile({ part, name, label, onRemove, }) {
|
|
197
|
+
const image = part.type === 'media' && part.mimeType.startsWith('image/');
|
|
198
|
+
return (_jsxs(VStack, { gap: 1, hAlign: "center", style: { width: TILE_PX, flexShrink: 0 }, children: [_jsx(Thumbnail, { ...(image && part.type === 'media'
|
|
199
|
+
? { src: `data:${part.mimeType};base64,${part.data}` }
|
|
200
|
+
: {}), alt: "", label: label, onRemove: onRemove }), _jsx(Text, { type: "supporting", color: "secondary", maxLines: 1, style: { width: '100%', textAlign: 'center' }, children: name ?? part.mimeType })] }));
|
|
201
|
+
}
|
|
202
|
+
/** The width a tile keeps, so a long name is cut instead of widening it. */
|
|
203
|
+
const TILE_PX = 72;
|
|
204
|
+
const documentKeys = new WeakMap();
|
|
205
|
+
let nextKey = 0;
|
|
206
|
+
/** A key that follows the document, not its place, so removing one leaves the others' images alone. */
|
|
207
|
+
function documentKey(document) {
|
|
208
|
+
let key = documentKeys.get(document);
|
|
209
|
+
if (key === undefined) {
|
|
210
|
+
key = nextKey;
|
|
211
|
+
nextKey += 1;
|
|
212
|
+
documentKeys.set(document, key);
|
|
213
|
+
}
|
|
214
|
+
return String(key);
|
|
215
|
+
}
|
|
216
|
+
/** Each file as a tile in a scrolling row. */
|
|
217
|
+
function FileTiles({ form }) {
|
|
218
|
+
const t = useLabels();
|
|
219
|
+
const bytes = useBytes();
|
|
220
|
+
const files = form.documents.flatMap((document, at) => {
|
|
221
|
+
const part = mediaOf(document.input);
|
|
222
|
+
return part ? [{ document, part, at }] : [];
|
|
223
|
+
});
|
|
224
|
+
if (files.length === 0)
|
|
225
|
+
return null;
|
|
226
|
+
return (_jsx(HStack, { gap: 2, wrap: "nowrap", style: { overflowX: 'auto' }, children: files.map(({ document, part, at }) => {
|
|
227
|
+
const { label, detail } = documentLabel(t, bytes, document, at + 1);
|
|
228
|
+
return (_jsx(FileTile, { name: document.from, part: part, label: [label, detail].filter(Boolean).join(' · '), onRemove: () => {
|
|
229
|
+
form.remove([at]);
|
|
230
|
+
} }, documentKey(document)));
|
|
231
|
+
}) }));
|
|
232
|
+
}
|
|
233
|
+
/** A preview of the documents: a few pieces per source, and every file as a tile. */
|
|
234
|
+
function DocumentList({ form }) {
|
|
235
|
+
const t = useLabels();
|
|
236
|
+
const groups = groupDocuments(form, t);
|
|
237
|
+
if (groups.length === 0 && form.documents.length === 0)
|
|
238
|
+
return null;
|
|
239
|
+
return (_jsxs(VStack, { gap: 3, children: [groups.map((group) => (_jsx(TextGroup, { group: group, form: form }, group.source))), _jsx(FileTiles, { form: form })] }));
|
|
240
|
+
}
|
|
241
|
+
/** Text with its file beside it at two thirds and one third, or whichever of them there is. */
|
|
242
|
+
export function TextBesideFile({ text, file }) {
|
|
243
|
+
if (!text || !file)
|
|
244
|
+
return _jsx(_Fragment, { children: text ?? file });
|
|
245
|
+
return (_jsxs(Grid, { columns: 3, gap: 3, children: [_jsx(GridSpan, { columns: 2, children: text }), _jsx(GridSpan, { columns: 1, children: file })] }));
|
|
246
|
+
}
|
|
247
|
+
/** The file types the documents take: the profile's attachments, and text files when it takes text. */
|
|
248
|
+
function acceptedFiles(rules) {
|
|
249
|
+
const text = rules.text ? ['text/*', '.md', '.csv', '.json'] : [];
|
|
250
|
+
return [...(rules.attachments?.accept ?? []), ...text];
|
|
251
|
+
}
|
|
252
|
+
/** The text to split, and how it splits. */
|
|
253
|
+
function DocumentText({ form }) {
|
|
254
|
+
const t = useLabels();
|
|
255
|
+
return (_jsxs(VStack, { gap: 3, children: [_jsx(TextArea, { label: t('@theorem.embedding.text'), isLabelHidden: true, value: form.text, placeholder: t('@theorem.embedding.text_placeholder'), rows: textRows(form.text, 4, 12), onChange: form.setText }), _jsx(SplitFields, { form: form })] }));
|
|
256
|
+
}
|
|
257
|
+
/** Text to split, files to add, and the documents they make. */
|
|
258
|
+
function DocumentFields({ form, rules }) {
|
|
259
|
+
const t = useLabels();
|
|
260
|
+
const accept = acceptedFiles(rules);
|
|
261
|
+
const drop = accept.length > 0 ? (_jsx(EmbeddingFileDrop, { label: t('@theorem.embedding.files'), placeholder: t('@theorem.embedding.drop_files'), accept: accept, isMultiple: true, onFiles: form.addFiles })) : null;
|
|
262
|
+
return (_jsxs(VStack, { gap: 3, children: [_jsx(TextBesideFile, { text: rules.text ? _jsx(DocumentText, { form: form }) : null, file: drop }), _jsx(DocumentList, { form: form })] }));
|
|
263
|
+
}
|
|
264
|
+
/** What the documents still lack, or how many requests they make and the shortcut. */
|
|
265
|
+
function documentsNote(t, form) {
|
|
266
|
+
if (form.jsonInvalid)
|
|
267
|
+
return t('@theorem.embedding.invalid_json');
|
|
268
|
+
if (form.inputs.length === 0)
|
|
269
|
+
return t('@theorem.embedding.no_documents');
|
|
270
|
+
return (form.issue ??
|
|
271
|
+
t('@theorem.embedding.ready', { count: form.inputs.length, requests: form.requests }));
|
|
272
|
+
}
|
|
273
|
+
/** The documents card: text and files as fields, or the inputs as JSON, and Index. */
|
|
274
|
+
export function EmbeddingDocuments({ form, rules, isRunning, onRun, onStop, }) {
|
|
275
|
+
const t = useLabels();
|
|
276
|
+
const isReady = !form.jsonInvalid && form.inputs.length > 0 && form.issue === null;
|
|
277
|
+
return (_jsxs(RequestCard, { title: t('@theorem.embedding.documents'), view: form.view, hasFields: true, onView: form.setView, onRun: onRun, children: [form.view === 'fields' ? (_jsx(DocumentFields, { form: form, rules: rules })) : (_jsx(JsonRequest, { label: t('@theorem.embedding.documents'), text: form.json, isInvalid: form.jsonInvalid, onChange: form.setJson })), _jsx(RunBar, { note: documentsNote(t, form), isReady: isReady, isRunning: isRunning, runLabel: t('@theorem.embedding.index'), runningLabel: t('@theorem.embedding.indexing'), stopLabel: t('@theorem.embedding.stop'), onRun: onRun, onStop: onStop })] }));
|
|
278
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type EmbeddingInput } from '@theoremjs/agents';
|
|
2
|
+
import { type EmbeddingFields } from '../client/embedding-input.ts';
|
|
3
|
+
import { type ConsoleView } from './ConsoleFrame.tsx';
|
|
4
|
+
import { type Rules } from './EmbeddingDocuments.tsx';
|
|
5
|
+
/** Where the embedded documents stand for a search: as embedded, or changed since. */
|
|
6
|
+
export type IndexState = 'fresh' | 'stale' | 'model';
|
|
7
|
+
/** The query, held as the JSON of the one input it sends, and the view it is edited in. */
|
|
8
|
+
export declare function useEmbeddingQuery(defaultQuery: string, rules: Rules): {
|
|
9
|
+
json: string;
|
|
10
|
+
setJson: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
11
|
+
input: EmbeddingInput | undefined;
|
|
12
|
+
fields: EmbeddingFields | null;
|
|
13
|
+
issue: string | null;
|
|
14
|
+
shown: ConsoleView;
|
|
15
|
+
setView: import("react").Dispatch<import("react").SetStateAction<ConsoleView>>;
|
|
16
|
+
setFields: (next: EmbeddingFields) => void;
|
|
17
|
+
};
|
|
18
|
+
export type EmbeddingQueryForm = ReturnType<typeof useEmbeddingQuery>;
|
|
19
|
+
/** The query card: its words and file, or its input as JSON, and Search. */
|
|
20
|
+
export declare function EmbeddingQuery({ form, rules, index, isRunning, onRun, onStop, }: {
|
|
21
|
+
form: EmbeddingQueryForm;
|
|
22
|
+
rules: Rules;
|
|
23
|
+
index: IndexState;
|
|
24
|
+
isRunning: boolean;
|
|
25
|
+
onRun: (input: EmbeddingInput) => void;
|
|
26
|
+
onStop: () => void;
|
|
27
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { TextArea } from '@astryxdesign/core/TextArea';
|
|
3
|
+
import { base64Bytes } from '@theoremjs/agents';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { checkInputs, fieldsInput, inputFields, readEmbeddingInput, } from "../client/embedding-input.js";
|
|
6
|
+
import { encodeFiles } from "../client/encode-files.js";
|
|
7
|
+
import { JsonRequest, RequestCard, RunBar, useBytes } from "./ConsoleFrame.js";
|
|
8
|
+
import { EmbeddingFileDrop, FileTile, inputIssue, TextBesideFile, } from "./EmbeddingDocuments.js";
|
|
9
|
+
import { useLabels } from "./labels-provider.js";
|
|
10
|
+
import { textRows } from "./StateFields.js";
|
|
11
|
+
/** The query, held as the JSON of the one input it sends, and the view it is edited in. */
|
|
12
|
+
export function useEmbeddingQuery(defaultQuery, rules) {
|
|
13
|
+
const t = useLabels();
|
|
14
|
+
const [json, setJson] = useState(() => JSON.stringify(defaultQuery));
|
|
15
|
+
const input = readEmbeddingInput(json);
|
|
16
|
+
const fields = input === undefined ? null : inputFields(input);
|
|
17
|
+
const [view, setView] = useState('fields');
|
|
18
|
+
const issue = input === undefined
|
|
19
|
+
? t('@theorem.embedding.query_invalid_json')
|
|
20
|
+
: inputIssue(t, checkInputs([input], rules), t('@theorem.embedding.query'));
|
|
21
|
+
return {
|
|
22
|
+
json,
|
|
23
|
+
setJson,
|
|
24
|
+
input,
|
|
25
|
+
fields,
|
|
26
|
+
issue,
|
|
27
|
+
shown: fields === null ? 'json' : view,
|
|
28
|
+
setView,
|
|
29
|
+
setFields: (next) => {
|
|
30
|
+
setJson(JSON.stringify(fieldsInput(next), null, 2));
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** The query's one file, or a drop for it when the profile takes files. */
|
|
35
|
+
function QueryFile({ fields, accept, onChange, }) {
|
|
36
|
+
const t = useLabels();
|
|
37
|
+
const bytes = useBytes();
|
|
38
|
+
const part = fields.media[0];
|
|
39
|
+
if (part) {
|
|
40
|
+
const size = part.type === 'media' ? bytes(base64Bytes(part.data)) : t('@theorem.embedding.held_file');
|
|
41
|
+
return (_jsx(FileTile, { part: part, label: [part.mimeType, size].join(' · '), onRemove: () => {
|
|
42
|
+
onChange({ ...fields, media: [] });
|
|
43
|
+
} }));
|
|
44
|
+
}
|
|
45
|
+
if (!accept)
|
|
46
|
+
return null;
|
|
47
|
+
return (_jsx(EmbeddingFileDrop, { label: t('@theorem.embedding.query_file'), placeholder: t('@theorem.embedding.query_file'), accept: accept, onFiles: async (files) => {
|
|
48
|
+
const encoded = await encodeFiles(files);
|
|
49
|
+
onChange({
|
|
50
|
+
...fields,
|
|
51
|
+
media: encoded.map(({ mimeType, data }) => ({
|
|
52
|
+
type: 'media',
|
|
53
|
+
mimeType,
|
|
54
|
+
data,
|
|
55
|
+
})),
|
|
56
|
+
});
|
|
57
|
+
} }));
|
|
58
|
+
}
|
|
59
|
+
/** The query's words, and one file to search with when the profile takes files. */
|
|
60
|
+
function QueryFields({ fields, rules, onChange, }) {
|
|
61
|
+
const t = useLabels();
|
|
62
|
+
const accept = rules.attachments?.accept;
|
|
63
|
+
const text = rules.text ? (_jsx(TextArea, { label: t('@theorem.embedding.query'), isLabelHidden: true, value: fields.text, placeholder: t('@theorem.embedding.query_placeholder'), rows: textRows(fields.text, 2, 6), onChange: (next) => {
|
|
64
|
+
onChange({ ...fields, text: next });
|
|
65
|
+
} })) : null;
|
|
66
|
+
const hasFile = fields.media.length > 0 || accept !== undefined;
|
|
67
|
+
const file = hasFile ? _jsx(QueryFile, { fields: fields, accept: accept, onChange: onChange }) : null;
|
|
68
|
+
return _jsx(TextBesideFile, { text: text, file: file });
|
|
69
|
+
}
|
|
70
|
+
/** What stands between the query and a search, once someone has tried one. */
|
|
71
|
+
function queryNote(t, form, index, tried) {
|
|
72
|
+
if (!tried)
|
|
73
|
+
return null;
|
|
74
|
+
if (form.issue)
|
|
75
|
+
return form.issue;
|
|
76
|
+
if (index === 'stale')
|
|
77
|
+
return t('@theorem.embedding.stale');
|
|
78
|
+
if (index === 'model')
|
|
79
|
+
return t('@theorem.embedding.remodelled');
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/** The query card: its words and file, or its input as JSON, and Search. */
|
|
83
|
+
export function EmbeddingQuery({ form, rules, index, isRunning, onRun, onStop, }) {
|
|
84
|
+
const t = useLabels();
|
|
85
|
+
const { input } = form;
|
|
86
|
+
const isReady = form.issue === null && index === 'fresh' && input !== undefined;
|
|
87
|
+
// why: What blocks a search is said only once someone tries one, not before they have started.
|
|
88
|
+
const [tried, setTried] = useState(false);
|
|
89
|
+
const run = () => {
|
|
90
|
+
setTried(true);
|
|
91
|
+
if (isReady)
|
|
92
|
+
onRun(input);
|
|
93
|
+
};
|
|
94
|
+
return (_jsxs(RequestCard, { title: t('@theorem.embedding.query'), view: form.shown, hasFields: form.fields !== null, onView: form.setView, onRun: run, children: [form.shown === 'fields' && form.fields !== null ? (_jsx(QueryFields, { fields: form.fields, rules: rules, onChange: form.setFields })) : (_jsx(JsonRequest, { label: t('@theorem.embedding.query'), text: form.json, isInvalid: input === undefined, onChange: form.setJson })), _jsx(RunBar, { note: queryNote(t, form, index, tried), isReady: isReady || !tried, isRunning: isRunning, runLabel: t('@theorem.embedding.search'), runningLabel: t('@theorem.embedding.searching'), stopLabel: t('@theorem.embedding.stop'), onRun: run, onStop: onStop })] }));
|
|
95
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { EmbeddingResult } from '@theoremjs/agents';
|
|
2
|
+
import type { EmbeddingMatch } from '../client/embedding-input.ts';
|
|
3
|
+
import type { EmbeddingIndex, IndexProgress } from '../hooks/use-embedding-index.ts';
|
|
4
|
+
import type { OneCallState } from '../hooks/use-one-call.ts';
|
|
5
|
+
import { type EmbeddingDocument } from './EmbeddingDocuments.tsx';
|
|
6
|
+
/** Which document the space and the matches both point at, and how either changes it. */
|
|
7
|
+
export type Pointing = {
|
|
8
|
+
hovered: number | null;
|
|
9
|
+
onHover(index: number | null): void;
|
|
10
|
+
};
|
|
11
|
+
/** Where indexing is, as `useEmbeddingIndex` reports it. */
|
|
12
|
+
export type IndexRun = OneCallState<'indexing', EmbeddingIndex> & {
|
|
13
|
+
progress: IndexProgress | null;
|
|
14
|
+
};
|
|
15
|
+
/** Where the search is, as `useTheoremEmbedding` reports it. */
|
|
16
|
+
export type SearchRun = OneCallState<'embedding', EmbeddingResult>;
|
|
17
|
+
export type IndexCardProps = {
|
|
18
|
+
run: IndexRun;
|
|
19
|
+
/** The documents as they were indexed. */
|
|
20
|
+
documents: readonly EmbeddingDocument[];
|
|
21
|
+
/** The last query's vector, once a search has run. */
|
|
22
|
+
query: readonly number[] | null;
|
|
23
|
+
/** The documents ranked by the query, best first. */
|
|
24
|
+
matches: readonly EmbeddingMatch[];
|
|
25
|
+
pointing: Pointing;
|
|
26
|
+
};
|
|
27
|
+
/** The index: the documents as a space or a list, or why it failed. */
|
|
28
|
+
export declare function IndexCard(props: IndexCardProps): import("react").JSX.Element;
|
|
29
|
+
/**
|
|
30
|
+
* What the search found: the matches for the last query, ranked in the index,
|
|
31
|
+
* best first. Each keeps one quiet line for the model, time, tokens,
|
|
32
|
+
* and cost of its own calls.
|
|
33
|
+
*/
|
|
34
|
+
export declare function EmbeddingResults({ index, search, documents, matches, pointing, }: {
|
|
35
|
+
index: IndexRun;
|
|
36
|
+
search: SearchRun;
|
|
37
|
+
documents: readonly EmbeddingDocument[];
|
|
38
|
+
/** The documents ranked by the last query, best first. */
|
|
39
|
+
matches: readonly EmbeddingMatch[];
|
|
40
|
+
pointing: Pointing;
|
|
41
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Banner } from '@astryxdesign/core/Banner';
|
|
3
|
+
import { Button } from '@astryxdesign/core/Button';
|
|
4
|
+
import { HStack } from '@astryxdesign/core/HStack';
|
|
5
|
+
import { useClipboard } from '@astryxdesign/core/hooks';
|
|
6
|
+
import { Icon } from '@astryxdesign/core/Icon';
|
|
7
|
+
import { IconButton } from '@astryxdesign/core/IconButton';
|
|
8
|
+
import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core/SegmentedControl';
|
|
9
|
+
import { StackItem } from '@astryxdesign/core/Stack';
|
|
10
|
+
import { Text } from '@astryxdesign/core/Text';
|
|
11
|
+
import { VStack } from '@astryxdesign/core/VStack';
|
|
12
|
+
import { IconCheck, IconCopy } from '@tabler/icons-react';
|
|
13
|
+
import { lazy, Suspense, useEffect, useMemo, useState } from 'react';
|
|
14
|
+
import { layoutSpaceSoon, outlierThreshold, placeQuery, } from "../client/embedding-space.js";
|
|
15
|
+
import { Arrive } from "./arrive.js";
|
|
16
|
+
import { useBytes } from "./ConsoleFrame.js";
|
|
17
|
+
import { ResultStack, Share, useNumbers } from "./DecisionAnswers.js";
|
|
18
|
+
import { documentLabel } from "./EmbeddingDocuments.js";
|
|
19
|
+
import { useLabels } from "./labels-provider.js";
|
|
20
|
+
import { keyedByContent } from "./row-keys.js";
|
|
21
|
+
import { ChartBody, ChartCard } from "./trace-patterns.js";
|
|
22
|
+
const EmbeddingSpaceView = lazy(() => import("./embedding-space-view.js").then((module) => ({ default: module.EmbeddingSpaceView })));
|
|
23
|
+
/** Matches the card lists, best first; the space highlights the same ones. */
|
|
24
|
+
const MATCHES_SHOWN = 10;
|
|
25
|
+
/** The most the space or the list stands, in pixels. */
|
|
26
|
+
const SPACE_PX = 460;
|
|
27
|
+
/** Values a vector shows before the rest are counted. */
|
|
28
|
+
const PREVIEW_VALUES = 4;
|
|
29
|
+
/** A list that reads as one to a screen reader and looks like none: no bullets, no indent. */
|
|
30
|
+
const BARE_LIST = {
|
|
31
|
+
listStyle: 'none',
|
|
32
|
+
margin: 0,
|
|
33
|
+
padding: 0,
|
|
34
|
+
display: 'flex',
|
|
35
|
+
flexDirection: 'column',
|
|
36
|
+
gap: 12,
|
|
37
|
+
};
|
|
38
|
+
/** Ground behind the document both views point at. */
|
|
39
|
+
const POINTED = 'var(--color-background-muted)';
|
|
40
|
+
function Failure({ failure }) {
|
|
41
|
+
return failure ? _jsx(Banner, { status: "error", role: "alert", title: failure.error }) : null;
|
|
42
|
+
}
|
|
43
|
+
/** A document's name as both cards word it: its words, its file's name, or its number. */
|
|
44
|
+
function useDocumentName(inputs, documents) {
|
|
45
|
+
const t = useLabels();
|
|
46
|
+
const bytes = useBytes();
|
|
47
|
+
return (at) => documentLabel(t, bytes, documents[at] ?? { input: inputs[at] }, at + 1).label;
|
|
48
|
+
}
|
|
49
|
+
/** The documents most like the query, each with its cosine similarity, best first. */
|
|
50
|
+
function MatchesBody({ matches, index, documents, pointing, }) {
|
|
51
|
+
const t = useLabels();
|
|
52
|
+
const n = useNumbers();
|
|
53
|
+
const label = useDocumentName(index.inputs, documents);
|
|
54
|
+
const lead = matches[0];
|
|
55
|
+
if (!lead)
|
|
56
|
+
return _jsx(Banner, { status: "warning", title: t('@theorem.embedding.no_matches') });
|
|
57
|
+
const shown = matches.slice(0, MATCHES_SHOWN);
|
|
58
|
+
return (_jsx(ChartBody, { title: t('@theorem.embedding.matches'), value: n.decimal.format(lead.score), note: matches.length > shown.length
|
|
59
|
+
? t('@theorem.embedding.matches_shown', { shown: shown.length, count: matches.length })
|
|
60
|
+
: t('@theorem.embedding.best', { name: label(lead.index) }), children: _jsx("ul", { style: BARE_LIST, children: shown.map((match, at) => {
|
|
61
|
+
const name = label(match.index);
|
|
62
|
+
const score = n.decimal.format(match.score);
|
|
63
|
+
return (_jsx("li", { "aria-label": `${name}: ${score}`, onPointerEnter: () => pointing.onHover(match.index), onPointerLeave: () => pointing.onHover(null), style: {
|
|
64
|
+
borderRadius: 'var(--radius-container)',
|
|
65
|
+
margin: '-4px -8px',
|
|
66
|
+
padding: '4px 8px',
|
|
67
|
+
background: pointing.hovered === match.index ? POINTED : undefined,
|
|
68
|
+
}, children: _jsxs(VStack, { gap: 1, children: [_jsxs(HStack, { gap: 3, vAlign: "start", "aria-hidden": true, children: [_jsx(StackItem, { size: "fill", children: _jsx(Text, { type: "supporting", maxLines: 2, children: name }) }), _jsx(Text, { type: "supporting", color: "secondary", hasTabularNumbers: true, children: score })] }), _jsx(Share, { value: Math.max(match.score, 0), isLead: at === 0 })] }) }, match.index));
|
|
69
|
+
}) }) }));
|
|
70
|
+
}
|
|
71
|
+
/** Copies a text; the button says so for a moment after. */
|
|
72
|
+
function CopyText({ text, label, isIcon }) {
|
|
73
|
+
const t = useLabels();
|
|
74
|
+
const copied = t('@theorem.embedding.copied');
|
|
75
|
+
const { copy, isCopied } = useClipboard({ announce: copied });
|
|
76
|
+
const shown = isCopied ? copied : label;
|
|
77
|
+
return isIcon ? (_jsx(IconButton, { label: shown, tooltip: shown, size: "sm", variant: "ghost", icon: _jsx(Icon, { icon: isCopied ? IconCheck : IconCopy, size: "xsm", color: "secondary" }), onClick: () => void copy(text) })) : (_jsx(Button, { label: shown, size: "sm", variant: "ghost", onClick: () => void copy(text) }));
|
|
78
|
+
}
|
|
79
|
+
/** Every document with the first values of its vector, each vector copyable, or all of them at once. */
|
|
80
|
+
function VectorList({ vectors, name, pointing, }) {
|
|
81
|
+
const t = useLabels();
|
|
82
|
+
const all = useMemo(() => JSON.stringify(vectors), [vectors]);
|
|
83
|
+
return (_jsxs(VStack, { gap: 2, children: [_jsxs(HStack, { gap: 2, vAlign: "center", children: [_jsx(StackItem, { size: "fill", children: _jsx(Text, { type: "supporting", color: "secondary", children: t('@theorem.embedding.width', { dimensions: vectors[0]?.length ?? 0 }) }) }), _jsx(CopyText, { text: all, label: t('@theorem.embedding.copy_vectors') })] }), _jsx("ul", { style: { ...BARE_LIST, gap: 0, maxHeight: SPACE_PX, overflowY: 'auto' }, children: keyedByContent(vectors, (vector) => vector.slice(0, PREVIEW_VALUES * 2).join()).map(({ item: vector, index: at, key }) => (_jsx("li", { onPointerEnter: () => pointing.onHover(at), onPointerLeave: () => pointing.onHover(null), style: {
|
|
84
|
+
borderRadius: 'var(--radius-container)',
|
|
85
|
+
padding: '4px 8px',
|
|
86
|
+
background: pointing.hovered === at ? POINTED : undefined,
|
|
87
|
+
}, children: _jsxs(HStack, { gap: 2, vAlign: "center", children: [_jsx(StackItem, { size: "fill", children: _jsxs(VStack, { gap: 0, children: [_jsx(Text, { type: "supporting", maxLines: 1, children: name(at) }), _jsx(Text, { type: "supporting", color: "secondary", maxLines: 1, hasTabularNumbers: true, children: `[${vector
|
|
88
|
+
.slice(0, PREVIEW_VALUES)
|
|
89
|
+
.map((v) => v.toFixed(4))
|
|
90
|
+
.join(', ')}${vector.length > PREVIEW_VALUES ? ', …' : ''}]` })] }) }), _jsx(CopyText, { text: JSON.stringify(vector), label: t('@theorem.embedding.copy_vector'), isIcon: true })] }) }, key))) })] }));
|
|
91
|
+
}
|
|
92
|
+
/** The space for an index, laid out in slices so a large one never freezes the page; `null` until it is ready. */
|
|
93
|
+
function useSpace(vectors) {
|
|
94
|
+
const [space, setSpace] = useState(null);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const stop = new AbortController();
|
|
97
|
+
layoutSpaceSoon(vectors, stop.signal).then((next) => setSpace({ of: vectors, space: next }),
|
|
98
|
+
// why: only the abort rejects, when the vectors changed or the card went; the newer run answers.
|
|
99
|
+
() => { });
|
|
100
|
+
return () => stop.abort();
|
|
101
|
+
}, [vectors]);
|
|
102
|
+
return space?.of === vectors ? space.space : null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The matches the space joins to the query: those shown whose similarity
|
|
106
|
+
* stands out against the query's similarity to every document, each with how
|
|
107
|
+
* far it passes that threshold, from 0 at it to 1 at identical.
|
|
108
|
+
*/
|
|
109
|
+
function closenessOf(matches) {
|
|
110
|
+
const threshold = outlierThreshold(matches.map((m) => m.score));
|
|
111
|
+
return matches
|
|
112
|
+
.slice(0, MATCHES_SHOWN)
|
|
113
|
+
.filter((m) => m.score > threshold)
|
|
114
|
+
.map((m) => ({
|
|
115
|
+
index: m.index,
|
|
116
|
+
closeness: threshold < 1 ? Math.min((m.score - threshold) / (1 - threshold), 1) : 1,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
const SPACE_ROOM = { width: '100%', aspectRatio: '1 / 1', maxHeight: SPACE_PX };
|
|
120
|
+
/** The space, with the last query landed among its matches; an empty box of the same size until it is laid out. */
|
|
121
|
+
function IndexSpace({ space, query, matches, name, pointing, onUnavailable, }) {
|
|
122
|
+
const t = useLabels();
|
|
123
|
+
const landed = useMemo(() => (space && query ? placeQuery(space, query) : null), [space, query]);
|
|
124
|
+
const close = useMemo(() => (landed ? closenessOf(matches) : []), [landed, matches]);
|
|
125
|
+
if (!space)
|
|
126
|
+
return _jsx("div", { style: SPACE_ROOM });
|
|
127
|
+
const label = t('@theorem.embedding.space_label', { count: space.documents.length });
|
|
128
|
+
return (_jsx(Suspense, { fallback: _jsx("div", { style: SPACE_ROOM }), children: _jsx(EmbeddingSpaceView, { space: space, query: landed, matches: close, hovered: pointing.hovered, onHover: pointing.onHover, nameOf: name, label: landed ? `${label} ${t('@theorem.embedding.space_query')}` : label, onUnavailable: onUnavailable }) }));
|
|
129
|
+
}
|
|
130
|
+
/** The switch between the space and the list. */
|
|
131
|
+
function ViewSwitch({ view, onChange }) {
|
|
132
|
+
const t = useLabels();
|
|
133
|
+
return (_jsxs(SegmentedControl, { label: t('@theorem.embedding.view'), size: "sm", value: view, onChange: (next) => onChange(next === 'list' ? 'list' : 'space'), children: [_jsx(SegmentedControlItem, { value: "space", label: t('@theorem.embedding.space') }), _jsx(SegmentedControlItem, { value: "list", label: t('@theorem.embedding.list') })] }));
|
|
134
|
+
}
|
|
135
|
+
/** The vectors listed, saying first when the space could not be drawn. */
|
|
136
|
+
function IndexList({ vectors, name, pointing, isDrawable, }) {
|
|
137
|
+
const t = useLabels();
|
|
138
|
+
return (_jsxs(VStack, { gap: 2, children: [isDrawable ? null : (_jsx(Text, { type: "supporting", color: "secondary", children: t('@theorem.embedding.space_off') })), _jsx(VectorList, { vectors: vectors, name: name, pointing: pointing })] }));
|
|
139
|
+
}
|
|
140
|
+
/** The finished index, as a space or a list. */
|
|
141
|
+
function IndexedCard({ run, result, documents, query, matches, pointing, }) {
|
|
142
|
+
const t = useLabels();
|
|
143
|
+
const [view, setView] = useState('space');
|
|
144
|
+
const [isDrawable, setDrawable] = useState(true);
|
|
145
|
+
const space = useSpace(result.vectors);
|
|
146
|
+
const name = useDocumentName(result.inputs, documents);
|
|
147
|
+
return (_jsxs(VStack, { gap: 4, children: [_jsx(Failure, { failure: run.failure }), _jsx(ResultStack, { model: result.model, usage: result.usage, elapsedMs: run.elapsedMs, tokens: (count) => t('@theorem.embedding.tokens', { count }), children: _jsx(ChartCard, { title: t('@theorem.embedding.index'), value: t('@theorem.embedding.indexed', { count: result.vectors.length }), aside: isDrawable ? _jsx(ViewSwitch, { view: view, onChange: setView }) : null, children: isDrawable && view === 'space' ? (_jsx(IndexSpace, { space: space, query: query, matches: matches, name: name, pointing: pointing, onUnavailable: () => setDrawable(false) })) : (_jsx(IndexList, { vectors: result.vectors, name: name, pointing: pointing, isDrawable: isDrawable })) }) })] }));
|
|
148
|
+
}
|
|
149
|
+
/** The index: the documents as a space or a list, or why it failed. */
|
|
150
|
+
export function IndexCard(props) {
|
|
151
|
+
const { result, failure } = props.run;
|
|
152
|
+
if (!result)
|
|
153
|
+
return _jsx(Failure, { failure: failure });
|
|
154
|
+
return _jsx(IndexedCard, { ...props, result: result });
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* What the search found: the matches for the last query, ranked in the index,
|
|
158
|
+
* best first. Each keeps one quiet line for the model, time, tokens,
|
|
159
|
+
* and cost of its own calls.
|
|
160
|
+
*/
|
|
161
|
+
export function EmbeddingResults({ index, search, documents, matches, pointing, }) {
|
|
162
|
+
const t = useLabels();
|
|
163
|
+
const searched = search.result?.embeddings[0]?.values;
|
|
164
|
+
return (_jsxs(VStack, { gap: 4, children: [_jsx(Failure, { failure: search.failure }), searched && index.result ? (_jsx(ResultStack, { model: search.result?.model ?? '', usage: search.result?.usage, elapsedMs: search.elapsedMs, isStale: search.status === 'embedding', tokens: (count) => t('@theorem.embedding.tokens', { count }), children: _jsx(Arrive, { children: _jsx(MatchesBody, { matches: matches, index: index.result, documents: documents, pointing: pointing }) }) })) : null] }));
|
|
165
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { EmbeddingSpace, SpacePoint } from '../client/embedding-space.ts';
|
|
2
|
+
import type { QueryMatch } from '../client/space-geometry.ts';
|
|
3
|
+
export type EmbeddingSpaceViewProps = {
|
|
4
|
+
space: EmbeddingSpace;
|
|
5
|
+
/** Where the last query landed, or `null` before any. */
|
|
6
|
+
query: SpacePoint | null;
|
|
7
|
+
matches: readonly QueryMatch[];
|
|
8
|
+
hovered: number | null;
|
|
9
|
+
onHover(index: number | null): void;
|
|
10
|
+
/** A document's name, as the matches list words it. */
|
|
11
|
+
nameOf(index: number): string;
|
|
12
|
+
/** What the canvas says to a screen reader. */
|
|
13
|
+
label: string;
|
|
14
|
+
/** The browser cannot draw the space. */
|
|
15
|
+
onUnavailable(): void;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The index drawn as a space: each document a dot in a ball, placed by the
|
|
19
|
+
* three directions the vectors differ most along, with a circle round the rim
|
|
20
|
+
* in each plane two of them span. Each document is joined to its nearest
|
|
21
|
+
* neighbour by cosine similarity when that pair stands out from the index's
|
|
22
|
+
* typical pair; documents the lines connect share a colour, and a query's
|
|
23
|
+
* white dot has a line to each match that stands out the same way. Drag to turn
|
|
24
|
+
* it; point at a dot to name its document.
|
|
25
|
+
*/
|
|
26
|
+
export declare function EmbeddingSpaceView(props: EmbeddingSpaceViewProps): import("react").JSX.Element;
|