@stll/chat 0.0.1-placeholder.0 → 0.1.2
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 +28 -0
- package/dist/composer.d.ts +31 -0
- package/dist/composer.js +78 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/model-selector.d.ts +32 -0
- package/dist/model-selector.js +77 -0
- package/dist/runtime.d.ts +78 -0
- package/dist/runtime.js +144 -0
- package/dist/stream.d.ts +14 -0
- package/dist/stream.js +13 -0
- package/package.json +84 -3
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# `@stll/chat`
|
|
2
|
+
|
|
3
|
+
Portable React chat primitives: a rich-editor composer shell, message stream,
|
|
4
|
+
model selector, and observable streaming runtime.
|
|
5
|
+
|
|
6
|
+
The host owns authentication, endpoint selection, persistence, provider
|
|
7
|
+
credentials, translations, and rich-message rendering. It supplies a typed
|
|
8
|
+
`ChatTransport`; the package never reads environment variables or silently
|
|
9
|
+
selects a provider.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { ChatComposer, ChatMessageStream, createChatRuntime } from "@stll/chat";
|
|
13
|
+
|
|
14
|
+
const runtime = createChatRuntime({ transport });
|
|
15
|
+
|
|
16
|
+
<ChatComposer
|
|
17
|
+
canSend={draft.length > 0}
|
|
18
|
+
isGenerating={runtime.getSnapshot().isStreaming}
|
|
19
|
+
labels={{ retry: "Retry", send: "Send", stop: "Stop" }}
|
|
20
|
+
onSend={sendDraft}
|
|
21
|
+
>
|
|
22
|
+
<RichEditor />
|
|
23
|
+
</ChatComposer>;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Validate every explicit provider/model selection with
|
|
27
|
+
`resolveChatModelSelection`. It rejects unknown, empty, duplicate, and
|
|
28
|
+
unconfigured provider/model combinations with `ChatConfigurationError`.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/composer.d.ts
|
|
3
|
+
type ChatComposerActionState = {
|
|
4
|
+
canSend: boolean;
|
|
5
|
+
isGenerating: boolean;
|
|
6
|
+
onRetry?: (() => void) | undefined;
|
|
7
|
+
onSend: () => void;
|
|
8
|
+
onStop?: (() => void) | undefined;
|
|
9
|
+
};
|
|
10
|
+
type ChatComposerActionMode = "retry" | "send" | "stop";
|
|
11
|
+
declare const resolveChatComposerAction: ({ isGenerating, onRetry, onStop }: Pick<ChatComposerActionState, "isGenerating" | "onRetry" | "onStop">) => ChatComposerActionMode;
|
|
12
|
+
type ChatComposerLabels = Record<ChatComposerActionMode, string>;
|
|
13
|
+
declare const ChatComposerAction: ({ canSend, className, isGenerating, labels, onRetry, onSend, onStop }: ChatComposerActionState & {
|
|
14
|
+
className?: string | undefined;
|
|
15
|
+
labels: ChatComposerLabels;
|
|
16
|
+
}) => import("react").JSX.Element;
|
|
17
|
+
type ChatComposerProps = ChatComposerActionState & {
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
className?: string | undefined;
|
|
20
|
+
labels: ChatComposerLabels;
|
|
21
|
+
leading?: ReactNode | undefined;
|
|
22
|
+
status?: ReactNode | undefined;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Layout-only composer shell for rich editors. `children` is deliberately a
|
|
26
|
+
* host-owned editor slot, so TipTap, ProseMirror, native inputs, and mobile
|
|
27
|
+
* editors can share send/stop/retry behavior without copying shell chrome.
|
|
28
|
+
*/
|
|
29
|
+
declare const ChatComposer: ({ children, className, labels, leading, status, ...action }: ChatComposerProps) => import("react").JSX.Element;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { ChatComposer, ChatComposerAction, ChatComposerActionMode, ChatComposerActionState, ChatComposerLabels, ChatComposerProps, resolveChatComposerAction };
|
package/dist/composer.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ArrowUpIcon, RotateCcwIcon, SquareIcon } from "lucide-react";
|
|
2
|
+
import { Button } from "@stll/ui/button";
|
|
3
|
+
import { cn } from "@stll/ui/utils";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/composer.tsx
|
|
6
|
+
const resolveChatComposerAction = ({ isGenerating, onRetry, onStop }) => {
|
|
7
|
+
if (isGenerating && onStop !== void 0) return "stop";
|
|
8
|
+
if (!isGenerating && onRetry !== void 0) return "retry";
|
|
9
|
+
return "send";
|
|
10
|
+
};
|
|
11
|
+
const ChatComposerAction = ({ canSend, className, isGenerating, labels, onRetry, onSend, onStop }) => {
|
|
12
|
+
const mode = resolveChatComposerAction({
|
|
13
|
+
isGenerating,
|
|
14
|
+
onRetry,
|
|
15
|
+
onStop
|
|
16
|
+
});
|
|
17
|
+
const disabled = mode === "send" && !canSend;
|
|
18
|
+
const onClick = () => {
|
|
19
|
+
switch (mode) {
|
|
20
|
+
case "send":
|
|
21
|
+
onSend();
|
|
22
|
+
return;
|
|
23
|
+
case "stop":
|
|
24
|
+
onStop?.();
|
|
25
|
+
return;
|
|
26
|
+
case "retry":
|
|
27
|
+
onRetry?.();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
return /* @__PURE__ */ jsxs(Button, {
|
|
32
|
+
"aria-label": labels[mode],
|
|
33
|
+
className: cn("bg-foreground text-background hover:bg-foreground/90 size-7 shrink-0 rounded-full", className),
|
|
34
|
+
disabled,
|
|
35
|
+
onClick,
|
|
36
|
+
size: "icon-sm",
|
|
37
|
+
tooltip: false,
|
|
38
|
+
type: "button",
|
|
39
|
+
children: [
|
|
40
|
+
mode === "send" && /* @__PURE__ */ jsx(ArrowUpIcon, {
|
|
41
|
+
"aria-hidden": "true",
|
|
42
|
+
className: "size-3.5"
|
|
43
|
+
}),
|
|
44
|
+
mode === "stop" && /* @__PURE__ */ jsx(SquareIcon, {
|
|
45
|
+
"aria-hidden": "true",
|
|
46
|
+
className: "size-3.5"
|
|
47
|
+
}),
|
|
48
|
+
mode === "retry" && /* @__PURE__ */ jsx(RotateCcwIcon, {
|
|
49
|
+
"aria-hidden": "true",
|
|
50
|
+
className: "size-3.5"
|
|
51
|
+
})
|
|
52
|
+
]
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Layout-only composer shell for rich editors. `children` is deliberately a
|
|
57
|
+
* host-owned editor slot, so TipTap, ProseMirror, native inputs, and mobile
|
|
58
|
+
* editors can share send/stop/retry behavior without copying shell chrome.
|
|
59
|
+
*/
|
|
60
|
+
const ChatComposer = ({ children, className, labels, leading, status, ...action }) => /* @__PURE__ */ jsxs("section", {
|
|
61
|
+
className: cn("flex flex-col gap-1.5", className),
|
|
62
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
63
|
+
className: "border-input bg-background flex min-w-0 items-end gap-2 rounded-xl border p-2",
|
|
64
|
+
children: [
|
|
65
|
+
leading,
|
|
66
|
+
/* @__PURE__ */ jsx("div", {
|
|
67
|
+
className: "min-w-0 flex-1",
|
|
68
|
+
children
|
|
69
|
+
}),
|
|
70
|
+
/* @__PURE__ */ jsx(ChatComposerAction, {
|
|
71
|
+
...action,
|
|
72
|
+
labels
|
|
73
|
+
})
|
|
74
|
+
]
|
|
75
|
+
}), status]
|
|
76
|
+
});
|
|
77
|
+
//#endregion
|
|
78
|
+
export { ChatComposer, ChatComposerAction, resolveChatComposerAction };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ChatComposer, ChatComposerAction, ChatComposerActionMode, ChatComposerActionState, ChatComposerLabels, ChatComposerProps, resolveChatComposerAction } from "./composer.js";
|
|
2
|
+
import { ChatConfigurationError, ChatMessage, ChatModelOption, ChatModelSelection, ChatProviderConfiguration, ChatRuntime, ChatRuntimeSnapshot, ChatStreamError, ChatStreamEvent, ChatTransport, createChatRuntime, resolveChatModelSelection } from "./runtime.js";
|
|
3
|
+
import { ChatModelSelector, ChatModelSelectorProps, compactModelDisplayName, groupReasoningEfforts, modelSelectionLabel } from "./model-selector.js";
|
|
4
|
+
import { ChatMessageStream, ChatMessageStreamProps } from "./stream.js";
|
|
5
|
+
export { ChatComposer, ChatComposerAction, type ChatComposerActionMode, type ChatComposerActionState, type ChatComposerLabels, type ChatComposerProps, ChatConfigurationError, type ChatMessage, ChatMessageStream, type ChatMessageStreamProps, type ChatModelOption, type ChatModelSelection, ChatModelSelector, type ChatModelSelectorProps, type ChatProviderConfiguration, type ChatRuntime, type ChatRuntimeSnapshot, ChatStreamError, type ChatStreamEvent, type ChatTransport, compactModelDisplayName, createChatRuntime, groupReasoningEfforts, modelSelectionLabel, resolveChatComposerAction, resolveChatModelSelection };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ChatComposer, ChatComposerAction, resolveChatComposerAction } from "./composer.js";
|
|
2
|
+
import { ChatConfigurationError, ChatStreamError, createChatRuntime, resolveChatModelSelection } from "./runtime.js";
|
|
3
|
+
import { ChatModelSelector, compactModelDisplayName, groupReasoningEfforts, modelSelectionLabel } from "./model-selector.js";
|
|
4
|
+
import { ChatMessageStream } from "./stream.js";
|
|
5
|
+
export { ChatComposer, ChatComposerAction, ChatConfigurationError, ChatMessageStream, ChatModelSelector, ChatStreamError, compactModelDisplayName, createChatRuntime, groupReasoningEfforts, modelSelectionLabel, resolveChatComposerAction, resolveChatModelSelection };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ChatModelOption, ChatModelSelection } from "./runtime.js";
|
|
2
|
+
import { ReasoningEffort } from "@stll/ai-catalog";
|
|
3
|
+
//#region src/model-selector.d.ts
|
|
4
|
+
declare const compactModelDisplayName: (displayName: string) => string;
|
|
5
|
+
declare const groupReasoningEfforts: (efforts: readonly ReasoningEffort[]) => {
|
|
6
|
+
extended: ReasoningEffort[];
|
|
7
|
+
standard: ReasoningEffort[];
|
|
8
|
+
};
|
|
9
|
+
declare const modelSelectionLabel: ({ defaultEffortLabel, displayName, formatSelectionLabel, providerDefaultEffort, reasoningEffort, translateEffort }: {
|
|
10
|
+
defaultEffortLabel: string;
|
|
11
|
+
displayName: string;
|
|
12
|
+
formatSelectionLabel: (values: {
|
|
13
|
+
effort: string;
|
|
14
|
+
model: string;
|
|
15
|
+
}) => string;
|
|
16
|
+
providerDefaultEffort: ReasoningEffort | null;
|
|
17
|
+
reasoningEffort: ReasoningEffort | null;
|
|
18
|
+
translateEffort: (effort: ReasoningEffort) => string;
|
|
19
|
+
}) => string;
|
|
20
|
+
type ChatModelSelectorProps = {
|
|
21
|
+
autoLabel: string;
|
|
22
|
+
disabled?: boolean | undefined;
|
|
23
|
+
label: string;
|
|
24
|
+
onSelectionChange: (selection: ChatModelSelection) => void;
|
|
25
|
+
options: readonly ChatModelOption[];
|
|
26
|
+
selection: ChatModelSelection;
|
|
27
|
+
};
|
|
28
|
+
/** Native, accessible model control. Provider configuration is validated by
|
|
29
|
+
* `resolveChatModelSelection` before a host accepts the new selection. */
|
|
30
|
+
declare const ChatModelSelector: ({ autoLabel, disabled, label, onSelectionChange, options, selection }: ChatModelSelectorProps) => import("react").JSX.Element;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { ChatModelSelector, ChatModelSelectorProps, compactModelDisplayName, groupReasoningEfforts, modelSelectionLabel };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ChatConfigurationError } from "./runtime.js";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/model-selector.tsx
|
|
4
|
+
const CLAUDE_FAMILY_PREFIX = /^Claude\s+/u;
|
|
5
|
+
const compactModelDisplayName = (displayName) => displayName.replace(CLAUDE_FAMILY_PREFIX, "");
|
|
6
|
+
const effortSection = {
|
|
7
|
+
none: "standard",
|
|
8
|
+
minimal: "standard",
|
|
9
|
+
low: "standard",
|
|
10
|
+
medium: "standard",
|
|
11
|
+
high: "standard",
|
|
12
|
+
xhigh: "extended",
|
|
13
|
+
max: "extended"
|
|
14
|
+
};
|
|
15
|
+
const groupReasoningEfforts = (efforts) => {
|
|
16
|
+
const standard = [];
|
|
17
|
+
const extended = [];
|
|
18
|
+
for (const effort of efforts) {
|
|
19
|
+
if (effortSection[effort] === "extended") {
|
|
20
|
+
extended.push(effort);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
standard.push(effort);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
extended,
|
|
27
|
+
standard
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
const modelSelectionLabel = ({ defaultEffortLabel, displayName, formatSelectionLabel, providerDefaultEffort, reasoningEffort, translateEffort }) => {
|
|
31
|
+
const effectiveEffort = reasoningEffort ?? providerDefaultEffort;
|
|
32
|
+
return formatSelectionLabel({
|
|
33
|
+
effort: effectiveEffort === null ? defaultEffortLabel : translateEffort(effectiveEffort),
|
|
34
|
+
model: compactModelDisplayName(displayName)
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
const toValue = ({ modelId, provider }) => modelId === null || provider === null ? "" : `${provider}:${modelId}`;
|
|
38
|
+
const parseSelection = (value, options) => {
|
|
39
|
+
if (value.length === 0) return {
|
|
40
|
+
modelId: null,
|
|
41
|
+
provider: null,
|
|
42
|
+
reasoningEffort: null
|
|
43
|
+
};
|
|
44
|
+
const option = options.find((candidate) => `${candidate.provider}:${candidate.modelId}` === value);
|
|
45
|
+
if (option === void 0) throw new ChatConfigurationError({
|
|
46
|
+
code: "unconfigured-model",
|
|
47
|
+
message: `Selected AI model is not available: ${value}.`,
|
|
48
|
+
modelId: value
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
modelId: option.modelId,
|
|
52
|
+
provider: option.provider,
|
|
53
|
+
reasoningEffort: null
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
/** Native, accessible model control. Provider configuration is validated by
|
|
57
|
+
* `resolveChatModelSelection` before a host accepts the new selection. */
|
|
58
|
+
const ChatModelSelector = ({ autoLabel, disabled = false, label, onSelectionChange, options, selection }) => {
|
|
59
|
+
const onChange = ({ currentTarget }) => {
|
|
60
|
+
onSelectionChange(parseSelection(currentTarget.value, options));
|
|
61
|
+
};
|
|
62
|
+
return /* @__PURE__ */ jsxs("select", {
|
|
63
|
+
"aria-label": label,
|
|
64
|
+
disabled,
|
|
65
|
+
onChange,
|
|
66
|
+
value: toValue(selection),
|
|
67
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
68
|
+
value: "",
|
|
69
|
+
children: autoLabel
|
|
70
|
+
}), options.map((option) => /* @__PURE__ */ jsx("option", {
|
|
71
|
+
value: `${option.provider}:${option.modelId}`,
|
|
72
|
+
children: option.displayName
|
|
73
|
+
}, `${option.provider}:${option.modelId}`))]
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
export { ChatModelSelector, compactModelDisplayName, groupReasoningEfforts, modelSelectionLabel };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { AIProvider, ReasoningEffort } from "@stll/ai-catalog";
|
|
2
|
+
//#region src/runtime.d.ts
|
|
3
|
+
type ChatModelOption = {
|
|
4
|
+
defaultReasoningEffort: ReasoningEffort | null;
|
|
5
|
+
displayName: string;
|
|
6
|
+
modelId: string;
|
|
7
|
+
provider: AIProvider;
|
|
8
|
+
reasoningEfforts: readonly ReasoningEffort[] | null;
|
|
9
|
+
};
|
|
10
|
+
type ChatModelSelection = {
|
|
11
|
+
modelId: string | null;
|
|
12
|
+
provider: AIProvider | null;
|
|
13
|
+
reasoningEffort: ReasoningEffort | null;
|
|
14
|
+
};
|
|
15
|
+
type ChatProviderConfiguration = {
|
|
16
|
+
modelIds: readonly string[];
|
|
17
|
+
provider: AIProvider;
|
|
18
|
+
};
|
|
19
|
+
declare const ChatConfigurationError_base: import("better-result").TaggedErrorClass<"ChatConfigurationError">;
|
|
20
|
+
declare class ChatConfigurationError extends ChatConfigurationError_base<{
|
|
21
|
+
code: "duplicate-model" | "empty-model-id" | "empty-provider" | "unknown-provider" | "unconfigured-model";
|
|
22
|
+
message: string;
|
|
23
|
+
modelId?: string | undefined;
|
|
24
|
+
provider?: string | undefined;
|
|
25
|
+
}> {}
|
|
26
|
+
declare const ChatStreamError_base: import("better-result").TaggedErrorClass<"ChatStreamError">;
|
|
27
|
+
declare class ChatStreamError extends ChatStreamError_base<{
|
|
28
|
+
cause: unknown;
|
|
29
|
+
message: string;
|
|
30
|
+
}> {}
|
|
31
|
+
/**
|
|
32
|
+
* Validate provider configuration at the shell boundary. A missing or unknown
|
|
33
|
+
* provider/model is an explicit typed error: the runtime never guesses a
|
|
34
|
+
* provider or silently routes a request somewhere else.
|
|
35
|
+
*/
|
|
36
|
+
declare const resolveChatModelSelection: ({ providers, selection }: {
|
|
37
|
+
providers: readonly ChatProviderConfiguration[];
|
|
38
|
+
selection: ChatModelSelection;
|
|
39
|
+
}) => ChatModelSelection;
|
|
40
|
+
type ChatMessage<Metadata = undefined> = {
|
|
41
|
+
content: string;
|
|
42
|
+
id: string;
|
|
43
|
+
metadata: Metadata;
|
|
44
|
+
role: "assistant" | "system" | "user";
|
|
45
|
+
};
|
|
46
|
+
type ChatStreamEvent<Metadata = undefined> = {
|
|
47
|
+
message: ChatMessage<Metadata>;
|
|
48
|
+
type: "message";
|
|
49
|
+
} | {
|
|
50
|
+
message: ChatMessage<Metadata>;
|
|
51
|
+
type: "replace-message";
|
|
52
|
+
};
|
|
53
|
+
type ChatTransport<Metadata = undefined> = (input: {
|
|
54
|
+
messages: readonly ChatMessage<Metadata>[];
|
|
55
|
+
signal: AbortSignal;
|
|
56
|
+
}) => AsyncIterable<ChatStreamEvent<Metadata>>;
|
|
57
|
+
type ChatRuntimeSnapshot<Metadata = undefined> = {
|
|
58
|
+
error: ChatStreamError | undefined;
|
|
59
|
+
isStreaming: boolean;
|
|
60
|
+
messages: readonly ChatMessage<Metadata>[];
|
|
61
|
+
};
|
|
62
|
+
type ChatRuntime<Metadata = undefined> = {
|
|
63
|
+
getSnapshot: () => ChatRuntimeSnapshot<Metadata>;
|
|
64
|
+
send: (message: ChatMessage<Metadata>) => Promise<void>;
|
|
65
|
+
stop: () => void;
|
|
66
|
+
subscribe: (listener: () => void) => () => void;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* A framework-neutral stream runtime. Hosts own authentication, endpoint
|
|
70
|
+
* selection, persistence, and provider credentials through `transport`; this
|
|
71
|
+
* primitive owns the observable stream lifecycle and makes failures explicit.
|
|
72
|
+
*/
|
|
73
|
+
declare const createChatRuntime: <Metadata = undefined>({ initialMessages, transport }: {
|
|
74
|
+
initialMessages?: readonly ChatMessage<Metadata>[];
|
|
75
|
+
transport: ChatTransport<Metadata>;
|
|
76
|
+
}) => ChatRuntime<Metadata>;
|
|
77
|
+
//#endregion
|
|
78
|
+
export { ChatConfigurationError, ChatMessage, ChatModelOption, ChatModelSelection, ChatProviderConfiguration, ChatRuntime, ChatRuntimeSnapshot, ChatStreamError, ChatStreamEvent, ChatTransport, createChatRuntime, resolveChatModelSelection };
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { TaggedError } from "better-result";
|
|
2
|
+
import { AI_PROVIDERS } from "@stll/ai-catalog";
|
|
3
|
+
//#region src/runtime.ts
|
|
4
|
+
var ChatConfigurationError = class extends TaggedError("ChatConfigurationError") {};
|
|
5
|
+
var ChatStreamError = class extends TaggedError("ChatStreamError") {};
|
|
6
|
+
const providerSet = new Set(AI_PROVIDERS);
|
|
7
|
+
const modelKey = (provider, modelId) => `${provider}:${modelId}`;
|
|
8
|
+
/**
|
|
9
|
+
* Validate provider configuration at the shell boundary. A missing or unknown
|
|
10
|
+
* provider/model is an explicit typed error: the runtime never guesses a
|
|
11
|
+
* provider or silently routes a request somewhere else.
|
|
12
|
+
*/
|
|
13
|
+
const resolveChatModelSelection = ({ providers, selection }) => {
|
|
14
|
+
const configuredModels = /* @__PURE__ */ new Set();
|
|
15
|
+
for (const provider of providers) {
|
|
16
|
+
if (!providerSet.has(provider.provider)) throw new ChatConfigurationError({
|
|
17
|
+
code: "unknown-provider",
|
|
18
|
+
message: `Unknown AI provider: ${provider.provider}.`,
|
|
19
|
+
provider: provider.provider
|
|
20
|
+
});
|
|
21
|
+
if (provider.modelIds.length === 0) throw new ChatConfigurationError({
|
|
22
|
+
code: "empty-provider",
|
|
23
|
+
message: `AI provider ${provider.provider} has no configured models.`,
|
|
24
|
+
provider: provider.provider
|
|
25
|
+
});
|
|
26
|
+
for (const modelId of provider.modelIds) {
|
|
27
|
+
if (modelId.trim().length === 0) throw new ChatConfigurationError({
|
|
28
|
+
code: "empty-model-id",
|
|
29
|
+
message: `AI provider ${provider.provider} has an empty model ID.`,
|
|
30
|
+
provider: provider.provider
|
|
31
|
+
});
|
|
32
|
+
const key = modelKey(provider.provider, modelId);
|
|
33
|
+
if (configuredModels.has(key)) throw new ChatConfigurationError({
|
|
34
|
+
code: "duplicate-model",
|
|
35
|
+
message: `AI model ${modelId} is configured more than once for ${provider.provider}.`,
|
|
36
|
+
modelId,
|
|
37
|
+
provider: provider.provider
|
|
38
|
+
});
|
|
39
|
+
configuredModels.add(key);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (selection.modelId === null || selection.provider === null) {
|
|
43
|
+
if (selection.modelId === null && selection.provider === null) return selection;
|
|
44
|
+
throw new ChatConfigurationError({
|
|
45
|
+
code: "unconfigured-model",
|
|
46
|
+
message: "A model selection must include both provider and model ID.",
|
|
47
|
+
modelId: selection.modelId ?? void 0,
|
|
48
|
+
provider: selection.provider ?? void 0
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (!configuredModels.has(modelKey(selection.provider, selection.modelId))) throw new ChatConfigurationError({
|
|
52
|
+
code: "unconfigured-model",
|
|
53
|
+
message: `AI model ${selection.modelId} is not configured for ${selection.provider}.`,
|
|
54
|
+
modelId: selection.modelId,
|
|
55
|
+
provider: selection.provider
|
|
56
|
+
});
|
|
57
|
+
return selection;
|
|
58
|
+
};
|
|
59
|
+
const toStreamError = (error) => error instanceof ChatStreamError ? error : new ChatStreamError({
|
|
60
|
+
cause: error,
|
|
61
|
+
message: error instanceof Error ? error.message : "Chat stream failed."
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* A framework-neutral stream runtime. Hosts own authentication, endpoint
|
|
65
|
+
* selection, persistence, and provider credentials through `transport`; this
|
|
66
|
+
* primitive owns the observable stream lifecycle and makes failures explicit.
|
|
67
|
+
*/
|
|
68
|
+
const createChatRuntime = ({ initialMessages = [], transport }) => {
|
|
69
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
70
|
+
let abortController;
|
|
71
|
+
let snapshot = {
|
|
72
|
+
error: void 0,
|
|
73
|
+
isStreaming: false,
|
|
74
|
+
messages: initialMessages
|
|
75
|
+
};
|
|
76
|
+
const emit = () => {
|
|
77
|
+
for (const listener of listeners) listener();
|
|
78
|
+
};
|
|
79
|
+
const update = (next) => {
|
|
80
|
+
snapshot = next;
|
|
81
|
+
emit();
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
getSnapshot: () => snapshot,
|
|
85
|
+
send: async (message) => {
|
|
86
|
+
if (snapshot.isStreaming) throw new ChatStreamError({
|
|
87
|
+
cause: void 0,
|
|
88
|
+
message: "A chat stream is already active."
|
|
89
|
+
});
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
abortController = controller;
|
|
92
|
+
update({
|
|
93
|
+
error: void 0,
|
|
94
|
+
isStreaming: true,
|
|
95
|
+
messages: [...snapshot.messages, message]
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
for await (const event of transport({
|
|
99
|
+
messages: snapshot.messages,
|
|
100
|
+
signal: controller.signal
|
|
101
|
+
})) {
|
|
102
|
+
if (abortController !== controller) return;
|
|
103
|
+
const messages = event.type === "message" ? [...snapshot.messages, event.message] : snapshot.messages.map((candidate) => candidate.id === event.message.id ? event.message : candidate);
|
|
104
|
+
update({
|
|
105
|
+
error: void 0,
|
|
106
|
+
isStreaming: true,
|
|
107
|
+
messages
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (abortController === controller) {
|
|
111
|
+
abortController = void 0;
|
|
112
|
+
update({
|
|
113
|
+
...snapshot,
|
|
114
|
+
isStreaming: false
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (abortController !== controller) return;
|
|
119
|
+
abortController = void 0;
|
|
120
|
+
update({
|
|
121
|
+
error: toStreamError(error),
|
|
122
|
+
isStreaming: false,
|
|
123
|
+
messages: snapshot.messages
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
stop: () => {
|
|
128
|
+
abortController?.abort();
|
|
129
|
+
abortController = void 0;
|
|
130
|
+
if (snapshot.isStreaming) update({
|
|
131
|
+
...snapshot,
|
|
132
|
+
isStreaming: false
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
subscribe: (listener) => {
|
|
136
|
+
listeners.add(listener);
|
|
137
|
+
return () => {
|
|
138
|
+
listeners.delete(listener);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
//#endregion
|
|
144
|
+
export { ChatConfigurationError, ChatStreamError, createChatRuntime, resolveChatModelSelection };
|
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ChatMessage } from "./runtime.js";
|
|
2
|
+
import { ReactElement } from "react";
|
|
3
|
+
//#region src/stream.d.ts
|
|
4
|
+
type ChatMessageStreamProps<Metadata> = {
|
|
5
|
+
className?: string | undefined;
|
|
6
|
+
empty?: ReactElement | undefined;
|
|
7
|
+
messages: readonly ChatMessage<Metadata>[];
|
|
8
|
+
renderMessage: (message: ChatMessage<Metadata>) => ReactElement;
|
|
9
|
+
};
|
|
10
|
+
/** A semantic transcript container; hosts supply their own rich markdown,
|
|
11
|
+
* citation, tool-call, and attachment renderers through `renderMessage`. */
|
|
12
|
+
declare const ChatMessageStream: <Metadata>({ className, empty, messages, renderMessage }: ChatMessageStreamProps<Metadata>) => ReactElement | null;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { ChatMessageStream, ChatMessageStreamProps };
|
package/dist/stream.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
//#region src/stream.tsx
|
|
3
|
+
/** A semantic transcript container; hosts supply their own rich markdown,
|
|
4
|
+
* citation, tool-call, and attachment renderers through `renderMessage`. */
|
|
5
|
+
const ChatMessageStream = ({ className, empty, messages, renderMessage }) => {
|
|
6
|
+
if (messages.length === 0) return empty ?? null;
|
|
7
|
+
return /* @__PURE__ */ jsx("ol", {
|
|
8
|
+
className,
|
|
9
|
+
children: messages.map((message) => /* @__PURE__ */ jsx("li", { children: renderMessage(message) }, message.id))
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { ChatMessageStream };
|
package/package.json
CHANGED
|
@@ -1,6 +1,87 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/chat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Typed React chat composer, message stream, model selector, and transport runtime primitives.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"chat",
|
|
8
|
+
"react",
|
|
9
|
+
"streaming"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/stella/stella/tree/main/packages/chat",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/stella/stella/issues"
|
|
14
|
+
},
|
|
15
|
+
"license": "Apache-2.0",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/stella/stella.git",
|
|
19
|
+
"directory": "packages/chat"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./composer": {
|
|
33
|
+
"types": "./dist/composer.d.ts",
|
|
34
|
+
"import": "./dist/composer.js"
|
|
35
|
+
},
|
|
36
|
+
"./model-selector": {
|
|
37
|
+
"types": "./dist/model-selector.d.ts",
|
|
38
|
+
"import": "./dist/model-selector.js"
|
|
39
|
+
},
|
|
40
|
+
"./runtime": {
|
|
41
|
+
"types": "./dist/runtime.d.ts",
|
|
42
|
+
"import": "./dist/runtime.js"
|
|
43
|
+
},
|
|
44
|
+
"./stream": {
|
|
45
|
+
"types": "./dist/stream.d.ts",
|
|
46
|
+
"import": "./dist/stream.js"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"clean": "git clean -xdf dist .cache .turbo node_modules",
|
|
54
|
+
"build": "tsdown",
|
|
55
|
+
"pack:dry-run": "bun pm pack --dry-run",
|
|
56
|
+
"test": "bun test src",
|
|
57
|
+
"typecheck": "bun ../../packages/scripts/src/tsc-native.ts --noEmit",
|
|
58
|
+
"lint": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --report-unused-disable-directives-severity=error --deny-warnings --type-aware packages/chat",
|
|
59
|
+
"lint:fix": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --type-aware --fix packages/chat",
|
|
60
|
+
"format": "oxfmt .",
|
|
61
|
+
"prepack": "bun run build"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@stll/ai-catalog": "^0.1.4",
|
|
65
|
+
"@stll/ui": "^0.13.0",
|
|
66
|
+
"better-result": "3.0.0",
|
|
67
|
+
"lucide-react": "1.30.0"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@stll/typescript-config": "0.0.0",
|
|
71
|
+
"@types/react": "^19.2.17",
|
|
72
|
+
"@types/react-dom": "^19.2.3",
|
|
73
|
+
"bun-types": "1.4.0",
|
|
74
|
+
"react": "^19.2.8",
|
|
75
|
+
"react-dom": "^19.2.8",
|
|
76
|
+
"tailwindcss": "4.3.3",
|
|
77
|
+
"tsdown": "0.22.14"
|
|
78
|
+
},
|
|
79
|
+
"peerDependencies": {
|
|
80
|
+
"@base-ui/react": "^1.7.0",
|
|
81
|
+
"react": ">=19",
|
|
82
|
+
"react-dom": ">=19",
|
|
83
|
+
"tailwindcss": "^4.3.0"
|
|
84
|
+
},
|
|
85
|
+
"main": "./dist/index.js",
|
|
86
|
+
"types": "./dist/index.d.ts"
|
|
6
87
|
}
|