@djangocfg/widget-chat 0.1.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/LICENSE +21 -0
- package/README.md +124 -0
- package/package.json +95 -0
- package/src/composer/composer.tsx +100 -0
- package/src/composer/context.ts +33 -0
- package/src/composer/controls/button.tsx +104 -0
- package/src/composer/controls/language-menu.tsx +123 -0
- package/src/composer/controls/send.tsx +83 -0
- package/src/composer/controls/think.tsx +50 -0
- package/src/composer/controls/tools.tsx +130 -0
- package/src/composer/controls/tooltip.tsx +10 -0
- package/src/composer/controls/voice.tsx +266 -0
- package/src/composer/field/field.tsx +140 -0
- package/src/composer/field/input.tsx +214 -0
- package/src/composer/field/keys.ts +44 -0
- package/src/composer/field/use-autosize.ts +134 -0
- package/src/composer/focus-field.ts +27 -0
- package/src/composer/index.ts +86 -0
- package/src/composer/stories/field.tsx +112 -0
- package/src/composer/stories/harness.tsx +127 -0
- package/src/composer/tools/menu.tsx +129 -0
- package/src/composer/tools/rows.tsx +180 -0
- package/src/composer/tools/use-menu-nav.ts +66 -0
- package/src/composer/tools/use-recent-tools.ts +30 -0
- package/src/composer/tray/banner.tsx +157 -0
- package/src/composer/tray/command-progress.tsx +116 -0
- package/src/composer/tray/picked-tools.tsx +104 -0
- package/src/composer/tray/queue.tsx +202 -0
- package/src/composer/tray/use-replace-text.ts +58 -0
- package/src/composer/use-draft-persistence.ts +79 -0
- package/src/content/index.ts +16 -0
- package/src/content/media-content.tsx +38 -0
- package/src/content/response.tsx +221 -0
- package/src/core/adapter.ts +29 -0
- package/src/core/capabilities.ts +286 -0
- package/src/core/chunks.ts +66 -0
- package/src/core/content/incomplete-math.ts +118 -0
- package/src/core/content/index.ts +9 -0
- package/src/core/draft-storage.ts +62 -0
- package/src/core/events.ts +88 -0
- package/src/core/focus/index.ts +10 -0
- package/src/core/focus/roving.ts +56 -0
- package/src/core/group-parts.ts +80 -0
- package/src/core/index.ts +34 -0
- package/src/core/links/index.ts +33 -0
- package/src/core/links/prose.ts +191 -0
- package/src/core/links/scheme.ts +98 -0
- package/src/core/links/tool-id.ts +26 -0
- package/src/core/media/allowed-src.ts +138 -0
- package/src/core/media/block.ts +254 -0
- package/src/core/media/index.ts +22 -0
- package/src/core/media/kinds.ts +85 -0
- package/src/core/outbound.ts +86 -0
- package/src/core/parts.ts +169 -0
- package/src/core/queue/index.ts +15 -0
- package/src/core/queue/queue.ts +100 -0
- package/src/core/recall/boundary.ts +77 -0
- package/src/core/recall/from-transcript.ts +47 -0
- package/src/core/recall/index.ts +23 -0
- package/src/core/recall/ladder.ts +171 -0
- package/src/core/reload-decision.ts +36 -0
- package/src/core/scroll/index.ts +17 -0
- package/src/core/scroll/missed.ts +106 -0
- package/src/core/scroll/sent.ts +54 -0
- package/src/core/text-replace.ts +20 -0
- package/src/core/tool-name.ts +31 -0
- package/src/core/tool-picks-set.ts +48 -0
- package/src/core/tool-picks.ts +142 -0
- package/src/core/tool-prompt.ts +47 -0
- package/src/core/tools/capability.ts +34 -0
- package/src/core/tools/catalogue.ts +109 -0
- package/src/core/tools/index.ts +17 -0
- package/src/core/tools/layout.ts +44 -0
- package/src/core/tools/nav.ts +32 -0
- package/src/core/tools/rank.ts +60 -0
- package/src/core/tools/recent.ts +39 -0
- package/src/core/transcript.ts +301 -0
- package/src/core/voice/earcons.ts +58 -0
- package/src/core/voice/index.ts +26 -0
- package/src/core/voice/language-search.ts +62 -0
- package/src/core/voice/languages.ts +267 -0
- package/src/core/voice/resolve-language.ts +93 -0
- package/src/core/voice/session.ts +196 -0
- package/src/core/voice/web-speech.ts +170 -0
- package/src/i18n/index.ts +87 -0
- package/src/i18n/locales/ar.ts +97 -0
- package/src/i18n/locales/da.ts +90 -0
- package/src/i18n/locales/de.ts +90 -0
- package/src/i18n/locales/en.ts +90 -0
- package/src/i18n/locales/es.ts +90 -0
- package/src/i18n/locales/fr.ts +90 -0
- package/src/i18n/locales/it.ts +90 -0
- package/src/i18n/locales/ja.ts +83 -0
- package/src/i18n/locales/ko.ts +83 -0
- package/src/i18n/locales/nl.ts +90 -0
- package/src/i18n/locales/no.ts +90 -0
- package/src/i18n/locales/pl.ts +96 -0
- package/src/i18n/locales/ptBR.ts +90 -0
- package/src/i18n/locales/ru.ts +96 -0
- package/src/i18n/locales/sv.ts +90 -0
- package/src/i18n/locales/tr.ts +89 -0
- package/src/i18n/locales/zh.ts +83 -0
- package/src/i18n/plural.ts +62 -0
- package/src/i18n/types.ts +257 -0
- package/src/index.ts +18 -0
- package/src/primitives/index.ts +43 -0
- package/src/primitives/message.tsx +85 -0
- package/src/primitives/part-registry.tsx +44 -0
- package/src/primitives/stories-harness.tsx +118 -0
- package/src/primitives/suggestions.tsx +112 -0
- package/src/primitives/tool-call.tsx +145 -0
- package/src/primitives/tooltip.tsx +48 -0
- package/src/primitives/transcript.tsx +285 -0
- package/src/primitives/use-dismiss.ts +100 -0
- package/src/primitives/use-missed-count.ts +30 -0
- package/src/primitives/use-overflows.ts +69 -0
- package/src/primitives/use-return-on-send.ts +29 -0
- package/src/primitives/use-roving-focus.ts +69 -0
- package/src/primitives/use-select-all-transcript.ts +111 -0
- package/src/primitives/use-stick-to-bottom.ts +83 -0
- package/src/primitives/use-turn-activity.ts +25 -0
- package/src/store/context.ts +28 -0
- package/src/store/hooks.ts +215 -0
- package/src/store/index.ts +31 -0
- package/src/store/provider.tsx +97 -0
- package/src/store/store.ts +394 -0
- package/src/store/subscribers.ts +55 -0
- package/src/store/surfaces.ts +174 -0
- package/src/store/use-draft.ts +168 -0
- package/src/store/use-queue.ts +61 -0
- package/src/store/use-recall.ts +264 -0
- package/src/store/use-reload-on-revision.ts +67 -0
- package/src/store/use-speech-language.ts +84 -0
- package/src/store/use-voice.ts +116 -0
- package/src/styles/block.css +128 -0
- package/src/styles/composer.css +449 -0
- package/src/styles/index.css +47 -0
- package/src/styles/markdown.css +368 -0
- package/src/styles/surface.css +102 -0
- package/src/styles/tool.css +128 -0
- package/src/styles/turn-mark.css +97 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/mock-adapter.ts +69 -0
- package/src/testing/mock-tools.ts +101 -0
- package/src/testing/mock-voice.ts +81 -0
- package/src/ui/chat-composer.tsx +286 -0
- package/src/ui/chat-reply-chip.tsx +42 -0
- package/src/ui/chat-suggestions.tsx +87 -0
- package/src/ui/chat-transcript.tsx +157 -0
- package/src/ui/chat.tsx +197 -0
- package/src/ui/collapsible-body.tsx +180 -0
- package/src/ui/collapsible-row.tsx +81 -0
- package/src/ui/day-divider.tsx +49 -0
- package/src/ui/index.ts +54 -0
- package/src/ui/jump-to-latest.tsx +60 -0
- package/src/ui/link-chip.tsx +73 -0
- package/src/ui/message-actions.tsx +163 -0
- package/src/ui/message-quote.tsx +47 -0
- package/src/ui/message.tsx +295 -0
- package/src/ui/pending.tsx +85 -0
- package/src/ui/renderers/data.tsx +63 -0
- package/src/ui/renderers/reasoning.tsx +73 -0
- package/src/ui/renderers/tool.tsx +176 -0
- package/src/ui/turn-mark.tsx +31 -0
- package/src/utils/day-heading.ts +46 -0
- package/src/utils/error-message.ts +10 -0
- package/src/utils/index.ts +12 -0
- package/src/utils/markdown-text.ts +175 -0
- package/src/widgets/index.ts +14 -0
- package/src/widgets/media.tsx +239 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ChatCopy } from "../types";
|
|
2
|
+
|
|
3
|
+
export const zh: ChatCopy = {
|
|
4
|
+
transcript: {
|
|
5
|
+
empty: "暂无消息。",
|
|
6
|
+
suggestionsLabel: "推荐提问",
|
|
7
|
+
log: "对话",
|
|
8
|
+
announceStreaming: "助手正在回复",
|
|
9
|
+
announceComplete: "回复完成",
|
|
10
|
+
jumpToLatest: "跳到最新",
|
|
11
|
+
thinking: "思考中",
|
|
12
|
+
thinkingElapsed: "{{seconds}} 秒",
|
|
13
|
+
today: "今天",
|
|
14
|
+
yesterday: "昨天",
|
|
15
|
+
},
|
|
16
|
+
message: {
|
|
17
|
+
copy: "复制",
|
|
18
|
+
reply: "回复",
|
|
19
|
+
copyLink: "复制链接",
|
|
20
|
+
copiedLink: "链接已复制",
|
|
21
|
+
jumpToQuoted: "跳转到引用的消息",
|
|
22
|
+
copied: "已复制",
|
|
23
|
+
reasoning: "推理",
|
|
24
|
+
reasoningLive: "推理中…",
|
|
25
|
+
reasoningTokens: {
|
|
26
|
+
other: "思考了 {{count}} 个 token",
|
|
27
|
+
},
|
|
28
|
+
showMore: "展开",
|
|
29
|
+
showLess: "收起",
|
|
30
|
+
},
|
|
31
|
+
tool: {
|
|
32
|
+
inputStreaming: "准备中",
|
|
33
|
+
inputAvailable: "运行中",
|
|
34
|
+
outputAvailable: "完成",
|
|
35
|
+
outputError: "失败",
|
|
36
|
+
collapse: "收起工具调用",
|
|
37
|
+
expand: "展开工具调用",
|
|
38
|
+
},
|
|
39
|
+
composer: {
|
|
40
|
+
replyClear: "取消回复",
|
|
41
|
+
input: "消息",
|
|
42
|
+
placeholder: "问我任何问题",
|
|
43
|
+
send: "发送",
|
|
44
|
+
sendShortcut: "按 Enter 发送",
|
|
45
|
+
stop: "停止",
|
|
46
|
+
toolsMenu: "工具",
|
|
47
|
+
toolsSearch: "查找工具…",
|
|
48
|
+
toolsEmpty: "没有该工具",
|
|
49
|
+
toolsRecent: "最近使用",
|
|
50
|
+
toolsForget: "从最近使用中移除",
|
|
51
|
+
toolRemove: "移除工具",
|
|
52
|
+
needsFile: "请附上文件",
|
|
53
|
+
needsSkill: "请说明技能",
|
|
54
|
+
toolsPicked: "已选工具",
|
|
55
|
+
thinkOn: "深入思考(更慢、成本更高)",
|
|
56
|
+
thinkOff: "关闭深入思考",
|
|
57
|
+
},
|
|
58
|
+
queue: {
|
|
59
|
+
prompts: { other: "{{count}} 条消息排队中" },
|
|
60
|
+
more: { other: "还有 {{count}} 条" },
|
|
61
|
+
showLess: "收起",
|
|
62
|
+
remove: "从队列中移除",
|
|
63
|
+
show: "显示队列",
|
|
64
|
+
edit: "编辑此提示",
|
|
65
|
+
send: "立即发送",
|
|
66
|
+
sendNow: "立即发送,并停止当前回复",
|
|
67
|
+
sendNowConfirmTitle: "立即发送这条消息?",
|
|
68
|
+
sendNowConfirmMessage:
|
|
69
|
+
"当前回复会停止,未完成的工作将丢失。队列中的其余消息保持原有顺序。",
|
|
70
|
+
replaceDraftTitle: "替换已输入的内容?",
|
|
71
|
+
replaceDraftMessage:
|
|
72
|
+
"草稿与这条消息不同。请选择保留哪一个。",
|
|
73
|
+
},
|
|
74
|
+
voice: {
|
|
75
|
+
start: "语音输入",
|
|
76
|
+
stop: "停止语音输入",
|
|
77
|
+
language: "语音输入语言",
|
|
78
|
+
languageMenu: "语音输入语言",
|
|
79
|
+
languageSearch: "搜索语言",
|
|
80
|
+
languageEmpty: "未找到语言",
|
|
81
|
+
languageBack: "返回",
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { PluralCopy } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fill `{{name}}` placeholders.
|
|
5
|
+
*
|
|
6
|
+
* The dictionary is plain strings, so there is no interpolator behind it. A
|
|
7
|
+
* template literal at each call site would put copy assembly back into JSX,
|
|
8
|
+
* which is the seam this package's i18n exists to close.
|
|
9
|
+
*/
|
|
10
|
+
export function fill(
|
|
11
|
+
template: string,
|
|
12
|
+
values: Readonly<Record<string, string | number>>,
|
|
13
|
+
): string {
|
|
14
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key: string) => {
|
|
15
|
+
const value = values[key];
|
|
16
|
+
return value === undefined ? match : String(value);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One `Intl.PluralRules` per language, kept for the life of the tab.
|
|
22
|
+
*
|
|
23
|
+
* Constructing one costs a locale-data lookup, and the composer's queue summary
|
|
24
|
+
* re-renders on every keystroke while a turn streams. The cache is unbounded on
|
|
25
|
+
* purpose: its key space is the language list, seventeen entries.
|
|
26
|
+
*/
|
|
27
|
+
const rules = new Map<string, Intl.PluralRules>();
|
|
28
|
+
|
|
29
|
+
function rulesFor(language: string): Intl.PluralRules {
|
|
30
|
+
const cached = rules.get(language);
|
|
31
|
+
if (cached !== undefined) return cached;
|
|
32
|
+
// An unknown or malformed tag throws rather than falling back, and a bad
|
|
33
|
+
// language must not take the composer down — English plurals on an unknown
|
|
34
|
+
// tag are wrong in a way the user can read past.
|
|
35
|
+
let made: Intl.PluralRules;
|
|
36
|
+
try {
|
|
37
|
+
made = new Intl.PluralRules(language);
|
|
38
|
+
} catch {
|
|
39
|
+
made = new Intl.PluralRules("en");
|
|
40
|
+
}
|
|
41
|
+
rules.set(language, made);
|
|
42
|
+
return made;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Choose a plural form and fill `{{count}}`.
|
|
47
|
+
*
|
|
48
|
+
* Falls back to `other` when the category a language selects is absent from the
|
|
49
|
+
* dictionary. That is not a defect to fail on: `other` is required by the type,
|
|
50
|
+
* so a fallback always exists, and rendering the wrong-but-readable form beats
|
|
51
|
+
* rendering nothing at the moment a user is counting their own queued prompts.
|
|
52
|
+
*/
|
|
53
|
+
export function plural(
|
|
54
|
+
copy: PluralCopy,
|
|
55
|
+
count: number,
|
|
56
|
+
language: string,
|
|
57
|
+
values: Readonly<Record<string, string | number>> = {},
|
|
58
|
+
): string {
|
|
59
|
+
const category = rulesFor(language).select(count);
|
|
60
|
+
const template = copy[category] ?? copy.other;
|
|
61
|
+
return fill(template, { ...values, count });
|
|
62
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chat package's copy schema.
|
|
3
|
+
*
|
|
4
|
+
* English is the schema of record: every locale dictionary is typed against
|
|
5
|
+
* `ChatCopy`, so a missing or misspelled key fails `tsc` rather than rendering
|
|
6
|
+
* `undefined` into a composer button.
|
|
7
|
+
*
|
|
8
|
+
* Typing is what replaces the shared locale gates. The four scripts under
|
|
9
|
+
* `packages/i18n/scripts/` walk the catalogue in `packages/i18n/src/locales/`
|
|
10
|
+
* and cannot see a package-local dictionary, exactly as they cannot see the
|
|
11
|
+
* onboarding one.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The plural categories `Intl.PluralRules` can report.
|
|
16
|
+
*
|
|
17
|
+
* `other` is required and the rest are not, because a language HAS the
|
|
18
|
+
* categories it has: English resolves only `one` and `other`, Russian adds
|
|
19
|
+
* `few` and `many`, Japanese uses `other` alone. Requiring all six would force
|
|
20
|
+
* an English catalogue to invent a `few` that no input can ever select.
|
|
21
|
+
*/
|
|
22
|
+
export interface PluralCopy {
|
|
23
|
+
readonly zero?: string;
|
|
24
|
+
readonly one?: string;
|
|
25
|
+
readonly two?: string;
|
|
26
|
+
readonly few?: string;
|
|
27
|
+
readonly many?: string;
|
|
28
|
+
readonly other: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Copy for the transcript: the log, its empty state, and the wait. */
|
|
32
|
+
export interface TranscriptCopy {
|
|
33
|
+
/** Shown when there are no messages — an empty transcript is a real state. */
|
|
34
|
+
readonly empty: string;
|
|
35
|
+
/**
|
|
36
|
+
* Accessible name of the starter-prompt list, so the group is not anonymous.
|
|
37
|
+
*
|
|
38
|
+
* Only the NAME lives here. The rows' own titles come from the host with its
|
|
39
|
+
* catalogue, because what an agent can be asked to do is a fact about the
|
|
40
|
+
* agent and this package does not hold it.
|
|
41
|
+
*/
|
|
42
|
+
readonly suggestionsLabel: string;
|
|
43
|
+
/** Accessible name of the log, so the landmark is not anonymous. */
|
|
44
|
+
readonly log: string;
|
|
45
|
+
/** The two strings a screen reader hears per turn. */
|
|
46
|
+
readonly announceStreaming: string;
|
|
47
|
+
readonly announceComplete: string;
|
|
48
|
+
/** The return-to-bottom control, which is icon-only. */
|
|
49
|
+
readonly jumpToLatest: string;
|
|
50
|
+
/** What the wait is for, before the first token arrives. */
|
|
51
|
+
readonly thinking: string;
|
|
52
|
+
/** The elapsed counter. `{{seconds}}` is whole seconds. */
|
|
53
|
+
readonly thinkingElapsed: string;
|
|
54
|
+
/**
|
|
55
|
+
* The two relative day headings. Older days are named by their date, which
|
|
56
|
+
* `lib-utils` formats in the reader's locale — so there is no third key here.
|
|
57
|
+
*/
|
|
58
|
+
readonly today: string;
|
|
59
|
+
readonly yesterday: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Copy for the message action row and the collapsible reasoning block. */
|
|
63
|
+
export interface MessageCopy {
|
|
64
|
+
readonly copy: string;
|
|
65
|
+
readonly copied: string;
|
|
66
|
+
readonly reply: string;
|
|
67
|
+
/** Copy a link that reopens this one message. */
|
|
68
|
+
readonly copyLink: string;
|
|
69
|
+
/** Confirmation after {@link copyLink}, distinct from `copied`: two actions
|
|
70
|
+
* in one row both confirm, and one word for both hides which fired. */
|
|
71
|
+
readonly copiedLink: string;
|
|
72
|
+
/** Names the quote above a turn, which is a button back to what it answers. */
|
|
73
|
+
readonly jumpToQuoted: string;
|
|
74
|
+
/** Summary of the collapsed reasoning block, once the thought has settled. */
|
|
75
|
+
readonly reasoning: string;
|
|
76
|
+
/**
|
|
77
|
+
* Summary WHILE the thought is still arriving.
|
|
78
|
+
*
|
|
79
|
+
* A separate key rather than reusing `reasoning`: a block that says
|
|
80
|
+
* "Reasoning" while it fills and "Reasoning" when it is done tells the reader
|
|
81
|
+
* nothing about which state they are looking at, and the live one is the
|
|
82
|
+
* state that explains why nothing else has appeared yet.
|
|
83
|
+
*/
|
|
84
|
+
readonly reasoningLive: string;
|
|
85
|
+
/**
|
|
86
|
+
* The block's summary when the engine stripped the words and reported only a
|
|
87
|
+
* count. `{{count}}` is reasoning tokens.
|
|
88
|
+
*
|
|
89
|
+
* This exists because the alternative is silence: a provider that ships the
|
|
90
|
+
* thinking block with its text removed leaves the figure as the only true
|
|
91
|
+
* thing, and a block keyed on text alone renders nothing at all.
|
|
92
|
+
*
|
|
93
|
+
* A `PluralCopy` because it counts: Russian and Polish select a different
|
|
94
|
+
* form at 1, 2 and 5, and a flat string would read as broken grammar on the
|
|
95
|
+
* majority of counts rather than on an edge case.
|
|
96
|
+
*/
|
|
97
|
+
readonly reasoningTokens: PluralCopy;
|
|
98
|
+
/**
|
|
99
|
+
* The two labels of the trigger that opens and closes a long turn.
|
|
100
|
+
*
|
|
101
|
+
* Both are present because the control is a toggle whose label states what
|
|
102
|
+
* the next press DOES, not what the current state is — an unlabelled arrow
|
|
103
|
+
* would leave the reader guessing which direction it goes.
|
|
104
|
+
*/
|
|
105
|
+
readonly showMore: string;
|
|
106
|
+
readonly showLess: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The four tool-call states, in the order they occur.
|
|
111
|
+
*
|
|
112
|
+
* Named for what the user sees rather than for the wire's `input-streaming`
|
|
113
|
+
* spelling: the vocabulary a person reads is not the vocabulary a protocol
|
|
114
|
+
* uses, and tying the copy to the second makes a protocol rename a copy change.
|
|
115
|
+
*/
|
|
116
|
+
export interface ToolCopy {
|
|
117
|
+
readonly inputStreaming: string;
|
|
118
|
+
readonly inputAvailable: string;
|
|
119
|
+
readonly outputAvailable: string;
|
|
120
|
+
readonly outputError: string;
|
|
121
|
+
/** Names the disclosure button while the call is open. */
|
|
122
|
+
readonly collapse: string;
|
|
123
|
+
/** Names it while the call is shut. The chevron is decorative, so this is
|
|
124
|
+
* the only thing a screen reader has to go on. */
|
|
125
|
+
readonly expand: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Copy for the strip of prompts waiting for the current turn to end. */
|
|
129
|
+
export interface QueueCopy {
|
|
130
|
+
/** The summary line above a collapsed queue. `{{count}}`. */
|
|
131
|
+
readonly prompts: PluralCopy;
|
|
132
|
+
/** The fold: how many rows are hidden past the inline limit. `{{count}}`. */
|
|
133
|
+
readonly more: PluralCopy;
|
|
134
|
+
/** Folds them back. */
|
|
135
|
+
readonly showLess: string;
|
|
136
|
+
readonly remove: string;
|
|
137
|
+
readonly show: string;
|
|
138
|
+
/** Takes the prompt back into the composer. Tooltip only — the row IS the control. */
|
|
139
|
+
readonly edit: string;
|
|
140
|
+
/**
|
|
141
|
+
* Names the control that cuts the running turn short to release one prompt.
|
|
142
|
+
*
|
|
143
|
+
* Must name BOTH halves — the turn is lost, and a label reading only "send"
|
|
144
|
+
* hides that from the one click where it matters.
|
|
145
|
+
*/
|
|
146
|
+
readonly sendNow: string;
|
|
147
|
+
/**
|
|
148
|
+
* The SAME control with no turn to cut short — after a Stop, where the queue
|
|
149
|
+
* refuses to drain on its own.
|
|
150
|
+
*
|
|
151
|
+
* A separate string rather than reusing {@link sendNow}, whose whole job is to
|
|
152
|
+
* name the interruption: promising to stop a reply that already ended
|
|
153
|
+
* describes something that cannot happen, and a label that over-warns is
|
|
154
|
+
* trusted no more than one that under-warns.
|
|
155
|
+
*/
|
|
156
|
+
readonly send: string;
|
|
157
|
+
/** The confirmation that guards it. Destructive: work already done is lost. */
|
|
158
|
+
readonly sendNowConfirmTitle: string;
|
|
159
|
+
readonly sendNowConfirmMessage: string;
|
|
160
|
+
/**
|
|
161
|
+
* Asked when taking a prompt back would overwrite what is already typed.
|
|
162
|
+
* The buttons are the dialog's own — only the question is ours.
|
|
163
|
+
*/
|
|
164
|
+
readonly replaceDraftTitle: string;
|
|
165
|
+
readonly replaceDraftMessage: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Copy for dictation: the mic, and the language picker beside it. */
|
|
169
|
+
export interface VoiceCopy {
|
|
170
|
+
readonly start: string;
|
|
171
|
+
readonly stop: string;
|
|
172
|
+
readonly language: string;
|
|
173
|
+
readonly languageMenu: string;
|
|
174
|
+
readonly languageSearch: string;
|
|
175
|
+
readonly languageEmpty: string;
|
|
176
|
+
readonly languageBack: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Copy for the composer's field, its tools menu, and the send control. */
|
|
180
|
+
export interface ComposerCopy {
|
|
181
|
+
/** Accessible name of the textarea. */
|
|
182
|
+
readonly input: string;
|
|
183
|
+
readonly placeholder: string;
|
|
184
|
+
readonly send: string;
|
|
185
|
+
/**
|
|
186
|
+
* Hover-only hint naming the key that sends, beneath the send label.
|
|
187
|
+
*
|
|
188
|
+
* A hint, never the accessible name: it states the binding `field/keys.ts`
|
|
189
|
+
* actually makes — bare Enter — so it must be re-read there before it is
|
|
190
|
+
* changed here, and it stays out of `send` so a screen reader is not read a
|
|
191
|
+
* keyboard tip it cannot act on.
|
|
192
|
+
*/
|
|
193
|
+
readonly sendShortcut: string;
|
|
194
|
+
readonly stop: string;
|
|
195
|
+
/** Dismiss the chip naming the message this draft answers. */
|
|
196
|
+
readonly replyClear: string;
|
|
197
|
+
/**
|
|
198
|
+
* The `+` menu: what this agent can do.
|
|
199
|
+
*
|
|
200
|
+
* Names the ARSENAL, not the button's shape. "Add" or "Attach" is what a `+`
|
|
201
|
+
* means in every other chat product, and this one lists capabilities — a
|
|
202
|
+
* label borrowed from the familiar gesture would promise a file picker.
|
|
203
|
+
*/
|
|
204
|
+
readonly toolsMenu: string;
|
|
205
|
+
readonly toolsSearch: string;
|
|
206
|
+
readonly toolsEmpty: string;
|
|
207
|
+
/** Heading for the tools this user reached for last, above the catalogue. */
|
|
208
|
+
readonly toolsRecent: string;
|
|
209
|
+
/** Drop one tool from that history: the row's `x`, not the chip's. */
|
|
210
|
+
readonly toolsForget: string;
|
|
211
|
+
/** Drop a picked tool from the draft: the chip's `x`. */
|
|
212
|
+
readonly toolRemove: string;
|
|
213
|
+
/**
|
|
214
|
+
* The field's placeholder while a picked tool still lacks its object.
|
|
215
|
+
*
|
|
216
|
+
* It REPLACES the ordinary placeholder rather than adding a line, so the
|
|
217
|
+
* composer stays one box: the standing advice ("Ask anything") is exactly
|
|
218
|
+
* what stops applying the moment a pick names something more specific.
|
|
219
|
+
*
|
|
220
|
+
* Both are imperative and name the act, never the tool — the chip beside the
|
|
221
|
+
* field already says which tool asked.
|
|
222
|
+
*/
|
|
223
|
+
readonly needsFile: string;
|
|
224
|
+
readonly needsSkill: string;
|
|
225
|
+
/**
|
|
226
|
+
* Names the ROW OF CHIPS already picked, not the menu that picks them.
|
|
227
|
+
* Each tool name renders twice — once in `toolsMenu`, once on its chip — so
|
|
228
|
+
* this label must not echo `toolsMenu`, or a screen reader and a test lose
|
|
229
|
+
* the only handle telling the two places apart.
|
|
230
|
+
*/
|
|
231
|
+
readonly toolsPicked: string;
|
|
232
|
+
/**
|
|
233
|
+
* Turn the longer-reasoning pass ON — the label while it is off.
|
|
234
|
+
*
|
|
235
|
+
* It NAMES THE COST ("slower, costs more") because this is the only surface
|
|
236
|
+
* that can: the package ships no tooltip, and the button is the last thing a
|
|
237
|
+
* user touches before spending. A bare "Think longer" sells the upside and
|
|
238
|
+
* hides the bill.
|
|
239
|
+
*
|
|
240
|
+
* **Never put a currency amount here.** Money beside the message box is what
|
|
241
|
+
* makes people ration — the same reason the allowance indicator is a bar with
|
|
242
|
+
* no numbers, and amounts live on the review surface (Settings > Plan). State
|
|
243
|
+
* the DIRECTION, never the figure.
|
|
244
|
+
*/
|
|
245
|
+
readonly thinkOn: string;
|
|
246
|
+
/** Turn it OFF — the label while it is on. No cost note: stopping costs nothing. */
|
|
247
|
+
readonly thinkOff: string;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface ChatCopy {
|
|
251
|
+
readonly transcript: TranscriptCopy;
|
|
252
|
+
readonly message: MessageCopy;
|
|
253
|
+
readonly tool: ToolCopy;
|
|
254
|
+
readonly composer: ComposerCopy;
|
|
255
|
+
readonly queue: QueueCopy;
|
|
256
|
+
readonly voice: VoiceCopy;
|
|
257
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Public root: <Chat>, the primitives, the composer, and the core contracts.
|
|
2
|
+
//
|
|
3
|
+
// `./content` is NOT re-exported here, and that is the one deliberate
|
|
4
|
+
// exclusion. It pulls `streamdown` plus shiki and katex — a host rendering a
|
|
5
|
+
// read-only transcript, or supplying its own markdown renderer, must not pay
|
|
6
|
+
// for a syntax highlighter. Everything else in this package is behaviour and
|
|
7
|
+
// types, which cost nothing to re-export.
|
|
8
|
+
//
|
|
9
|
+
// A host that renders model markdown imports `@djangocfg/widget-chat/content` and
|
|
10
|
+
// registers `Response` as its own text renderer. That is one line in the part
|
|
11
|
+
// registry, and it keeps the choice — and the weight — with the host.
|
|
12
|
+
|
|
13
|
+
export * from "./core";
|
|
14
|
+
export * from "./i18n";
|
|
15
|
+
export * from "./store";
|
|
16
|
+
export * from "./primitives";
|
|
17
|
+
export * from "./composer";
|
|
18
|
+
export * from "./ui";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Headless compound components: behaviour and accessibility, NO styling.
|
|
2
|
+
// Transcript, message, tool calls, the part registry, focus and scroll.
|
|
3
|
+
//
|
|
4
|
+
// May import: store, core, utils.
|
|
5
|
+
// May NOT import: composer, ui, Tailwind classes.
|
|
6
|
+
//
|
|
7
|
+
// Unstyled on purpose — this is the escape hatch. Beside a ready-made <Chat>
|
|
8
|
+
// there is always <ChatProvider> plus your own UI. Without one, prop explosion
|
|
9
|
+
// returns: that is exactly how the predecessor reached 83 props.
|
|
10
|
+
|
|
11
|
+
export { Transcript } from "./transcript";
|
|
12
|
+
export type { TranscriptProps } from "./transcript";
|
|
13
|
+
export { Message, MessageContent } from "./message";
|
|
14
|
+
export type { MessageProps, MessageContentProps } from "./message";
|
|
15
|
+
export { PartRegistryProvider, usePartRenderer } from "./part-registry";
|
|
16
|
+
export type { PartRegistry, PartRenderer, PartRenderProps, PartRegistryProviderProps } from "./part-registry";
|
|
17
|
+
export { ToolCall, ToolCallHeader, ToolCallInput, ToolCallOutput, SubagentThread } from "./tool-call";
|
|
18
|
+
export type {
|
|
19
|
+
ToolCallProps,
|
|
20
|
+
ToolCallHeaderProps,
|
|
21
|
+
ToolCallInputProps,
|
|
22
|
+
ToolCallOutputProps,
|
|
23
|
+
SubagentThreadProps,
|
|
24
|
+
} from "./tool-call";
|
|
25
|
+
export { Suggestions, useHasSuggestions } from "./suggestions";
|
|
26
|
+
export type { SuggestionsProps, SuggestionRenderProps } from "./suggestions";
|
|
27
|
+
export { useDismiss } from "./use-dismiss";
|
|
28
|
+
export type { UseDismissOptions } from "./use-dismiss";
|
|
29
|
+
export {
|
|
30
|
+
useSelectAllTranscript,
|
|
31
|
+
selectTranscript,
|
|
32
|
+
claimsSelectAll,
|
|
33
|
+
isEditableTarget,
|
|
34
|
+
} from "./use-select-all-transcript";
|
|
35
|
+
export { useRovingFocus } from "./use-roving-focus";
|
|
36
|
+
export type { RovingFocus } from "./use-roving-focus";
|
|
37
|
+
export { useStickToBottom } from "./use-stick-to-bottom";
|
|
38
|
+
export type { StickToBottom } from "./use-stick-to-bottom";
|
|
39
|
+
export { useOverflows } from "./use-overflows";
|
|
40
|
+
export { useTurnActivity } from "./use-turn-activity";
|
|
41
|
+
export type { TurnActivity } from "./use-turn-activity";
|
|
42
|
+
export { ChatTooltip } from "./tooltip";
|
|
43
|
+
export type { ChatTooltipProps } from "./tooltip";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { memo, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import type { ChatMessage, MessagePart } from "../core/parts";
|
|
4
|
+
import { useMessage } from "../store";
|
|
5
|
+
import { usePartRenderer } from "./part-registry";
|
|
6
|
+
|
|
7
|
+
export interface MessageProps {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly className?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Wrap the rendered parts — the bubble, the avatar, the timestamp. Receives
|
|
12
|
+
* the message so a host can vary by role or status without this component
|
|
13
|
+
* growing a prop per variation.
|
|
14
|
+
*/
|
|
15
|
+
readonly children?: (message: ChatMessage, parts: ReactNode) => ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One message, rendered from its parts.
|
|
20
|
+
*
|
|
21
|
+
* There is no `renderText` / `renderTool` / `renderImage` prop set. Part
|
|
22
|
+
* rendering resolves through a registry keyed on part type, so a product adds a
|
|
23
|
+
* part kind by registering a renderer rather than by widening this signature —
|
|
24
|
+
* the difference between an API that grows O(1) and one that reached 83 props.
|
|
25
|
+
*/
|
|
26
|
+
export const Message = memo(function Message({ id, className, children }: MessageProps) {
|
|
27
|
+
const message = useMessage(id);
|
|
28
|
+
if (message === undefined) return null;
|
|
29
|
+
return (
|
|
30
|
+
<MessageContent message={message} className={className}>
|
|
31
|
+
{children}
|
|
32
|
+
</MessageContent>
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export interface MessageContentProps {
|
|
37
|
+
readonly message: ChatMessage;
|
|
38
|
+
readonly className?: string;
|
|
39
|
+
readonly children?: (message: ChatMessage, parts: ReactNode) => ReactNode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Render a message we already hold, bypassing the store.
|
|
44
|
+
*
|
|
45
|
+
* A subagent's transcript lives INSIDE a tool part, not in the store, so
|
|
46
|
+
* `useMessage(id)` would find nothing for it. Splitting the lookup from the
|
|
47
|
+
* rendering lets both paths share one renderer instead of growing a second,
|
|
48
|
+
* subtly-different one for nested threads.
|
|
49
|
+
*/
|
|
50
|
+
export const MessageContent = memo(function MessageContent({
|
|
51
|
+
message,
|
|
52
|
+
className,
|
|
53
|
+
children,
|
|
54
|
+
}: MessageContentProps) {
|
|
55
|
+
const parts = (
|
|
56
|
+
<>
|
|
57
|
+
{message.parts.map((part) => (
|
|
58
|
+
<Part key={part.id} part={part} message={message} />
|
|
59
|
+
))}
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (children !== undefined) return <>{children(message, parts)}</>;
|
|
64
|
+
return (
|
|
65
|
+
<div className={className} data-role={message.role} data-status={message.status.state}>
|
|
66
|
+
{parts}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
interface PartProps {
|
|
72
|
+
readonly part: MessagePart;
|
|
73
|
+
readonly message: ChatMessage;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Memoized per part so a delta landing in the last text part does not re-render
|
|
78
|
+
* the tool call above it. Parts are immutable in the reducer, so reference
|
|
79
|
+
* equality is an exact "did this part change".
|
|
80
|
+
*/
|
|
81
|
+
const Part = memo(function Part({ part, message }: PartProps) {
|
|
82
|
+
const render = usePartRenderer(part.type);
|
|
83
|
+
if (render === undefined) return null;
|
|
84
|
+
return <>{render({ part, message })}</>;
|
|
85
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import type { ChatMessage, MessagePart } from "../core/parts";
|
|
4
|
+
|
|
5
|
+
export interface PartRenderProps {
|
|
6
|
+
readonly part: MessagePart;
|
|
7
|
+
readonly message: ChatMessage;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type PartRenderer = (props: PartRenderProps) => ReactNode;
|
|
11
|
+
|
|
12
|
+
/** `type` → renderer. Unregistered types render nothing rather than crashing. */
|
|
13
|
+
export type PartRegistry = Readonly<Record<string, PartRenderer>>;
|
|
14
|
+
|
|
15
|
+
const PartRegistryContext = createContext<PartRegistry>({});
|
|
16
|
+
|
|
17
|
+
export interface PartRegistryProviderProps {
|
|
18
|
+
readonly renderers: PartRegistry;
|
|
19
|
+
readonly children: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Registry of part renderers, inherited by nested threads.
|
|
24
|
+
*
|
|
25
|
+
* Inheritance is the load-bearing part, and it is why this is a context rather
|
|
26
|
+
* than a prop. A subagent's transcript renders inside a tool part; without
|
|
27
|
+
* inheritance every host would have to re-register the same renderers at every
|
|
28
|
+
* nesting level, and the ones they forgot would silently render nothing.
|
|
29
|
+
* assistant-ui reached the same conclusion.
|
|
30
|
+
*
|
|
31
|
+
* Nesting MERGES rather than replaces, so a nested thread can override one
|
|
32
|
+
* renderer — a compact tool card, say — without losing the rest.
|
|
33
|
+
*/
|
|
34
|
+
export function PartRegistryProvider({ renderers, children }: PartRegistryProviderProps) {
|
|
35
|
+
const inherited = useContext(PartRegistryContext);
|
|
36
|
+
const merged = useMemo(() => ({ ...inherited, ...renderers }), [inherited, renderers]);
|
|
37
|
+
return (
|
|
38
|
+
<PartRegistryContext.Provider value={merged}>{children}</PartRegistryContext.Provider>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function usePartRenderer(type: string): PartRenderer | undefined {
|
|
43
|
+
return useContext(PartRegistryContext)[type];
|
|
44
|
+
}
|