@extrachill/chat 0.5.0 → 0.6.0
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/CHANGELOG.md +10 -0
- package/README.md +1 -1
- package/css/chat.css +184 -1
- package/dist/Chat.d.ts +27 -1
- package/dist/Chat.d.ts.map +1 -1
- package/dist/Chat.js +4 -2
- package/dist/client-context.d.ts +31 -0
- package/dist/client-context.d.ts.map +1 -0
- package/dist/client-context.js +100 -0
- package/dist/components/ChatMessages.d.ts +18 -1
- package/dist/components/ChatMessages.d.ts.map +1 -1
- package/dist/components/ChatMessages.js +5 -1
- package/dist/components/CopyTranscriptButton.d.ts +12 -0
- package/dist/components/CopyTranscriptButton.d.ts.map +1 -0
- package/dist/components/CopyTranscriptButton.js +26 -0
- package/dist/components/DiffCard.d.ts +40 -0
- package/dist/components/DiffCard.d.ts.map +1 -0
- package/dist/components/DiffCard.js +162 -0
- package/dist/components/ErrorBoundary.d.ts +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/diff.d.ts +27 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +109 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/transcript.d.ts +4 -0
- package/dist/transcript.d.ts.map +1 -0
- package/dist/transcript.js +32 -0
- package/package.json +1 -1
- package/src/Chat.tsx +58 -9
- package/src/client-context.ts +160 -0
- package/src/components/ChatMessages.tsx +26 -0
- package/src/components/CopyTranscriptButton.tsx +58 -0
- package/src/components/DiffCard.tsx +252 -0
- package/src/components/index.ts +1 -0
- package/src/diff.ts +159 -0
- package/src/index.ts +39 -1
- package/src/transcript.ts +41 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Portable diff visualization card.
|
|
5
|
+
*
|
|
6
|
+
* Renders a before/after comparison with word-level `<ins>` / `<del>` tags
|
|
7
|
+
* and Accept / Reject buttons. Pure React — no Gutenberg or WordPress
|
|
8
|
+
* dependencies. Works anywhere `@extrachill/chat` is consumed.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <DiffCard
|
|
13
|
+
* diff={{
|
|
14
|
+
* diffId: 'abc123',
|
|
15
|
+
* diffType: 'edit',
|
|
16
|
+
* originalContent: 'Hello world',
|
|
17
|
+
* replacementContent: 'Hello universe',
|
|
18
|
+
* }}
|
|
19
|
+
* onAccept={(id) => apiFetch({ path: `/resolve/${id}`, method: 'POST', data: { action: 'accept' } })}
|
|
20
|
+
* onReject={(id) => apiFetch({ path: `/resolve/${id}`, method: 'POST', data: { action: 'reject' } })}
|
|
21
|
+
* />
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function DiffCard({ diff, onAccept, onReject, loading = false, className, }) {
|
|
25
|
+
const [status, setStatus] = useState(diff.status ?? 'pending');
|
|
26
|
+
const baseClass = 'ec-chat-diff';
|
|
27
|
+
const classes = [
|
|
28
|
+
baseClass,
|
|
29
|
+
status !== 'pending' ? `${baseClass}--${status}` : '',
|
|
30
|
+
className,
|
|
31
|
+
].filter(Boolean).join(' ');
|
|
32
|
+
const handleAccept = () => {
|
|
33
|
+
setStatus('accepted');
|
|
34
|
+
onAccept?.(diff.diffId);
|
|
35
|
+
};
|
|
36
|
+
const handleReject = () => {
|
|
37
|
+
setStatus('rejected');
|
|
38
|
+
onReject?.(diff.diffId);
|
|
39
|
+
};
|
|
40
|
+
const diffHtml = renderDiff(diff);
|
|
41
|
+
return (_jsxs("div", { className: classes, children: [_jsxs("div", { className: `${baseClass}__header`, children: [_jsx("span", { className: `${baseClass}__icon`, children: status === 'accepted' ? '✓' : status === 'rejected' ? '✗' : '⟳' }), _jsx("span", { className: `${baseClass}__label`, children: diff.summary ?? formatDiffLabel(diff.diffType) }), status !== 'pending' && (_jsx("span", { className: `${baseClass}__status`, children: status === 'accepted' ? 'Applied' : 'Rejected' }))] }), diff.insertionPoint && (_jsx("div", { className: `${baseClass}__meta`, children: diff.insertionPoint })), _jsx("div", { className: `${baseClass}__content`, dangerouslySetInnerHTML: { __html: diffHtml } }), status === 'pending' && (_jsxs("div", { className: `${baseClass}__actions`, children: [_jsx("button", { type: "button", className: `${baseClass}__accept`, onClick: handleAccept, disabled: loading, children: "Accept" }), _jsx("button", { type: "button", className: `${baseClass}__reject`, onClick: handleReject, disabled: loading, children: "Reject" })] }))] }));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Render diff HTML based on the diff type.
|
|
45
|
+
*
|
|
46
|
+
* For 'edit' diffs: word-level comparison between original and replacement.
|
|
47
|
+
* For 'replace' diffs: word-level comparison between original and replacement.
|
|
48
|
+
* For 'insert' diffs: everything is new (all `<ins>`).
|
|
49
|
+
*/
|
|
50
|
+
function renderDiff(diff) {
|
|
51
|
+
const { diffType, originalContent, replacementContent } = diff;
|
|
52
|
+
if (diffType === 'insert') {
|
|
53
|
+
return `<ins class="ec-chat-diff__added">${escapeHtml(replacementContent)}</ins>`;
|
|
54
|
+
}
|
|
55
|
+
// Both 'edit' and 'replace' get word-level diff
|
|
56
|
+
return createWordLevelDiff(originalContent, replacementContent);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create a word-level diff between two strings.
|
|
60
|
+
*
|
|
61
|
+
* Splits both strings into words, runs a longest-common-subsequence (LCS)
|
|
62
|
+
* alignment, and wraps removed words in `<del>` and added words in `<ins>`.
|
|
63
|
+
* Consecutive unchanged words are emitted as-is.
|
|
64
|
+
*/
|
|
65
|
+
function createWordLevelDiff(oldText, newText) {
|
|
66
|
+
if (oldText === newText) {
|
|
67
|
+
return escapeHtml(newText);
|
|
68
|
+
}
|
|
69
|
+
const oldWords = tokenize(oldText);
|
|
70
|
+
const newWords = tokenize(newText);
|
|
71
|
+
// LCS table
|
|
72
|
+
const m = oldWords.length;
|
|
73
|
+
const n = newWords.length;
|
|
74
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
75
|
+
for (let i = 1; i <= m; i++) {
|
|
76
|
+
for (let j = 1; j <= n; j++) {
|
|
77
|
+
if (oldWords[i - 1] === newWords[j - 1]) {
|
|
78
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Backtrack to produce diff operations
|
|
86
|
+
const ops = [];
|
|
87
|
+
let i = m;
|
|
88
|
+
let j = n;
|
|
89
|
+
while (i > 0 || j > 0) {
|
|
90
|
+
if (i > 0 && j > 0 && oldWords[i - 1] === newWords[j - 1]) {
|
|
91
|
+
ops.unshift({ type: 'keep', text: oldWords[i - 1] });
|
|
92
|
+
i--;
|
|
93
|
+
j--;
|
|
94
|
+
}
|
|
95
|
+
else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
96
|
+
ops.unshift({ type: 'ins', text: newWords[j - 1] });
|
|
97
|
+
j--;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
ops.unshift({ type: 'del', text: oldWords[i - 1] });
|
|
101
|
+
i--;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Merge consecutive same-type ops into spans
|
|
105
|
+
let html = '';
|
|
106
|
+
let currentType = null;
|
|
107
|
+
let buffer = [];
|
|
108
|
+
const flush = () => {
|
|
109
|
+
if (buffer.length === 0)
|
|
110
|
+
return;
|
|
111
|
+
const text = buffer.join(' ');
|
|
112
|
+
if (currentType === 'del') {
|
|
113
|
+
html += `<del class="ec-chat-diff__removed">${escapeHtml(text)}</del>`;
|
|
114
|
+
}
|
|
115
|
+
else if (currentType === 'ins') {
|
|
116
|
+
html += `<ins class="ec-chat-diff__added">${escapeHtml(text)}</ins>`;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
html += escapeHtml(text);
|
|
120
|
+
}
|
|
121
|
+
buffer = [];
|
|
122
|
+
};
|
|
123
|
+
for (const op of ops) {
|
|
124
|
+
if (op.type !== currentType) {
|
|
125
|
+
flush();
|
|
126
|
+
currentType = op.type;
|
|
127
|
+
}
|
|
128
|
+
buffer.push(op.text);
|
|
129
|
+
}
|
|
130
|
+
flush();
|
|
131
|
+
return html;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Tokenize text into words, preserving whitespace as separate tokens
|
|
135
|
+
* so the diff output is readable.
|
|
136
|
+
*/
|
|
137
|
+
function tokenize(text) {
|
|
138
|
+
return text.split(/(\s+)/).filter(Boolean);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Escape HTML special characters.
|
|
142
|
+
*/
|
|
143
|
+
function escapeHtml(text) {
|
|
144
|
+
return text
|
|
145
|
+
.replace(/&/g, '&')
|
|
146
|
+
.replace(/</g, '<')
|
|
147
|
+
.replace(/>/g, '>')
|
|
148
|
+
.replace(/"/g, '"');
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Human-readable label for a diff type.
|
|
152
|
+
*/
|
|
153
|
+
function formatDiffLabel(diffType) {
|
|
154
|
+
switch (diffType) {
|
|
155
|
+
case 'edit':
|
|
156
|
+
return 'Content edit';
|
|
157
|
+
case 'replace':
|
|
158
|
+
return 'Content replacement';
|
|
159
|
+
case 'insert':
|
|
160
|
+
return 'New content';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -21,7 +21,7 @@ export declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBo
|
|
|
21
21
|
static getDerivedStateFromError(error: Error): ErrorBoundaryState;
|
|
22
22
|
componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
|
|
23
23
|
reset: () => void;
|
|
24
|
-
render(): string | number | boolean |
|
|
24
|
+
render(): string | number | boolean | import("react/jsx-runtime").JSX.Element | Iterable<ReactNode> | null | undefined;
|
|
25
25
|
}
|
|
26
26
|
export {};
|
|
27
27
|
//# sourceMappingURL=ErrorBoundary.d.ts.map
|
|
@@ -2,6 +2,7 @@ export { ChatMessage, type ChatMessageProps } from './ChatMessage.tsx';
|
|
|
2
2
|
export { ChatMessages, type ChatMessagesProps } from './ChatMessages.tsx';
|
|
3
3
|
export { ChatInput, type ChatInputProps } from './ChatInput.tsx';
|
|
4
4
|
export { ToolMessage, type ToolMessageProps, type ToolGroup } from './ToolMessage.tsx';
|
|
5
|
+
export { DiffCard, type DiffCardProps, type DiffData } from './DiffCard.tsx';
|
|
5
6
|
export { TypingIndicator, type TypingIndicatorProps } from './TypingIndicator.tsx';
|
|
6
7
|
export { SessionSwitcher, type SessionSwitcherProps } from './SessionSwitcher.tsx';
|
|
7
8
|
export { ErrorBoundary, type ErrorBoundaryProps } from './ErrorBoundary.tsx';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/components/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { ChatMessage } from "./ChatMessage.js";
|
|
|
2
2
|
export { ChatMessages } from "./ChatMessages.js";
|
|
3
3
|
export { ChatInput } from "./ChatInput.js";
|
|
4
4
|
export { ToolMessage } from "./ToolMessage.js";
|
|
5
|
+
export { DiffCard } from "./DiffCard.js";
|
|
5
6
|
export { TypingIndicator } from "./TypingIndicator.js";
|
|
6
7
|
export { SessionSwitcher } from "./SessionSwitcher.js";
|
|
7
8
|
export { ErrorBoundary } from "./ErrorBoundary.js";
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ToolGroup } from './components/ToolMessage.tsx';
|
|
2
|
+
export type CanonicalDiffType = 'edit' | 'replace' | 'insert';
|
|
3
|
+
export type CanonicalDiffStatus = 'pending' | 'accepted' | 'rejected';
|
|
4
|
+
export interface CanonicalDiffItem {
|
|
5
|
+
blockIndex?: number;
|
|
6
|
+
originalContent?: string;
|
|
7
|
+
replacementContent?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface CanonicalDiffEditorData {
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface CanonicalDiffData {
|
|
13
|
+
diffId: string;
|
|
14
|
+
diffType: CanonicalDiffType;
|
|
15
|
+
originalContent: string;
|
|
16
|
+
replacementContent: string;
|
|
17
|
+
status?: CanonicalDiffStatus;
|
|
18
|
+
summary?: string;
|
|
19
|
+
items?: CanonicalDiffItem[];
|
|
20
|
+
position?: string;
|
|
21
|
+
insertionPoint?: string;
|
|
22
|
+
editor?: CanonicalDiffEditorData;
|
|
23
|
+
}
|
|
24
|
+
export declare function parseCanonicalDiff(value: unknown): CanonicalDiffData | null;
|
|
25
|
+
export declare function parseCanonicalDiffFromJson(json: string): CanonicalDiffData | null;
|
|
26
|
+
export declare function parseCanonicalDiffFromToolGroup(group: ToolGroup): CanonicalDiffData | null;
|
|
27
|
+
//# sourceMappingURL=diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAE9D,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;AAC9D,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;AAEtE,MAAM,WAAW,iBAAiB;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,uBAAuB;IACvC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,uBAAuB,CAAC;CACjC;AA0CD,wBAAgB,kBAAkB,CAAE,KAAK,EAAE,OAAO,GAAI,iBAAiB,GAAG,IAAI,CA0E7E;AAED,wBAAgB,0BAA0B,CAAE,IAAI,EAAE,MAAM,GAAI,iBAAiB,GAAG,IAAI,CAMnF;AAED,wBAAgB,+BAA+B,CAAE,KAAK,EAAE,SAAS,GAAI,iBAAiB,GAAG,IAAI,CAM5F"}
|
package/dist/diff.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function normalizeItem(item) {
|
|
5
|
+
if (!isRecord(item)) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const blockIndex = typeof item.blockIndex === 'number'
|
|
9
|
+
? item.blockIndex
|
|
10
|
+
: typeof item.block_index === 'number'
|
|
11
|
+
? item.block_index
|
|
12
|
+
: undefined;
|
|
13
|
+
const originalContent = typeof item.originalContent === 'string'
|
|
14
|
+
? item.originalContent
|
|
15
|
+
: typeof item.original_content === 'string'
|
|
16
|
+
? item.original_content
|
|
17
|
+
: undefined;
|
|
18
|
+
const replacementContent = typeof item.replacementContent === 'string'
|
|
19
|
+
? item.replacementContent
|
|
20
|
+
: typeof item.replacement_content === 'string'
|
|
21
|
+
? item.replacement_content
|
|
22
|
+
: undefined;
|
|
23
|
+
if (blockIndex === undefined && originalContent === undefined && replacementContent === undefined) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
blockIndex,
|
|
28
|
+
originalContent,
|
|
29
|
+
replacementContent,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function parseCanonicalDiff(value) {
|
|
33
|
+
if (!isRecord(value)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const container = isRecord(value.data) ? value.data : value;
|
|
37
|
+
const rawDiff = isRecord(container.diff) ? container.diff : container;
|
|
38
|
+
const diffId = typeof rawDiff.diffId === 'string'
|
|
39
|
+
? rawDiff.diffId
|
|
40
|
+
: typeof rawDiff.diff_id === 'string'
|
|
41
|
+
? rawDiff.diff_id
|
|
42
|
+
: typeof container.diff_id === 'string'
|
|
43
|
+
? container.diff_id
|
|
44
|
+
: '';
|
|
45
|
+
const diffType = rawDiff.diffType === 'replace' || rawDiff.diffType === 'insert'
|
|
46
|
+
? rawDiff.diffType
|
|
47
|
+
: rawDiff.diffType === 'edit'
|
|
48
|
+
? 'edit'
|
|
49
|
+
: rawDiff.diff_type === 'replace' || rawDiff.diff_type === 'insert'
|
|
50
|
+
? rawDiff.diff_type
|
|
51
|
+
: 'edit';
|
|
52
|
+
const originalContent = typeof rawDiff.originalContent === 'string'
|
|
53
|
+
? rawDiff.originalContent
|
|
54
|
+
: typeof rawDiff.original_content === 'string'
|
|
55
|
+
? rawDiff.original_content
|
|
56
|
+
: '';
|
|
57
|
+
const replacementContent = typeof rawDiff.replacementContent === 'string'
|
|
58
|
+
? rawDiff.replacementContent
|
|
59
|
+
: typeof rawDiff.replacement_content === 'string'
|
|
60
|
+
? rawDiff.replacement_content
|
|
61
|
+
: '';
|
|
62
|
+
if (!diffId && !originalContent && !replacementContent) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const itemsSource = Array.isArray(rawDiff.items)
|
|
66
|
+
? rawDiff.items
|
|
67
|
+
: Array.isArray(rawDiff.edits)
|
|
68
|
+
? rawDiff.edits
|
|
69
|
+
: Array.isArray(rawDiff.replacements)
|
|
70
|
+
? rawDiff.replacements
|
|
71
|
+
: undefined;
|
|
72
|
+
const items = itemsSource
|
|
73
|
+
?.map(normalizeItem)
|
|
74
|
+
.filter((item) => item !== null);
|
|
75
|
+
const summary = typeof rawDiff.summary === 'string'
|
|
76
|
+
? rawDiff.summary
|
|
77
|
+
: typeof container.message === 'string'
|
|
78
|
+
? container.message
|
|
79
|
+
: undefined;
|
|
80
|
+
const status = rawDiff.status === 'accepted' || rawDiff.status === 'rejected' || rawDiff.status === 'pending'
|
|
81
|
+
? rawDiff.status
|
|
82
|
+
: undefined;
|
|
83
|
+
return {
|
|
84
|
+
diffId,
|
|
85
|
+
diffType,
|
|
86
|
+
originalContent,
|
|
87
|
+
replacementContent,
|
|
88
|
+
status,
|
|
89
|
+
summary,
|
|
90
|
+
items: items && items.length > 0 ? items : undefined,
|
|
91
|
+
position: typeof rawDiff.position === 'string' ? rawDiff.position : undefined,
|
|
92
|
+
insertionPoint: typeof rawDiff.insertionPoint === 'string' ? rawDiff.insertionPoint : undefined,
|
|
93
|
+
editor: isRecord(rawDiff.editor) ? rawDiff.editor : undefined,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function parseCanonicalDiffFromJson(json) {
|
|
97
|
+
try {
|
|
98
|
+
return parseCanonicalDiff(JSON.parse(json));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function parseCanonicalDiffFromToolGroup(group) {
|
|
105
|
+
if (!group.resultMessage) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return parseCanonicalDiffFromJson(group.resultMessage.content);
|
|
109
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,14 +3,19 @@ export type { FetchFn, FetchOptions, ChatApiConfig, SendResult, ContinueResult,
|
|
|
3
3
|
export { sendMessage, continueResponse, listSessions, loadSession, deleteSession, } from './api.ts';
|
|
4
4
|
export { normalizeMessage, normalizeConversation, normalizeSession } from './normalizer.ts';
|
|
5
5
|
export { markdownToHtml } from './markdown.ts';
|
|
6
|
+
export { formatChatAsMarkdown, copyChatAsMarkdown } from './transcript.ts';
|
|
7
|
+
export { parseCanonicalDiff, parseCanonicalDiffFromJson, parseCanonicalDiffFromToolGroup, type CanonicalDiffData, type CanonicalDiffEditorData, type CanonicalDiffItem, type CanonicalDiffStatus, type CanonicalDiffType, } from './diff.ts';
|
|
8
|
+
export { getOrCreateClientContextRegistry, registerClientContextProvider, getClientContextMetadata, useClientContextMetadata, type ClientContextProvider, type ClientContextProviderSnapshot, type ClientContextSnapshot, type ClientContextRegistry, } from './client-context.ts';
|
|
6
9
|
export { ChatMessage as ChatMessageComponent, type ChatMessageProps, } from './components/ChatMessage.tsx';
|
|
7
10
|
export { ChatMessages, type ChatMessagesProps, } from './components/ChatMessages.tsx';
|
|
8
11
|
export { ChatInput, type ChatInputProps, } from './components/ChatInput.tsx';
|
|
12
|
+
export { CopyTranscriptButton, type CopyTranscriptButtonProps, } from './components/CopyTranscriptButton.tsx';
|
|
9
13
|
export { ToolMessage, type ToolMessageProps, type ToolGroup, } from './components/ToolMessage.tsx';
|
|
14
|
+
export { DiffCard, type DiffCardProps, type DiffData, } from './components/DiffCard.tsx';
|
|
10
15
|
export { TypingIndicator, type TypingIndicatorProps, } from './components/TypingIndicator.tsx';
|
|
11
16
|
export { SessionSwitcher, type SessionSwitcherProps, } from './components/SessionSwitcher.tsx';
|
|
12
17
|
export { ErrorBoundary, type ErrorBoundaryProps, } from './components/ErrorBoundary.tsx';
|
|
13
18
|
export { AvailabilityGate, type AvailabilityGateProps, } from './components/AvailabilityGate.tsx';
|
|
14
19
|
export { useChat, type UseChatOptions, type UseChatReturn, } from './hooks/useChat.ts';
|
|
15
|
-
export { Chat, type ChatProps } from './Chat.tsx';
|
|
20
|
+
export { Chat, type ChatProps, type ChatSessionUi } from './Chat.tsx';
|
|
16
21
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACX,WAAW,EACX,QAAQ,EACR,cAAc,EACd,eAAe,EACf,WAAW,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,UAAU,EACV,eAAe,GACf,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACjH,OAAO,EACN,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,aAAa,GACb,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAG5F,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C,OAAO,EACN,WAAW,IAAI,oBAAoB,EACnC,KAAK,gBAAgB,GACrB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACN,YAAY,EACZ,KAAK,iBAAiB,GACtB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EACN,SAAS,EACT,KAAK,cAAc,GACnB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACN,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,SAAS,GACd,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACN,eAAe,EACf,KAAK,oBAAoB,GACzB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACN,eAAe,EACf,KAAK,oBAAoB,GACzB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACN,aAAa,EACb,KAAK,kBAAkB,GACvB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACN,gBAAgB,EAChB,KAAK,qBAAqB,GAC1B,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EACN,OAAO,EACP,KAAK,cAAc,EACnB,KAAK,aAAa,GAClB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACX,WAAW,EACX,QAAQ,EACR,cAAc,EACd,eAAe,EACf,WAAW,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,UAAU,EACV,eAAe,GACf,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACjH,OAAO,EACN,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,aAAa,GACb,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAG5F,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAG3E,OAAO,EACN,kBAAkB,EAClB,0BAA0B,EAC1B,+BAA+B,EAC/B,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACtB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACN,gCAAgC,EAChC,6BAA6B,EAC7B,wBAAwB,EACxB,wBAAwB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,GAC1B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACN,WAAW,IAAI,oBAAoB,EACnC,KAAK,gBAAgB,GACrB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACN,YAAY,EACZ,KAAK,iBAAiB,GACtB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EACN,SAAS,EACT,KAAK,cAAc,GACnB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACN,oBAAoB,EACpB,KAAK,yBAAyB,GAC9B,MAAM,uCAAuC,CAAC;AAE/C,OAAO,EACN,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,SAAS,GACd,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACN,QAAQ,EACR,KAAK,aAAa,EAClB,KAAK,QAAQ,GACb,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACN,eAAe,EACf,KAAK,oBAAoB,GACzB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACN,eAAe,EACf,KAAK,oBAAoB,GACzB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACN,aAAa,EACb,KAAK,kBAAkB,GACvB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACN,gBAAgB,EAChB,KAAK,qBAAqB,GAC1B,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EACN,OAAO,EACP,KAAK,cAAc,EACnB,KAAK,aAAa,GAClB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,19 @@ export { sendMessage, continueResponse, listSessions, loadSession, deleteSession
|
|
|
3
3
|
export { normalizeMessage, normalizeConversation, normalizeSession } from "./normalizer.js";
|
|
4
4
|
// Markdown
|
|
5
5
|
export { markdownToHtml } from "./markdown.js";
|
|
6
|
+
// Transcript
|
|
7
|
+
export { formatChatAsMarkdown, copyChatAsMarkdown } from "./transcript.js";
|
|
8
|
+
// Diff helpers
|
|
9
|
+
export { parseCanonicalDiff, parseCanonicalDiffFromJson, parseCanonicalDiffFromToolGroup, } from "./diff.js";
|
|
10
|
+
// Client context
|
|
11
|
+
export { getOrCreateClientContextRegistry, registerClientContextProvider, getClientContextMetadata, useClientContextMetadata, } from "./client-context.js";
|
|
6
12
|
// Components
|
|
7
13
|
export { ChatMessage as ChatMessageComponent, } from "./components/ChatMessage.js";
|
|
8
14
|
export { ChatMessages, } from "./components/ChatMessages.js";
|
|
9
15
|
export { ChatInput, } from "./components/ChatInput.js";
|
|
16
|
+
export { CopyTranscriptButton, } from "./components/CopyTranscriptButton.js";
|
|
10
17
|
export { ToolMessage, } from "./components/ToolMessage.js";
|
|
18
|
+
export { DiffCard, } from "./components/DiffCard.js";
|
|
11
19
|
export { TypingIndicator, } from "./components/TypingIndicator.js";
|
|
12
20
|
export { SessionSwitcher, } from "./components/SessionSwitcher.js";
|
|
13
21
|
export { ErrorBoundary, } from "./components/ErrorBoundary.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../src/transcript.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,wBAAgB,oBAAoB,CAAE,QAAQ,EAAE,WAAW,EAAE,GAAI,MAAM,CA0BtE;AAED,wBAAsB,kBAAkB,CAAE,QAAQ,EAAE,WAAW,EAAE,GAAI,OAAO,CAAE,MAAM,CAAE,CAUrF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function formatChatAsMarkdown(messages) {
|
|
2
|
+
return messages
|
|
3
|
+
.filter((message) => {
|
|
4
|
+
if (message.role === 'system' || message.role === 'tool_call') {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
return true;
|
|
8
|
+
})
|
|
9
|
+
.map((message) => {
|
|
10
|
+
const timestamp = message.timestamp
|
|
11
|
+
? new Date(message.timestamp).toLocaleString()
|
|
12
|
+
: '';
|
|
13
|
+
const timestampStr = timestamp ? ` (${timestamp})` : '';
|
|
14
|
+
if (message.role === 'tool_result') {
|
|
15
|
+
const toolName = message.toolResult?.toolName ?? 'Tool';
|
|
16
|
+
const success = message.toolResult?.success;
|
|
17
|
+
const status = success === false ? 'FAILED' : 'SUCCESS';
|
|
18
|
+
return `**Tool Response (${toolName} - ${status})${timestampStr}:**\n${message.content}`;
|
|
19
|
+
}
|
|
20
|
+
const role = message.role === 'user' ? 'User' : 'Assistant';
|
|
21
|
+
return `**${role}${timestampStr}:**\n${message.content}`;
|
|
22
|
+
})
|
|
23
|
+
.join('\n\n---\n\n');
|
|
24
|
+
}
|
|
25
|
+
export async function copyChatAsMarkdown(messages) {
|
|
26
|
+
const markdown = formatChatAsMarkdown(messages);
|
|
27
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
|
|
28
|
+
throw new Error('Clipboard API is not available.');
|
|
29
|
+
}
|
|
30
|
+
await navigator.clipboard.writeText(markdown);
|
|
31
|
+
return markdown;
|
|
32
|
+
}
|
package/package.json
CHANGED
package/src/Chat.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import type { ChatMessage as ChatMessageType, ContentFormat } from './types/index.ts';
|
|
3
3
|
import type { FetchFn } from './api.ts';
|
|
4
|
+
import type { ToolGroup } from './components/ToolMessage.tsx';
|
|
4
5
|
import { useChat, type UseChatOptions } from './hooks/useChat.ts';
|
|
5
6
|
import { ErrorBoundary } from './components/ErrorBoundary.tsx';
|
|
6
7
|
import { AvailabilityGate } from './components/AvailabilityGate.tsx';
|
|
@@ -8,6 +9,10 @@ import { ChatMessages } from './components/ChatMessages.tsx';
|
|
|
8
9
|
import { ChatInput } from './components/ChatInput.tsx';
|
|
9
10
|
import { TypingIndicator } from './components/TypingIndicator.tsx';
|
|
10
11
|
import { SessionSwitcher } from './components/SessionSwitcher.tsx';
|
|
12
|
+
import { CopyTranscriptButton } from './components/CopyTranscriptButton.tsx';
|
|
13
|
+
import type { UseChatReturn } from './hooks/useChat.ts';
|
|
14
|
+
|
|
15
|
+
export type ChatSessionUi = 'list' | 'none';
|
|
11
16
|
|
|
12
17
|
export interface ChatProps {
|
|
13
18
|
/**
|
|
@@ -32,6 +37,14 @@ export interface ChatProps {
|
|
|
32
37
|
showTools?: boolean;
|
|
33
38
|
/** Map of tool function names to friendly display labels. */
|
|
34
39
|
toolNames?: Record<string, string>;
|
|
40
|
+
/**
|
|
41
|
+
* Custom renderers for specific tool names.
|
|
42
|
+
*
|
|
43
|
+
* Map tool function names to React render functions. When a tool group
|
|
44
|
+
* matches a registered name, the custom renderer is used instead of
|
|
45
|
+
* the default ToolMessage JSON display.
|
|
46
|
+
*/
|
|
47
|
+
toolRenderers?: Record<string, (group: ToolGroup) => ReactNode>;
|
|
35
48
|
/** Placeholder text for the input. */
|
|
36
49
|
placeholder?: string;
|
|
37
50
|
/** Content shown when conversation is empty. */
|
|
@@ -50,12 +63,27 @@ export interface ChatProps {
|
|
|
50
63
|
className?: string;
|
|
51
64
|
/** Whether to show the session switcher. Defaults to true. */
|
|
52
65
|
showSessions?: boolean;
|
|
66
|
+
/** Session UI mode. 'list' renders the built-in switcher, 'none' lets the consumer render its own. */
|
|
67
|
+
sessionUi?: ChatSessionUi;
|
|
53
68
|
/** Label shown during multi-turn processing. */
|
|
54
69
|
processingLabel?: (turnCount: number) => string;
|
|
55
70
|
/** Whether to show the attachment button in the input. Defaults to true. */
|
|
56
71
|
allowAttachments?: boolean;
|
|
57
72
|
/** Accepted file types for attachments. Defaults to 'image/*,video/*'. */
|
|
58
73
|
acceptFileTypes?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Arbitrary metadata forwarded to the backend with each message.
|
|
76
|
+
* Use for client-side context injection (e.g. `{ client_context: { tab: 'compose', postId: 123 } }`).
|
|
77
|
+
*/
|
|
78
|
+
metadata?: Record<string, unknown>;
|
|
79
|
+
/** Whether to show a built-in copy transcript button. Defaults to false. */
|
|
80
|
+
showCopyTranscript?: boolean;
|
|
81
|
+
/** Label for the built-in copy transcript button. */
|
|
82
|
+
copyTranscriptLabel?: string;
|
|
83
|
+
/** Label shown after the transcript is copied. */
|
|
84
|
+
copyTranscriptCopiedLabel?: string;
|
|
85
|
+
/** Optional custom header/actions area rendered above messages with live chat state. */
|
|
86
|
+
renderHeader?: ( chat: UseChatReturn ) => ReactNode;
|
|
59
87
|
}
|
|
60
88
|
|
|
61
89
|
/**
|
|
@@ -89,6 +117,7 @@ export function Chat({
|
|
|
89
117
|
renderContent,
|
|
90
118
|
showTools = true,
|
|
91
119
|
toolNames,
|
|
120
|
+
toolRenderers,
|
|
92
121
|
placeholder,
|
|
93
122
|
emptyState,
|
|
94
123
|
initialMessages,
|
|
@@ -98,9 +127,15 @@ export function Chat({
|
|
|
98
127
|
onMessage,
|
|
99
128
|
className,
|
|
100
129
|
showSessions = true,
|
|
130
|
+
sessionUi = 'list',
|
|
101
131
|
processingLabel,
|
|
102
132
|
allowAttachments = true,
|
|
103
133
|
acceptFileTypes,
|
|
134
|
+
metadata,
|
|
135
|
+
showCopyTranscript = false,
|
|
136
|
+
copyTranscriptLabel,
|
|
137
|
+
copyTranscriptCopiedLabel,
|
|
138
|
+
renderHeader,
|
|
104
139
|
}: ChatProps) {
|
|
105
140
|
const chat = useChat({
|
|
106
141
|
basePath,
|
|
@@ -111,6 +146,7 @@ export function Chat({
|
|
|
111
146
|
maxContinueTurns,
|
|
112
147
|
onError,
|
|
113
148
|
onMessage,
|
|
149
|
+
metadata,
|
|
114
150
|
});
|
|
115
151
|
|
|
116
152
|
const baseClass = 'ec-chat';
|
|
@@ -120,7 +156,19 @@ export function Chat({
|
|
|
120
156
|
<ErrorBoundary onError={onError ? (err) => onError(err) : undefined}>
|
|
121
157
|
<div className={classes}>
|
|
122
158
|
<AvailabilityGate availability={chat.availability}>
|
|
123
|
-
{
|
|
159
|
+
{renderHeader?.( chat )}
|
|
160
|
+
|
|
161
|
+
{showCopyTranscript && (
|
|
162
|
+
<div className="ec-chat__actions">
|
|
163
|
+
<CopyTranscriptButton
|
|
164
|
+
messages={chat.messages}
|
|
165
|
+
label={copyTranscriptLabel}
|
|
166
|
+
copiedLabel={copyTranscriptCopiedLabel}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{showSessions && sessionUi === 'list' && (
|
|
124
172
|
<SessionSwitcher
|
|
125
173
|
sessions={chat.sessions}
|
|
126
174
|
activeSessionId={chat.sessionId ?? undefined}
|
|
@@ -131,14 +179,15 @@ export function Chat({
|
|
|
131
179
|
/>
|
|
132
180
|
)}
|
|
133
181
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
182
|
+
<ChatMessages
|
|
183
|
+
messages={chat.messages}
|
|
184
|
+
contentFormat={contentFormat}
|
|
185
|
+
renderContent={renderContent}
|
|
186
|
+
showTools={showTools}
|
|
187
|
+
toolNames={toolNames}
|
|
188
|
+
toolRenderers={toolRenderers}
|
|
189
|
+
emptyState={emptyState}
|
|
190
|
+
/>
|
|
142
191
|
|
|
143
192
|
<TypingIndicator
|
|
144
193
|
visible={chat.isLoading}
|