@fromsko/chat-ui 0.0.0-stage → 0.1.6
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/LICENSE +21 -0
- package/README.md +100 -2
- package/lib/adapters/event-bus.d.ts +80 -0
- package/lib/adapters/event-bus.js +73 -0
- package/lib/adapters/index.d.ts +3 -0
- package/lib/adapters/index.js +3 -0
- package/lib/adapters/labels.d.ts +6 -0
- package/lib/adapters/labels.js +17 -0
- package/lib/adapters/stream-bridge.d.ts +9 -0
- package/lib/adapters/stream-bridge.js +7 -0
- package/lib/blocks/cards/AskCard.d.ts +8 -0
- package/lib/blocks/cards/AskCard.js +97 -0
- package/lib/blocks/cards/CompactionCard.d.ts +5 -0
- package/lib/blocks/cards/CompactionCard.js +5 -0
- package/lib/blocks/cards/ContextUsage.d.ts +14 -0
- package/lib/blocks/cards/ContextUsage.js +136 -0
- package/lib/blocks/cards/DisplayCard.d.ts +7 -0
- package/lib/blocks/cards/DisplayCard.js +27 -0
- package/lib/blocks/cards/MemoryPill.d.ts +5 -0
- package/lib/blocks/cards/MemoryPill.js +5 -0
- package/lib/blocks/cards/OutputCard.d.ts +5 -0
- package/lib/blocks/cards/OutputCard.js +13 -0
- package/lib/blocks/cards/PermissionCard.d.ts +21 -0
- package/lib/blocks/cards/PermissionCard.js +26 -0
- package/lib/blocks/cards/SpringCheck.d.ts +9 -0
- package/lib/blocks/cards/SpringCheck.js +17 -0
- package/lib/blocks/cards/TodoCard.d.ts +19 -0
- package/lib/blocks/cards/TodoCard.js +28 -0
- package/lib/blocks/cards/index.d.ts +10 -0
- package/lib/blocks/cards/index.js +10 -0
- package/lib/blocks/composer/Composer.d.ts +66 -0
- package/lib/blocks/composer/Composer.js +73 -0
- package/lib/blocks/composer/ComposerAttachments.d.ts +17 -0
- package/lib/blocks/composer/ComposerAttachments.js +10 -0
- package/lib/blocks/composer/ComposerGrip.d.ts +1 -0
- package/lib/blocks/composer/ComposerGrip.js +39 -0
- package/lib/blocks/composer/ComposerInput.d.ts +18 -0
- package/lib/blocks/composer/ComposerInput.js +60 -0
- package/lib/blocks/composer/ComposerMenus.d.ts +10 -0
- package/lib/blocks/composer/ComposerMenus.js +78 -0
- package/lib/blocks/composer/ComposerToolbar.d.ts +27 -0
- package/lib/blocks/composer/ComposerToolbar.js +16 -0
- package/lib/blocks/composer/MentionPicker.d.ts +13 -0
- package/lib/blocks/composer/MentionPicker.js +71 -0
- package/lib/blocks/composer/PromptPop.d.ts +7 -0
- package/lib/blocks/composer/PromptPop.js +40 -0
- package/lib/blocks/composer/QueuedPrompts.d.ts +14 -0
- package/lib/blocks/composer/QueuedPrompts.js +21 -0
- package/lib/blocks/composer/SlingButton.d.ts +10 -0
- package/lib/blocks/composer/SlingButton.js +112 -0
- package/lib/blocks/composer/index.d.ts +7 -0
- package/lib/blocks/composer/index.js +1 -0
- package/lib/blocks/conversation/ChatEmptyScene.d.ts +4 -0
- package/lib/blocks/conversation/ChatEmptyScene.js +21 -0
- package/lib/blocks/conversation/ChatHeader.d.ts +13 -0
- package/lib/blocks/conversation/ChatHeader.js +20 -0
- package/lib/blocks/conversation/ChatPane.d.ts +90 -0
- package/lib/blocks/conversation/ChatPane.js +60 -0
- package/lib/blocks/conversation/Conversation.d.ts +11 -0
- package/lib/blocks/conversation/Conversation.js +41 -0
- package/lib/blocks/conversation/LineSidebar.d.ts +14 -0
- package/lib/blocks/conversation/LineSidebar.js +42 -0
- package/lib/blocks/conversation/index.d.ts +5 -0
- package/lib/blocks/conversation/index.js +5 -0
- package/lib/blocks/index.d.ts +9 -0
- package/lib/blocks/index.js +9 -0
- package/lib/blocks/message/DayDivider.d.ts +10 -0
- package/lib/blocks/message/DayDivider.js +29 -0
- package/lib/blocks/message/DiffView.d.ts +5 -0
- package/lib/blocks/message/DiffView.js +18 -0
- package/lib/blocks/message/MermaidDiagram.d.ts +4 -0
- package/lib/blocks/message/MermaidDiagram.js +43 -0
- package/lib/blocks/message/Message.d.ts +17 -0
- package/lib/blocks/message/Message.js +45 -0
- package/lib/blocks/message/MessageActionBar.d.ts +18 -0
- package/lib/blocks/message/MessageActionBar.js +15 -0
- package/lib/blocks/message/MessageArtifacts.d.ts +13 -0
- package/lib/blocks/message/MessageArtifacts.js +9 -0
- package/lib/blocks/message/MessageAttachments.d.ts +5 -0
- package/lib/blocks/message/MessageAttachments.js +28 -0
- package/lib/blocks/message/MessageBubble.d.ts +11 -0
- package/lib/blocks/message/MessageBubble.js +31 -0
- package/lib/blocks/message/MessageRow.d.ts +20 -0
- package/lib/blocks/message/MessageRow.js +49 -0
- package/lib/blocks/message/MessageStream.d.ts +23 -0
- package/lib/blocks/message/MessageStream.js +80 -0
- package/lib/blocks/message/MessageTrace.d.ts +16 -0
- package/lib/blocks/message/MessageTrace.js +39 -0
- package/lib/blocks/message/PartView.d.ts +10 -0
- package/lib/blocks/message/PartView.js +29 -0
- package/lib/blocks/message/code-text.d.ts +15 -0
- package/lib/blocks/message/code-text.js +17 -0
- package/lib/blocks/message/index.d.ts +12 -0
- package/lib/blocks/message/index.js +12 -0
- package/lib/blocks/reasoning/Reasoning.d.ts +7 -0
- package/lib/blocks/reasoning/Reasoning.js +26 -0
- package/lib/blocks/reasoning/ReasoningBlock.d.ts +6 -0
- package/lib/blocks/reasoning/ReasoningBlock.js +5 -0
- package/lib/blocks/reasoning/index.d.ts +2 -0
- package/lib/blocks/reasoning/index.js +2 -0
- package/lib/blocks/selection/SelectionActions.d.ts +12 -0
- package/lib/blocks/selection/SelectionActions.js +29 -0
- package/lib/blocks/selection/SelectionBar.d.ts +10 -0
- package/lib/blocks/selection/SelectionBar.js +6 -0
- package/lib/blocks/selection/index.d.ts +2 -0
- package/lib/blocks/selection/index.js +2 -0
- package/lib/blocks/settings/DisplaySettingsPanel.d.ts +5 -0
- package/lib/blocks/settings/DisplaySettingsPanel.js +33 -0
- package/lib/blocks/settings/SquishSwitch.d.ts +8 -0
- package/lib/blocks/settings/SquishSwitch.js +21 -0
- package/lib/blocks/settings/index.d.ts +2 -0
- package/lib/blocks/settings/index.js +2 -0
- package/lib/blocks/status/LatticeLoader.d.ts +12 -0
- package/lib/blocks/status/LatticeLoader.js +22 -0
- package/lib/blocks/status/TypingIndicator.d.ts +5 -0
- package/lib/blocks/status/TypingIndicator.js +5 -0
- package/lib/blocks/status/index.d.ts +2 -0
- package/lib/blocks/status/index.js +2 -0
- package/lib/blocks/tool/SandboxCard.d.ts +5 -0
- package/lib/blocks/tool/SandboxCard.js +10 -0
- package/lib/blocks/tool/ToolCard.d.ts +10 -0
- package/lib/blocks/tool/ToolCard.js +32 -0
- package/lib/blocks/tool/ToolGroup.d.ts +6 -0
- package/lib/blocks/tool/ToolGroup.js +23 -0
- package/lib/blocks/tool/exec-envelope.d.ts +7 -0
- package/lib/blocks/tool/exec-envelope.js +43 -0
- package/lib/blocks/tool/index.d.ts +4 -0
- package/lib/blocks/tool/index.js +4 -0
- package/lib/domain/ask.d.ts +14 -0
- package/lib/domain/ask.js +50 -0
- package/lib/domain/attachments.d.ts +16 -0
- package/lib/domain/attachments.js +126 -0
- package/lib/domain/attribution.d.ts +4 -0
- package/lib/domain/attribution.js +11 -0
- package/lib/domain/composer-palette.d.ts +15 -0
- package/lib/domain/composer-palette.js +131 -0
- package/lib/domain/index.d.ts +12 -0
- package/lib/domain/index.js +12 -0
- package/lib/domain/mention.d.ts +17 -0
- package/lib/domain/mention.js +95 -0
- package/lib/domain/message-blocks.d.ts +29 -0
- package/lib/domain/message-blocks.js +63 -0
- package/lib/domain/message-menu.d.ts +4 -0
- package/lib/domain/message-menu.js +46 -0
- package/lib/domain/queue.d.ts +23 -0
- package/lib/domain/queue.js +52 -0
- package/lib/domain/reducer.d.ts +137 -0
- package/lib/domain/reducer.js +442 -0
- package/lib/domain/selection-actions.d.ts +13 -0
- package/lib/domain/selection-actions.js +56 -0
- package/lib/domain/selection-gates.d.ts +6 -0
- package/lib/domain/selection-gates.js +36 -0
- package/lib/domain/slash-commands.d.ts +16 -0
- package/lib/domain/slash-commands.js +142 -0
- package/lib/hooks/index.d.ts +8 -0
- package/lib/hooks/index.js +8 -0
- package/lib/hooks/useComposerPalette.d.ts +35 -0
- package/lib/hooks/useComposerPalette.js +120 -0
- package/lib/hooks/useComposerSubmit.d.ts +31 -0
- package/lib/hooks/useComposerSubmit.js +135 -0
- package/lib/hooks/useInputHistory.d.ts +14 -0
- package/lib/hooks/useInputHistory.js +52 -0
- package/lib/hooks/usePendingAsk.d.ts +11 -0
- package/lib/hooks/usePendingAsk.js +33 -0
- package/lib/hooks/usePendingPermissions.d.ts +10 -0
- package/lib/hooks/usePendingPermissions.js +26 -0
- package/lib/hooks/useQueuedPrompts.d.ts +12 -0
- package/lib/hooks/useQueuedPrompts.js +91 -0
- package/lib/hooks/useSelectionPop.d.ts +13 -0
- package/lib/hooks/useSelectionPop.js +130 -0
- package/lib/hooks/useStreamBridge.d.ts +1 -0
- package/lib/hooks/useStreamBridge.js +23 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +10 -0
- package/lib/primitives/Badge.d.ts +7 -0
- package/lib/primitives/Badge.js +13 -0
- package/lib/primitives/Button.d.ts +9 -0
- package/lib/primitives/Button.js +21 -0
- package/lib/primitives/Collapsible.d.ts +12 -0
- package/lib/primitives/Collapsible.js +14 -0
- package/lib/primitives/Dialog.d.ts +10 -0
- package/lib/primitives/Dialog.js +19 -0
- package/lib/primitives/DropdownMenu.d.ts +28 -0
- package/lib/primitives/DropdownMenu.js +37 -0
- package/lib/primitives/Empty.d.ts +8 -0
- package/lib/primitives/Empty.js +6 -0
- package/lib/primitives/Input.d.ts +3 -0
- package/lib/primitives/Input.js +6 -0
- package/lib/primitives/Kbd.d.ts +2 -0
- package/lib/primitives/Kbd.js +5 -0
- package/lib/primitives/Popover.d.ts +15 -0
- package/lib/primitives/Popover.js +32 -0
- package/lib/primitives/ScrollArea.d.ts +5 -0
- package/lib/primitives/ScrollArea.js +5 -0
- package/lib/primitives/Separator.d.ts +5 -0
- package/lib/primitives/Separator.js +5 -0
- package/lib/primitives/Spinner.d.ts +5 -0
- package/lib/primitives/Spinner.js +5 -0
- package/lib/primitives/Textarea.d.ts +3 -0
- package/lib/primitives/Textarea.js +6 -0
- package/lib/primitives/Tooltip.d.ts +8 -0
- package/lib/primitives/Tooltip.js +7 -0
- package/lib/primitives/index.d.ts +14 -0
- package/lib/primitives/index.js +14 -0
- package/lib/reactbits/Motion.d.ts +24 -0
- package/lib/reactbits/Motion.js +52 -0
- package/lib/reactbits/index.d.ts +1 -0
- package/lib/reactbits/index.js +1 -0
- package/lib/stores/chat-store.d.ts +33 -0
- package/lib/stores/chat-store.js +93 -0
- package/lib/stores/display-store.d.ts +15 -0
- package/lib/stores/display-store.js +116 -0
- package/lib/stores/index.d.ts +3 -0
- package/lib/stores/index.js +3 -0
- package/lib/stores/theme-store.d.ts +8 -0
- package/lib/stores/theme-store.js +29 -0
- package/lib/tokens/index.d.ts +21 -0
- package/lib/tokens/index.js +66 -0
- package/lib/types/agent.d.ts +62 -0
- package/lib/types/agent.js +1 -0
- package/lib/types/callbacks.d.ts +55 -0
- package/lib/types/callbacks.js +1 -0
- package/lib/types/composer.d.ts +116 -0
- package/lib/types/composer.js +1 -0
- package/lib/types/conversation.d.ts +54 -0
- package/lib/types/conversation.js +1 -0
- package/lib/types/display.d.ts +25 -0
- package/lib/types/display.js +40 -0
- package/lib/types/index.d.ts +8 -0
- package/lib/types/index.js +8 -0
- package/lib/types/message.d.ts +100 -0
- package/lib/types/message.js +1 -0
- package/lib/types/selection.d.ts +60 -0
- package/lib/types/selection.js +1 -0
- package/lib/types/theme.d.ts +100 -0
- package/lib/types/theme.js +1 -0
- package/lib/utils/clipboard.d.ts +4 -0
- package/lib/utils/clipboard.js +41 -0
- package/lib/utils/cn.d.ts +2 -0
- package/lib/utils/cn.js +14 -0
- package/lib/utils/index.d.ts +3 -0
- package/lib/utils/index.js +3 -0
- package/lib/utils/storage.d.ts +11 -0
- package/lib/utils/storage.js +48 -0
- package/package.json +93 -4
- package/src/styles/blocks.css +2439 -0
- package/src/styles/index.css +3 -0
- package/src/tokens/theme.css +464 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 chat-ui contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,101 @@
|
|
|
1
|
-
#
|
|
1
|
+
# chat-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React components for AI chat surfaces: a message stream, composer, tool-call cards, ask/permission
|
|
4
|
+
prompts, selection actions and a token-driven theme layer. Composable blocks, no backend, no routing,
|
|
5
|
+
no data fetching — you own the state and the transport.
|
|
6
|
+
|
|
7
|
+
- **Composable blocks** — `ChatPane` for the whole surface, or assemble message / composer / card
|
|
8
|
+
blocks yourself.
|
|
9
|
+
- **Token-driven theming** — CSS variables plus light / dark / glass presets, overridable per host.
|
|
10
|
+
- **Streaming-ready** — message parts, reasoning blocks, tool envelopes and queued prompts render
|
|
11
|
+
incrementally.
|
|
12
|
+
- **Typed end to end** — strict TypeScript, exported types for every prop and adapter contract.
|
|
13
|
+
- **Playwright screenshot check** — CI renders the demo and captures full-page screenshots.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @fromsko/chat-ui
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Peer dependencies: `react@^19`, `react-dom@^19`. `gsap` is optional (only used by a few
|
|
22
|
+
decorative blocks). Tailwind CSS v4 is expected in the host app for the shipped stylesheet.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { ChatPane, applyTheme } from "@fromsko/chat-ui";
|
|
28
|
+
import "@fromsko/chat-ui/styles.css";
|
|
29
|
+
import { useEffect, useState } from "react";
|
|
30
|
+
import type { Agent, ChatMessage, Conversation } from "@fromsko/chat-ui";
|
|
31
|
+
|
|
32
|
+
export function Chat() {
|
|
33
|
+
const [draft, setDraft] = useState("");
|
|
34
|
+
const [messages] = useState<ChatMessage[]>([]);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
applyTheme("dark");
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<ChatPane
|
|
42
|
+
conversation={{} as Conversation}
|
|
43
|
+
members={[] as Agent[]}
|
|
44
|
+
messages={messages}
|
|
45
|
+
status="idle"
|
|
46
|
+
draft={draft}
|
|
47
|
+
onDraftChange={setDraft}
|
|
48
|
+
onSend={(text) => {
|
|
49
|
+
setDraft("");
|
|
50
|
+
void text;
|
|
51
|
+
}}
|
|
52
|
+
onOpenDetail={() => {}}
|
|
53
|
+
onBack={() => {}}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Hooks cover the parts that are tedious to re-implement: `useComposerSubmit`, `useComposerPalette`,
|
|
60
|
+
`useQueuedPrompts`, `useInputHistory`, `useSelectionPop`, `usePendingAsk`, `usePendingPermissions`.
|
|
61
|
+
|
|
62
|
+
## Theming
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { applyTheme, readTheme, tokens } from "@fromsko/chat-ui";
|
|
66
|
+
|
|
67
|
+
applyTheme("dark");
|
|
68
|
+
applyTheme("light");
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
All colors, radii, spacing and motion values come from CSS variables under
|
|
72
|
+
`src/tokens/theme.css`. Override them on `:root` or a subtree to re-skin the blocks without touching
|
|
73
|
+
component code.
|
|
74
|
+
|
|
75
|
+
## Entry points
|
|
76
|
+
|
|
77
|
+
| Specifier | Contents |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `@fromsko/chat-ui` | All components, hooks, stores, adapters and types |
|
|
80
|
+
| `@fromsko/chat-ui/types` | Type-only surface |
|
|
81
|
+
| `@fromsko/chat-ui/tokens` | Design tokens and theme helpers |
|
|
82
|
+
| `@fromsko/chat-ui/styles.css` | Stylesheet (Tailwind CSS v4 source) |
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
bun install
|
|
88
|
+
bun run dev # demo app on the Rsbuild dev server
|
|
89
|
+
bun run typecheck # tsc --noEmit
|
|
90
|
+
bun run lint # rslint
|
|
91
|
+
bun run build # demo site -> dist/
|
|
92
|
+
bun run build:lib # package build -> lib/ (JS + .d.ts)
|
|
93
|
+
bun run check # typecheck + lint + build
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The demo under `src/demo` is the reference host: it wires the blocks to in-memory state and is what
|
|
97
|
+
the screenshot check renders.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ChatStore } from "../stores/chat-store";
|
|
2
|
+
export interface StreamEventMap {
|
|
3
|
+
"stream:delta": {
|
|
4
|
+
requestId: string;
|
|
5
|
+
text: string;
|
|
6
|
+
};
|
|
7
|
+
"stream:reasoning": {
|
|
8
|
+
requestId: string;
|
|
9
|
+
text: string;
|
|
10
|
+
};
|
|
11
|
+
"stream:tool-call": {
|
|
12
|
+
requestId: string;
|
|
13
|
+
toolCallId: string;
|
|
14
|
+
name: string;
|
|
15
|
+
args: unknown;
|
|
16
|
+
};
|
|
17
|
+
"stream:tool-result": {
|
|
18
|
+
requestId: string;
|
|
19
|
+
toolCallId: string;
|
|
20
|
+
result: string;
|
|
21
|
+
isError: boolean;
|
|
22
|
+
};
|
|
23
|
+
"stream:usage": {
|
|
24
|
+
requestId: string;
|
|
25
|
+
inputTokens: number;
|
|
26
|
+
outputTokens: number;
|
|
27
|
+
window: number;
|
|
28
|
+
};
|
|
29
|
+
"stream:memory": {
|
|
30
|
+
requestId: string;
|
|
31
|
+
count: number;
|
|
32
|
+
excerpt: string;
|
|
33
|
+
};
|
|
34
|
+
"stream:compaction": {
|
|
35
|
+
requestId: string | null;
|
|
36
|
+
beforeTokens: number;
|
|
37
|
+
afterTokens: number;
|
|
38
|
+
reason: string;
|
|
39
|
+
};
|
|
40
|
+
"stream:end": {
|
|
41
|
+
requestId: string;
|
|
42
|
+
outcome: "done" | "error";
|
|
43
|
+
error?: string;
|
|
44
|
+
};
|
|
45
|
+
"stream:bind": {
|
|
46
|
+
requestId: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface DisplayEventMap {
|
|
50
|
+
"display:render": {
|
|
51
|
+
requestId: string;
|
|
52
|
+
view: string;
|
|
53
|
+
title?: string;
|
|
54
|
+
payload?: unknown;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export interface OutputEventMap {
|
|
58
|
+
"output:emit": {
|
|
59
|
+
requestId: string;
|
|
60
|
+
outputKind: "text" | "file" | "link" | "json";
|
|
61
|
+
label?: string;
|
|
62
|
+
value: string;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export interface ChatEventMap extends StreamEventMap, DisplayEventMap, OutputEventMap {
|
|
66
|
+
}
|
|
67
|
+
export type ChatEventType = keyof ChatEventMap;
|
|
68
|
+
export type ChatEventHandler<K extends ChatEventType> = (payload: ChatEventMap[K]) => void;
|
|
69
|
+
export declare class ChatEventBus {
|
|
70
|
+
private handlers;
|
|
71
|
+
on<K extends ChatEventType>(type: K, handler: ChatEventHandler<K>): () => void;
|
|
72
|
+
off<K extends ChatEventType>(type: K, handler: ChatEventHandler<K>): void;
|
|
73
|
+
emit<K extends ChatEventType>(type: K, payload: ChatEventMap[K]): void;
|
|
74
|
+
clear(): void;
|
|
75
|
+
}
|
|
76
|
+
export declare const chatEventBus: ChatEventBus;
|
|
77
|
+
export declare function bindBusToStore(bus: ChatEventBus, store: {
|
|
78
|
+
getState: () => ChatStore;
|
|
79
|
+
}): () => void;
|
|
80
|
+
export declare function bridgeCallbacksToBus(bus: ChatEventBus): import("../types/callbacks").StreamEventCallbacks;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export class ChatEventBus {
|
|
2
|
+
handlers = new Map();
|
|
3
|
+
on(type, handler) {
|
|
4
|
+
let set = this.handlers.get(type);
|
|
5
|
+
if (!set) {
|
|
6
|
+
set = new Set();
|
|
7
|
+
this.handlers.set(type, set);
|
|
8
|
+
}
|
|
9
|
+
set.add(handler);
|
|
10
|
+
return () => this.off(type, handler);
|
|
11
|
+
}
|
|
12
|
+
off(type, handler) {
|
|
13
|
+
this.handlers.get(type)?.delete(handler);
|
|
14
|
+
}
|
|
15
|
+
emit(type, payload) {
|
|
16
|
+
const set = this.handlers.get(type);
|
|
17
|
+
if (!set)
|
|
18
|
+
return;
|
|
19
|
+
for (const handler of [...set]) {
|
|
20
|
+
handler(payload);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
clear() {
|
|
24
|
+
this.handlers.clear();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export const chatEventBus = new ChatEventBus();
|
|
28
|
+
export function bindBusToStore(bus, store) {
|
|
29
|
+
const s = () => store.getState();
|
|
30
|
+
const offs = [
|
|
31
|
+
bus.on("stream:bind", ({ requestId }) => s().bindRequest(requestId)),
|
|
32
|
+
bus.on("stream:delta", ({ requestId, text }) => s().streamDelta(requestId, text)),
|
|
33
|
+
bus.on("stream:reasoning", ({ requestId, text }) => s().streamReasoning(requestId, text)),
|
|
34
|
+
bus.on("stream:tool-call", ({ requestId, toolCallId, name, args }) => s().streamToolCall(requestId, toolCallId, name, args)),
|
|
35
|
+
bus.on("stream:tool-result", ({ requestId, toolCallId, result, isError }) => s().streamToolResult(requestId, toolCallId, result, isError)),
|
|
36
|
+
bus.on("stream:usage", ({ requestId, inputTokens, outputTokens, window }) => s().streamContextUsage(requestId, inputTokens, outputTokens, window)),
|
|
37
|
+
bus.on("stream:memory", ({ requestId, count, excerpt }) => s().streamMemory(requestId, count, excerpt)),
|
|
38
|
+
bus.on("stream:compaction", ({ requestId, beforeTokens, afterTokens, reason }) => s().streamCompaction(requestId, beforeTokens, afterTokens, reason)),
|
|
39
|
+
bus.on("stream:end", ({ requestId, outcome, error }) => s().streamEnd(requestId, outcome, error)),
|
|
40
|
+
bus.on("display:render", ({ requestId, view, title, payload }) => s().streamDisplay(requestId, view, title, payload)),
|
|
41
|
+
bus.on("output:emit", ({ requestId, outputKind, label, value }) => s().streamOutput(requestId, outputKind, label, value)),
|
|
42
|
+
];
|
|
43
|
+
return () => offs.forEach((off) => off());
|
|
44
|
+
}
|
|
45
|
+
export function bridgeCallbacksToBus(bus) {
|
|
46
|
+
return {
|
|
47
|
+
onDelta: (requestId, text) => bus.emit("stream:delta", { requestId, text }),
|
|
48
|
+
onReasoning: (requestId, text) => bus.emit("stream:reasoning", { requestId, text }),
|
|
49
|
+
onToolCall: (requestId, toolCallId, name, args) => bus.emit("stream:tool-call", { requestId, toolCallId, name, args }),
|
|
50
|
+
onToolResult: (requestId, toolCallId, result, isError) => bus.emit("stream:tool-result", { requestId, toolCallId, result, isError }),
|
|
51
|
+
onContextUsage: (requestId, inputTokens, outputTokens, window) => bus.emit("stream:usage", {
|
|
52
|
+
requestId,
|
|
53
|
+
inputTokens,
|
|
54
|
+
outputTokens,
|
|
55
|
+
window,
|
|
56
|
+
}),
|
|
57
|
+
onMemory: (requestId, count, excerpt) => bus.emit("stream:memory", { requestId, count, excerpt }),
|
|
58
|
+
onCompaction: (requestId, beforeTokens, afterTokens, reason) => bus.emit("stream:compaction", {
|
|
59
|
+
requestId,
|
|
60
|
+
beforeTokens,
|
|
61
|
+
afterTokens,
|
|
62
|
+
reason,
|
|
63
|
+
}),
|
|
64
|
+
onEnd: (requestId, outcome, error) => bus.emit("stream:end", { requestId, outcome, error }),
|
|
65
|
+
onDisplay: (requestId, view, title, payload) => bus.emit("display:render", { requestId, view, title, payload }),
|
|
66
|
+
onOutput: (requestId, outputKind, label, value) => bus.emit("output:emit", {
|
|
67
|
+
requestId,
|
|
68
|
+
outputKind,
|
|
69
|
+
label,
|
|
70
|
+
value: value ?? "",
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { setToolLabel, toolLabel, setClockLabel, clockLabel, type ToolLabelFn, type ClockLabelFn, } from "./labels";
|
|
2
|
+
export { setStreamBridge, getStreamBridge, type StreamBridge, } from "./stream-bridge";
|
|
3
|
+
export { ChatEventBus, chatEventBus, bindBusToStore, bridgeCallbacksToBus, type ChatEventMap, type ChatEventType, type ChatEventHandler, type StreamEventMap, type DisplayEventMap, type OutputEventMap, } from "./event-bus";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type ToolLabelFn = (tool: string) => string;
|
|
2
|
+
export declare function setToolLabel(fn: ToolLabelFn): void;
|
|
3
|
+
export declare function toolLabel(tool: string): string;
|
|
4
|
+
export type ClockLabelFn = (isoOrNow?: string) => string;
|
|
5
|
+
export declare function setClockLabel(fn: ClockLabelFn): void;
|
|
6
|
+
export declare function clockLabel(isoOrNow?: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
let toolLabelImpl = (tool) => tool;
|
|
2
|
+
export function setToolLabel(fn) {
|
|
3
|
+
toolLabelImpl = fn;
|
|
4
|
+
}
|
|
5
|
+
export function toolLabel(tool) {
|
|
6
|
+
return toolLabelImpl(tool);
|
|
7
|
+
}
|
|
8
|
+
let clockLabelImpl = () => {
|
|
9
|
+
const d = new Date();
|
|
10
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
11
|
+
};
|
|
12
|
+
export function setClockLabel(fn) {
|
|
13
|
+
clockLabelImpl = fn;
|
|
14
|
+
}
|
|
15
|
+
export function clockLabel(isoOrNow) {
|
|
16
|
+
return clockLabelImpl(isoOrNow);
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StreamEventCallbacks } from "../types/callbacks";
|
|
2
|
+
export interface StreamBridge {
|
|
3
|
+
subscribe(callbacks: StreamEventCallbacks): () => void;
|
|
4
|
+
cancel?(requestId: string): Promise<void> | void;
|
|
5
|
+
respondAsk?(turnId: string, callId: string, answer: string): Promise<void> | void;
|
|
6
|
+
respondPermission?(permissionId: string, approved: boolean, remembered: boolean): Promise<void> | void;
|
|
7
|
+
}
|
|
8
|
+
export declare function setStreamBridge(next: StreamBridge | null): void;
|
|
9
|
+
export declare function getStreamBridge(): StreamBridge | null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PendingAsk } from "../../types/conversation";
|
|
2
|
+
export interface AskCardProps {
|
|
3
|
+
ask: PendingAsk;
|
|
4
|
+
queued: number;
|
|
5
|
+
onAnswer: (text: string) => void;
|
|
6
|
+
onSkip?: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function AskCard({ ask, queued, onAnswer, onSkip }: AskCardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { ArrowRight, Check, CircleHelp, Sparkles, Square, SquareCheck, X } from "lucide-react";
|
|
4
|
+
import { MAX_ASK_QUESTIONS, answeredCount, batchAnswersOf, canDefaultAll, defaultAnswersOf, isBatchAsk, } from "../../domain/ask";
|
|
5
|
+
import { cn } from "../../utils/cn";
|
|
6
|
+
function toQuestions(ask) {
|
|
7
|
+
if (isBatchAsk(ask))
|
|
8
|
+
return ask.questions.slice(0, MAX_ASK_QUESTIONS);
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
id: ask.callId,
|
|
12
|
+
prompt: ask.prompt,
|
|
13
|
+
choices: ask.choices,
|
|
14
|
+
default: ask.default,
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
export function AskCard({ ask, queued, onAnswer, onSkip }) {
|
|
19
|
+
const questions = useMemo(() => toQuestions(ask), [ask]);
|
|
20
|
+
const batch = isBatchAsk(ask);
|
|
21
|
+
const [answers, setAnswers] = useState({});
|
|
22
|
+
const [freeOpen, setFreeOpen] = useState({});
|
|
23
|
+
const answered = answeredCount(questions, answers);
|
|
24
|
+
const total = questions.length;
|
|
25
|
+
const canDefault = canDefaultAll(questions);
|
|
26
|
+
const hiddenOverflow = batch ? ask.questions.length - questions.length : 0;
|
|
27
|
+
const selectedOf = (q) => {
|
|
28
|
+
const raw = answers[q.id] ?? "";
|
|
29
|
+
if (!q.multiSelect)
|
|
30
|
+
return raw ? [raw] : [];
|
|
31
|
+
return raw ? raw.split(", ").filter(Boolean) : [];
|
|
32
|
+
};
|
|
33
|
+
const pick = (q, choice) => {
|
|
34
|
+
if (q.multiSelect) {
|
|
35
|
+
const cur = selectedOf(q);
|
|
36
|
+
const next = cur.includes(choice)
|
|
37
|
+
? cur.filter((c) => c !== choice)
|
|
38
|
+
: [...cur, choice];
|
|
39
|
+
setAnswers((prev) => ({ ...prev, [q.id]: next.join(", ") }));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
setAnswers((prev) => ({ ...prev, [q.id]: choice }));
|
|
43
|
+
if (!batch)
|
|
44
|
+
onAnswer(choice);
|
|
45
|
+
};
|
|
46
|
+
const useAllDefaults = () => {
|
|
47
|
+
const defaults = defaultAnswersOf(questions);
|
|
48
|
+
if (!batch) {
|
|
49
|
+
const only = questions[0];
|
|
50
|
+
const v = defaults[only.id];
|
|
51
|
+
if (v)
|
|
52
|
+
onAnswer(v);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
onAnswer(batchAnswersOf(questions, defaults));
|
|
56
|
+
};
|
|
57
|
+
return (_jsxs("div", { className: "ac-ask-sheet", "data-cmp": "ac/ask-card", "data-id": "ask-sheet", "data-state": "waiting", "data-mode": batch ? "batch" : "single", "aria-label": "Agent \u63D0\u95EE", children: [_jsxs("div", { className: "ac-ask-sheet__head", children: [_jsx(CircleHelp, { className: "ac-ask-sheet__head-icon" }), _jsx("span", { className: "ac-ask-sheet__who", children: ask.agent ? `${ask.agent} 想确认` : "Agent 想确认" }), total > 1 ? (_jsxs("span", { className: "ac-ask-sheet__progress", "data-cmp": "ac/ask-progress", children: [answered, "/", total] })) : null, queued > 0 ? (_jsxs("span", { className: "ac-ask-sheet__queued", children: ["+", queued, " \u5F85\u7B54"] })) : null, onSkip ? (_jsxs("button", { type: "button", "data-cmp": "ac/ask-skip", className: "ac-ask-sheet__skip", title: "\u8DF3\u8FC7\uFF08Agent \u5C06\u81EA\u884C\u51B3\u65AD\u5E76\u7EE7\u7EED\uFF09", onClick: onSkip, children: [_jsx(X, { className: "size-3" }), "\u8DF3\u8FC7"] })) : null] }), _jsxs("div", { className: "ac-ask-sheet__body", children: [questions.map((q, i) => {
|
|
58
|
+
const chosen = answers[q.id] ?? "";
|
|
59
|
+
const def = q.default ?? q.choices[0] ?? null;
|
|
60
|
+
const picked = selectedOf(q);
|
|
61
|
+
const isFree = Boolean(freeOpen[q.id]) ||
|
|
62
|
+
(chosen.length > 0 &&
|
|
63
|
+
!picked.every((p) => q.choices.includes(p)));
|
|
64
|
+
const freeText = picked
|
|
65
|
+
.filter((p) => !q.choices.includes(p))
|
|
66
|
+
.join(", ");
|
|
67
|
+
return (_jsxs("div", { className: "ac-ask-q", "data-cmp": "ac/ask-question", "data-index": i + 1, "data-answered": chosen ? "true" : "false", "data-multi": q.multiSelect ? "true" : "false", children: [_jsxs("p", { className: "ac-ask-q__prompt", "data-cmp": "ac/ask-prompt", children: [total > 1 ? (_jsx("span", { className: "ac-ask-q__index", children: i + 1 })) : null, q.prompt, q.multiSelect ? (_jsx("span", { className: "ac-ask-q__multi-tag", children: "\u53EF\u591A\u9009" })) : null] }), _jsxs("div", { className: "ac-ask-q__choices", children: [q.choices.map((choice) => {
|
|
68
|
+
const recommended = choice === def;
|
|
69
|
+
const isPicked = picked.includes(choice);
|
|
70
|
+
return (_jsxs("button", { type: "button", "data-cmp": "ac/ask-choice", "data-state": isPicked
|
|
71
|
+
? "chosen"
|
|
72
|
+
: recommended
|
|
73
|
+
? "recommended"
|
|
74
|
+
: "idle", className: "ac-ask-q__chip", title: recommended ? "推荐答案" : "点击采用", onClick: () => pick(q, choice), children: [q.multiSelect ? (isPicked ? (_jsx(SquareCheck, { className: "size-3" })) : (_jsx(Square, { className: "size-3 opacity-50" }))) : isPicked ? (_jsx(Check, { className: "size-3" })) : recommended ? (_jsx(Sparkles, { className: "size-3" })) : null, _jsx("span", { children: choice })] }, choice));
|
|
75
|
+
}), _jsx("button", { type: "button", "data-cmp": "ac/ask-other", className: "ac-ask-q__chip ac-ask-q__chip--other", "data-state": isFree ? "chosen" : "idle", title: "\u8F93\u5165\u81EA\u5DF1\u7684\u7B54\u6848", onClick: () => setFreeOpen((prev) => ({ ...prev, [q.id]: !prev[q.id] })), children: "\u5176\u4ED6\u2026" })] }), isFree ? (_jsx("input", { "data-cmp": "ac/ask-input", className: "ac-ask-q__input", "aria-label": `第 ${i + 1} 题自由作答`, placeholder: "\u8F93\u5165\u4F60\u7684\u7B54\u6848\u2026", autoFocus: true, value: freeText, onChange: (e) => {
|
|
76
|
+
const keep = picked.filter((p) => q.choices.includes(p));
|
|
77
|
+
const next = e.target.value
|
|
78
|
+
? [...keep, e.target.value]
|
|
79
|
+
: keep;
|
|
80
|
+
setAnswers((prev) => ({
|
|
81
|
+
...prev,
|
|
82
|
+
[q.id]: next.join(", "),
|
|
83
|
+
}));
|
|
84
|
+
}, onKeyDown: (e) => {
|
|
85
|
+
if (e.key === "Enter") {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
const v = (answers[q.id] ?? "").trim();
|
|
88
|
+
if (!v)
|
|
89
|
+
return;
|
|
90
|
+
if (!batch)
|
|
91
|
+
onAnswer(v);
|
|
92
|
+
else if (total === 1)
|
|
93
|
+
onAnswer(v);
|
|
94
|
+
}
|
|
95
|
+
} })) : null] }, q.id));
|
|
96
|
+
}), hiddenOverflow > 0 ? (_jsxs("p", { className: "ac-ask-sheet__overflow", children: ["\u53E6\u6709 ", hiddenOverflow, " \u4E2A\u95EE\u9898\uFF0C\u5C06\u5728\u672C\u8F6E\u7B54\u590D\u540E\u7EE7\u7EED"] })) : null] }), total > 1 ? (_jsxs("div", { className: "ac-ask-sheet__actions", children: [_jsxs("button", { type: "button", "data-cmp": "ac/ask-default-all", className: "ac-ask-sheet__default", disabled: !canDefault, title: canDefault ? "全部采用推荐答案" : "存在无默认项的问题,需手动作答", onClick: useAllDefaults, children: [_jsx(Sparkles, { className: "size-3" }), "\u5168\u90E8\u7528\u9ED8\u8BA4"] }), _jsxs("button", { type: "button", "data-cmp": "ac/ask-submit", className: cn("ac-ask-sheet__submit", answered === 0 && "is-empty"), disabled: answered === 0, title: answered === 0 ? "至少答一题才能提交" : "提交全部答案", onClick: () => onAnswer(batchAnswersOf(questions, answers)), children: ["\u63D0\u4EA4 ", answered, "/", total, _jsx(ArrowRight, { className: "size-3" })] })] })) : null] }));
|
|
97
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Minimize2 } from "lucide-react";
|
|
3
|
+
export function CompactionCard({ part }) {
|
|
4
|
+
return (_jsxs("div", { className: "ac-compact", "data-cmp": "ac/compact", "data-state": "done", children: [_jsx(Minimize2, { "aria-hidden": "true", size: 12 }), _jsx("span", { className: "ac-compact__title", children: "\u4E0A\u4E0B\u6587\u538B\u7F29" }), _jsxs("span", { className: "ac-compact__tokens", children: [part.beforeTokens, " \u2192 ", part.afterTokens, " tokens"] }), _jsx("span", { className: "ac-compact__reason", title: part.reason, children: part.reason })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ContextUsageSegment, ContextWindowPreset } from "../../types/message";
|
|
2
|
+
export declare function formatTokens(n: number): string;
|
|
3
|
+
export interface ContextUsageProps {
|
|
4
|
+
input: number;
|
|
5
|
+
output?: number;
|
|
6
|
+
window: number;
|
|
7
|
+
breakdown?: readonly ContextUsageSegment[];
|
|
8
|
+
windowPresets?: readonly ContextWindowPreset[];
|
|
9
|
+
onWindowChange?: (window: number) => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
side?: "top" | "bottom";
|
|
12
|
+
onClose?: () => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function ContextUsage({ input, output, window: ctxWindow, breakdown, windowPresets, onWindowChange, className, side, onClose, }: ContextUsageProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId, useRef, useState } from "react";
|
|
3
|
+
import { AlertTriangle, X } from "lucide-react";
|
|
4
|
+
import { Popover } from "../../primitives/Popover";
|
|
5
|
+
import { cn } from "../../utils/cn";
|
|
6
|
+
const LEVEL_COLOR = {
|
|
7
|
+
comfortable: "text-[var(--accent-success)]",
|
|
8
|
+
warning: "text-[var(--accent-warning)]",
|
|
9
|
+
critical: "text-[var(--accent-danger)]",
|
|
10
|
+
};
|
|
11
|
+
const LEVEL_LABEL = {
|
|
12
|
+
comfortable: "充裕",
|
|
13
|
+
warning: "偏紧",
|
|
14
|
+
critical: "告急",
|
|
15
|
+
};
|
|
16
|
+
const SEGMENT_FALLBACK = [
|
|
17
|
+
"#818cf8",
|
|
18
|
+
"#34d399",
|
|
19
|
+
"#fbbf24",
|
|
20
|
+
"#a78bfa",
|
|
21
|
+
"#f472b6",
|
|
22
|
+
];
|
|
23
|
+
export function formatTokens(n) {
|
|
24
|
+
if (!Number.isFinite(n) || n < 0)
|
|
25
|
+
return "0";
|
|
26
|
+
if (n < 1000)
|
|
27
|
+
return String(n);
|
|
28
|
+
if (n < 1_000_000) {
|
|
29
|
+
const k = n / 1000;
|
|
30
|
+
return k < 10 ? `${k.toFixed(1)}K` : `${Math.round(k)}K`;
|
|
31
|
+
}
|
|
32
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
33
|
+
}
|
|
34
|
+
function Ring({ ratio, level }) {
|
|
35
|
+
const size = 20;
|
|
36
|
+
const stroke = 2.5;
|
|
37
|
+
const r = (size - stroke) / 2;
|
|
38
|
+
const circumference = 2 * Math.PI * r;
|
|
39
|
+
const color = level === "critical"
|
|
40
|
+
? "var(--accent-danger)"
|
|
41
|
+
: level === "warning"
|
|
42
|
+
? "var(--accent-warning)"
|
|
43
|
+
: "var(--accent-primary)";
|
|
44
|
+
return (_jsxs("svg", { width: size, height: size, "aria-hidden": "true", className: "shrink-0", children: [_jsx("circle", { cx: size / 2, cy: size / 2, r: r, fill: "none", stroke: "var(--surface-700)", strokeWidth: stroke }), _jsx("circle", { cx: size / 2, cy: size / 2, r: r, fill: "none", stroke: color, strokeWidth: stroke, strokeLinecap: "round", strokeDasharray: circumference, strokeDashoffset: circumference * (1 - Math.min(1, Math.max(0, ratio))), transform: `rotate(-90 ${size / 2} ${size / 2})` })] }));
|
|
45
|
+
}
|
|
46
|
+
function SegmentedBar({ segments, total, level, usedRatio, }) {
|
|
47
|
+
if (segments.length === 0) {
|
|
48
|
+
return (_jsx("div", { className: "flex h-2 w-full overflow-hidden rounded-full bg-secondary", children: _jsx("div", { className: "h-full rounded-full transition-[width] duration-300", style: {
|
|
49
|
+
width: `${Math.max(usedRatio * 100, usedRatio > 0 ? 2 : 0)}%`,
|
|
50
|
+
background: level === "critical"
|
|
51
|
+
? "var(--accent-danger)"
|
|
52
|
+
: level === "warning"
|
|
53
|
+
? "var(--accent-warning)"
|
|
54
|
+
: "var(--accent-primary)",
|
|
55
|
+
} }) }));
|
|
56
|
+
}
|
|
57
|
+
return (_jsx("div", { className: "flex h-2 w-full overflow-hidden rounded-full bg-secondary", children: segments.map((seg, i) => (_jsx("div", { title: `${seg.label} ${formatTokens(seg.tokens)}`, className: "h-full transition-[width] duration-300", style: {
|
|
58
|
+
width: `${total > 0 ? (seg.tokens / total) * 100 : 0}%`,
|
|
59
|
+
background: seg.color,
|
|
60
|
+
} }, `${seg.label}-${i}`))) }));
|
|
61
|
+
}
|
|
62
|
+
function WindowSlider({ presets, current, onChange, }) {
|
|
63
|
+
const idx = Math.max(0, presets.findIndex((p) => p.window === current));
|
|
64
|
+
const active = presets[idx] ?? presets[0];
|
|
65
|
+
const interactive = onChange !== undefined;
|
|
66
|
+
const trackRef = useRef(null);
|
|
67
|
+
const dragging = useRef(false);
|
|
68
|
+
const pickAt = (clientX) => {
|
|
69
|
+
if (!interactive || !trackRef.current)
|
|
70
|
+
return;
|
|
71
|
+
const rect = trackRef.current.getBoundingClientRect();
|
|
72
|
+
const frac = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
|
|
73
|
+
const next = Math.round(frac * (presets.length - 1));
|
|
74
|
+
if (presets[next] && presets[next].window !== current) {
|
|
75
|
+
onChange(presets[next].window);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
return (_jsxs("div", { className: "border-t border-border px-3 py-2.5", children: [_jsxs("div", { className: "mb-1.5 text-center text-body font-semibold", children: [formatTokens(active.window), " \u4E0A\u4E0B\u6587 \u00B7 ", active.label] }), _jsx("div", { ref: trackRef, role: interactive ? "slider" : undefined, "aria-valuemin": 0, "aria-valuemax": presets.length - 1, "aria-valuenow": idx, "aria-valuetext": `${formatTokens(active.window)} 上下文 · ${active.label}`, "aria-label": "\u4E0A\u4E0B\u6587\u7A97\u53E3\u6863\u4F4D", tabIndex: interactive ? 0 : undefined, className: cn("flex items-center gap-2 py-1", interactive && "cursor-pointer touch-none select-none"), onPointerDown: (e) => {
|
|
79
|
+
if (!interactive)
|
|
80
|
+
return;
|
|
81
|
+
dragging.current = true;
|
|
82
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
83
|
+
pickAt(e.clientX);
|
|
84
|
+
}, onPointerMove: (e) => {
|
|
85
|
+
if (!dragging.current)
|
|
86
|
+
return;
|
|
87
|
+
pickAt(e.clientX);
|
|
88
|
+
}, onPointerUp: (e) => {
|
|
89
|
+
dragging.current = false;
|
|
90
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
91
|
+
}, onKeyDown: (e) => {
|
|
92
|
+
if (!interactive)
|
|
93
|
+
return;
|
|
94
|
+
if (e.key === "ArrowLeft" && idx > 0) {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
onChange(presets[idx - 1].window);
|
|
97
|
+
}
|
|
98
|
+
if (e.key === "ArrowRight" && idx < presets.length - 1) {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
onChange(presets[idx + 1].window);
|
|
101
|
+
}
|
|
102
|
+
}, children: _jsxs("span", { className: "relative flex h-3 flex-1 items-center", children: [_jsx("span", { className: "h-1 w-full rounded-full bg-secondary" }), _jsx("span", { className: "absolute left-0 h-1 rounded-full bg-[var(--accent-primary)]", style: {
|
|
103
|
+
width: `${presets.length > 1 ? (idx / (presets.length - 1)) * 100 : 100}%`,
|
|
104
|
+
} }), presets.map((p, i) => (_jsx("span", { className: "absolute size-2.5 rounded-full border transition-colors", style: {
|
|
105
|
+
left: `calc(${presets.length > 1 ? (i / (presets.length - 1)) * 100 : 50}% - 5px)`,
|
|
106
|
+
background: i <= idx ? "var(--accent-primary)" : "var(--surface-600)",
|
|
107
|
+
borderColor: i <= idx ? "var(--accent-primary)" : "var(--border-600)",
|
|
108
|
+
} }, p.label)))] }) })] }));
|
|
109
|
+
}
|
|
110
|
+
export function ContextUsage({ input, output, window: ctxWindow, breakdown, windowPresets, onWindowChange, className, side = "top", onClose, }) {
|
|
111
|
+
const barId = useId();
|
|
112
|
+
const [open, setOpen] = useState(false);
|
|
113
|
+
if (!Number.isFinite(ctxWindow) || ctxWindow <= 0)
|
|
114
|
+
return null;
|
|
115
|
+
const used = input + (output ?? 0);
|
|
116
|
+
const usedPercent = Math.min(100, (used / ctxWindow) * 100);
|
|
117
|
+
const remainingPercent = Math.max(0, 100 - usedPercent);
|
|
118
|
+
const level = remainingPercent <= 10
|
|
119
|
+
? "critical"
|
|
120
|
+
: remainingPercent <= 25
|
|
121
|
+
? "warning"
|
|
122
|
+
: "comfortable";
|
|
123
|
+
const ratio = used / ctxWindow;
|
|
124
|
+
const segments = (breakdown ?? []).map((seg, i) => ({
|
|
125
|
+
label: seg.label,
|
|
126
|
+
tokens: seg.tokens,
|
|
127
|
+
color: seg.color || SEGMENT_FALLBACK[i % SEGMENT_FALLBACK.length],
|
|
128
|
+
}));
|
|
129
|
+
return (_jsx(Popover, { side: side, align: "end", open: open, onOpenChange: (next) => {
|
|
130
|
+
setOpen(next);
|
|
131
|
+
if (!next)
|
|
132
|
+
onClose?.();
|
|
133
|
+
}, trigger: ({ open: triggerOpen }) => (_jsxs("span", { role: "button", tabIndex: 0, "data-cmp": "ai/context-usage", "data-id": "usage-ring", "data-state": level, "data-level": level, "data-open": triggerOpen ? "true" : "false", "aria-label": `上下文用量:已用 ${usedPercent.toFixed(1)}%,剩余 ${remainingPercent.toFixed(1)}%`, title: `${LEVEL_LABEL[level]} · 已用 ${usedPercent.toFixed(1)}%`, className: cn("inline-flex h-[var(--control-sm)] min-w-7 cursor-pointer items-center gap-1.5 rounded-full px-1.5 transition-colors hover:bg-secondary", LEVEL_COLOR[level], className), children: [_jsx(Ring, { ratio: ratio, level: level }), _jsxs("span", { className: "text-micro tabular-nums", children: [usedPercent.toFixed(0), "%"] }), level === "critical" ? _jsx(AlertTriangle, { className: "size-3" }) : null] })), children: _jsxs("div", { "data-cmp": "ai/context-usage-panel", "data-open": open ? "true" : "false", className: "w-72 p-0", children: [_jsxs("div", { className: "flex items-start justify-between px-3.5 pt-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-body font-semibold", children: "\u4E0A\u4E0B\u6587\u7528\u91CF" }), _jsxs("div", { className: "mt-1 flex items-baseline gap-1.5", children: [_jsxs("span", { className: "text-heading font-semibold tabular-nums", children: [usedPercent.toFixed(1), "%"] }), _jsxs("span", { className: "text-meta text-muted-foreground", children: ["\u5DF2\u4F7F\u7528 ", formatTokens(used), "/ ", formatTokens(ctxWindow)] })] })] }), _jsx("button", { type: "button", "aria-label": "\u5173\u95ED\u7528\u91CF\u9762\u677F", className: "rounded p-0.5 text-muted-foreground hover:text-foreground", onClick: () => setOpen(false), children: _jsx(X, { className: "size-4" }) })] }), _jsx("div", { className: "px-3.5 pt-2.5", children: _jsx(SegmentedBar, { segments: segments, total: ctxWindow, level: level, usedRatio: ratio }, barId) }), segments.length > 0 ? (_jsx("div", { className: "px-3.5 pt-2.5 pb-1", children: segments.map((seg, i) => (_jsxs("div", { className: "flex items-center gap-2 py-0.5 text-meta", children: [_jsx("span", { className: "size-2 shrink-0 rounded-full", style: { background: seg.color } }), _jsx("span", { className: "text-foreground", children: seg.label }), _jsx("span", { className: "ml-auto tabular-nums text-muted-foreground", children: ctxWindow > 0
|
|
134
|
+
? `${((seg.tokens / ctxWindow) * 100).toFixed(1)}%`
|
|
135
|
+
: "0%" })] }, `${seg.label}-${i}`))) })) : (_jsxs("div", { className: "px-3.5 pt-2 pb-1", children: [_jsxs("div", { className: "flex items-center justify-between py-0.5 text-meta", children: [_jsx("span", { className: "text-muted-foreground", children: "\u8F93\u5165" }), _jsx("span", { className: "tabular-nums text-foreground", children: formatTokens(input) })] }), output !== undefined ? (_jsxs("div", { className: "flex items-center justify-between py-0.5 text-meta", children: [_jsx("span", { className: "text-muted-foreground", children: "\u8F93\u51FA" }), _jsx("span", { className: "tabular-nums text-foreground", children: formatTokens(output) })] })) : null] })), windowPresets && windowPresets.length > 1 ? (_jsx(WindowSlider, { presets: windowPresets, current: ctxWindow, onChange: onWindowChange })) : null] }) }));
|
|
136
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { DisplayPart } from "../../types/message";
|
|
3
|
+
export interface DisplayCardProps {
|
|
4
|
+
part: DisplayPart;
|
|
5
|
+
renderDisplay?: (part: DisplayPart) => ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export declare function DisplayCard({ part, renderDisplay }: DisplayCardProps): import("react").JSX.Element;
|