Skip to content

Commit 41e6333

Browse files
authored
refactor: embeddable script support message annotations (#113)
* update deps * refactor: embeddable script support annotation
1 parent bb3792a commit 41e6333

File tree

12 files changed

+1909
-5561
lines changed

12 files changed

+1909
-5561
lines changed

embeddable-javascript/.yarnrc.yml

-1
This file was deleted.

embeddable-javascript/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
},
3030
"devDependencies": {
3131
"@types/node": "^20.11.10",
32+
"@types/prop-types": "^15.7.12",
3233
"@types/react": "^18.2.48",
3334
"@types/react-dom": "^18.2.18",
3435
"@types/react-syntax-highlighter": "^15",
@@ -41,8 +42,8 @@
4142
"eslint-plugin-react-refresh": "^0.4.5",
4243
"postcss": "^8.4.33",
4344
"prettier": "^3.2.4",
45+
"prop-types": "^15.8.1",
4446
"typescript": "^5.2.2",
4547
"vite": "^5.0.8"
46-
},
47-
"packageManager": "[email protected]"
48+
}
4849
}

embeddable-javascript/src/components/Chatbot/ChatContainer.tsx

+2-38
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
22
import { styled, css } from '@mui/system';
33
import { useChat } from 'ai/react';
4+
import { withReCaptcha } from '../../lib/withCaptcha.ts';
45

56
import { ChatItem, ChatItemLoading, ExampleQuestions } from './ChatItem';
67
import { ChatActionBar, ChatItemActionBar } from './ChatActionBar';
@@ -16,41 +17,6 @@ declare module 'ai/react' {
1617
}
1718
}
1819

19-
declare const grecaptcha: {
20-
ready: (cb: () => void) => void;
21-
execute: (siteKey: string, options: { action: string }) => Promise<string>;
22-
enterprise: {
23-
ready: (cb: () => void) => void;
24-
execute: (siteKey: string, options: { action: string }) => Promise<string>;
25-
};
26-
};
27-
28-
async function withReCaptcha(
29-
options: {
30-
action: string;
31-
siteKey: string;
32-
mode?: 'v3' | 'enterprise';
33-
},
34-
func: (data: { action: string; siteKey: string; token: string }) => void
35-
) {
36-
const { action, siteKey } = options;
37-
// skip if no siteKey
38-
if (!siteKey) {
39-
return func({ action, siteKey, token: '' });
40-
}
41-
if (options.mode === 'v3') {
42-
grecaptcha.ready(async () => {
43-
const token = await grecaptcha.execute(siteKey, { action });
44-
func({ action, siteKey, token });
45-
});
46-
} else if (options.mode === 'enterprise') {
47-
grecaptcha.enterprise.ready(async () => {
48-
const token = await grecaptcha.enterprise.execute(siteKey, { action });
49-
func({ action, siteKey, token });
50-
});
51-
}
52-
}
53-
5420
export default function ChatContainer(props: {
5521
exampleQuestions: string[];
5622
inputPlaceholder?: string;
@@ -145,9 +111,7 @@ export default function ChatContainer(props: {
145111
items={exampleQuestions}
146112
/>
147113
)}
148-
{isLoading && messages[messages.length - 1]?.role !== 'assistant' && (
149-
<ChatItemLoading />
150-
)}
114+
{isLoading && <ChatItemLoading annotations={messages[messages.length - 1]?.annotations ?? [] as any} />}
151115
{messages
152116
.sort((a, b) => {
153117
const aTime = new Date(a.createdAt || 0).getTime();

embeddable-javascript/src/components/Chatbot/ChatItem.tsx

+149-71
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,31 @@
1+
import { LoaderIcon } from 'lucide-react';
2+
import { useContext, useMemo } from 'react';
13
import * as React from 'react';
24
import { styled, css, Theme, SxProps } from '@mui/system';
5+
import { CfgContext } from '../../context';
36

47
import { MDMessage } from './Markdown';
58
import { a11yDark, a11yLight } from '../../theme/code';
69

10+
export type AppChatStreamSource = { title: string, uri: string };
11+
12+
export const enum AppChatStreamState {
13+
CONNECTING = 'CONNECTING', // only client side
14+
CREATING = 'CREATING',
15+
SEARCHING = 'SEARCHING',
16+
RERANKING = 'RERANKING',
17+
GENERATING = 'GENERATING',
18+
FINISHED = 'FINISHED',
19+
ERROR = 'ERROR',
20+
}
21+
22+
export type MyChatMessageAnnotation = {
23+
context?: AppChatStreamSource[],
24+
state?: AppChatStreamState,
25+
stateMessage?: string
26+
};
27+
28+
729
export function ChatItem(props: {
830
role?: 'user' | 'bot';
931
children: string;
@@ -50,7 +72,42 @@ export function ChatItem(props: {
5072
);
5173
}
5274

53-
export function ChatItemLoading() {
75+
function getHost (baseUrl: string) {
76+
try {
77+
return new URL(baseUrl).hostname;
78+
} catch {
79+
return location.hostname;
80+
}
81+
}
82+
83+
export function ChatItemLoading({ annotations }: { annotations: MyChatMessageAnnotation[] }) {
84+
const annotation = useMemo(() => {
85+
return getChatMessageAnnotations(annotations);
86+
}, [annotations])
87+
88+
const { baseUrl } = useContext(CfgContext);
89+
let text: string;
90+
switch (annotation.state) {
91+
case undefined:
92+
case 'CONNECTING':
93+
text = `Connecting to ${getHost(baseUrl)}...`;
94+
break;
95+
case 'CREATING':
96+
text = 'Preparing to ask...';
97+
break;
98+
case 'SEARCHING':
99+
text = 'Gathering resources...';
100+
break;
101+
case 'RERANKING':
102+
text = 'Reranking resources...';
103+
break;
104+
case 'GENERATING':
105+
text = 'Generating answer...';
106+
break;
107+
default:
108+
return null;
109+
}
110+
54111
return (
55112
<StyledChatItemRow
56113
role='bot'
@@ -59,48 +116,57 @@ export function ChatItemLoading() {
59116
}}
60117
>
61118
<StyledChatItem role='bot'>
62-
<StyledChatMutedText>Gathering Resources...</StyledChatMutedText>
63-
<StyledTextSkeleton />
119+
<StyledChatMutedText>
120+
<StyledLoaderIcon sx={{
121+
animation: "spin 2s linear infinite",
122+
"@keyframes spin": {
123+
"0%": {
124+
transform: "rotate(360deg)",
125+
},
126+
"100%": {
127+
transform: "rotate(0deg)",
128+
},
129+
}
130+
}} />
131+
{text}
132+
</StyledChatMutedText>
64133
</StyledChatItem>
65134
</StyledChatItemRow>
66135
);
67136
}
68137

69-
// export function ChatItemRelatedLinkCard(props: { title: string; uri: string }) {
70-
// const { title, uri } = props;
138+
export function ChatItemRelatedLinkCard(props: { title: string; uri: string }) {
139+
const { title, uri } = props;
71140

72-
// const hostnameMemo = React.useMemo(() => {
73-
// try {
74-
// return new URL(uri).hostname;
75-
// } catch {
76-
// return '';
77-
// }
78-
// }, [uri]);
141+
const hostnameMemo = React.useMemo(() => {
142+
try {
143+
return new URL(uri).hostname;
144+
} catch {
145+
return '';
146+
}
147+
}, [uri]);
79148

80-
// return (
81-
// <StyledChatItemLinkCard href={uri} target='_blank'>
82-
// <div>{title}</div>
83-
// <div>{hostnameMemo}</div>
84-
// </StyledChatItemLinkCard>
85-
// );
86-
// }
149+
return (
150+
<StyledChatItemLinkCard href={uri} target='_blank'>
151+
<div>{title}</div>
152+
<div>{hostnameMemo}</div>
153+
</StyledChatItemLinkCard>
154+
);
155+
}
87156

88-
// export function ChatItemRelatedLinkCardWrapper(props: {
89-
// items: {
90-
// title: string;
91-
// uri: string;
92-
// }[];
93-
// }) {
94-
// const { items } = props;
157+
export function ChatItemRelatedLinkCardWrapper(props: {
158+
items: AppChatStreamSource[];
159+
}) {
160+
const { items } = props;
95161

96-
// return (
97-
// <StyledChatItemLinkCardWrapper>
98-
// {items.map((item, index) => (
99-
// <ChatItemRelatedLinkCard key={index} {...item} />
100-
// ))}
101-
// </StyledChatItemLinkCardWrapper>
102-
// );
103-
// }
162+
return (
163+
<StyledChatItemLinkCardWrapper>
164+
{items.map((item, index) => (
165+
<ChatItemRelatedLinkCard key={index} {...item} />
166+
))}
167+
</StyledChatItemLinkCardWrapper>
168+
);
169+
}
104170

105171
export function ExampleQuestions(props: {
106172
items: string[];
@@ -161,7 +227,9 @@ export const StyledChatItem = styled('div')(({ theme, role }) =>
161227

162228
export const StyledChatMutedText = styled('div')(({ theme }) =>
163229
css({
164-
display: 'block',
230+
display: 'flex',
231+
alignItems: 'center',
232+
gap: '4px',
165233
fontSize: '12px',
166234
color: theme.palette.mutedForeground,
167235
'& > a': {
@@ -170,49 +238,54 @@ export const StyledChatMutedText = styled('div')(({ theme }) =>
170238
})
171239
);
172240

173-
export const StyledTextSkeleton = styled('div')(({ theme }) =>
174-
css({
175-
backgroundColor: theme.palette.muted,
176-
color: theme.palette.primary,
177-
borderRadius: theme.shape.borderRadius,
178-
padding: '0.5rem 0.75rem',
179-
display: 'inline-block',
180-
animation: 'animation-breath 1.5s infinite',
181-
'@keyframes animation-breath': {
182-
'0%': {
183-
opacity: 1,
184-
},
185-
'50%': {
186-
opacity: 0.4,
187-
},
188-
'100%': {
189-
opacity: 1,
190-
},
191-
},
192-
})
193-
);
241+
const StyledLoaderIcon = styled(LoaderIcon)`
242+
width: 1em;
243+
height: 1em;
244+
`
194245

195-
// export const StyledChatItemLinkCard = styled('a')(({ theme }) =>
246+
// export const StyledTextSkeleton = styled('div')(({ theme }) =>
196247
// css({
197-
// display: 'block',
198-
// padding: '0.5rem',
199-
// borderRadius: theme.shape.borderRadius,
200248
// backgroundColor: theme.palette.muted,
201-
// color: theme.palette.accentForeground,
202-
// '&:hover': {
203-
// backgroundColor: theme.palette.muted,
204-
// color: theme.palette.accentForeground,
249+
// color: theme.palette.primary,
250+
// borderRadius: theme.shape.borderRadius,
251+
// padding: '0.5rem 0.75rem',
252+
// display: 'inline-block',
253+
// animation: 'animation-breath 1.5s infinite',
254+
// '@keyframes animation-breath': {
255+
// '0%': {
256+
// opacity: 1,
257+
// },
258+
// '50%': {
259+
// opacity: 0.4,
260+
// },
261+
// '100%': {
262+
// opacity: 1,
263+
// },
205264
// },
206265
// })
207266
// );
208267

209-
// export const StyledChatItemLinkCardWrapper = styled('div')(() =>
210-
// css({
211-
// display: 'flex',
212-
// gapX: '0.5rem',
213-
// overflowX: 'auto',
214-
// })
215-
// );
268+
export const StyledChatItemLinkCard = styled('a')(({ theme }) =>
269+
css({
270+
display: 'block',
271+
padding: '0.5rem',
272+
borderRadius: theme.shape.borderRadius,
273+
backgroundColor: theme.palette.muted,
274+
color: theme.palette.accentForeground,
275+
'&:hover': {
276+
backgroundColor: theme.palette.muted,
277+
color: theme.palette.accentForeground,
278+
},
279+
})
280+
);
281+
282+
export const StyledChatItemLinkCardWrapper = styled('div')(() =>
283+
css({
284+
display: 'flex',
285+
gapX: '0.5rem',
286+
overflowX: 'auto',
287+
})
288+
);
216289

217290
export const StyledExampleQuestionItem = styled('div')(({ theme }) =>
218291
css({
@@ -273,3 +346,8 @@ export const StyledExampleQuestionWrapper = styled('div')(() =>
273346
// }),
274347
// })
275348
// );
349+
350+
function getChatMessageAnnotations (annotations: MyChatMessageAnnotation[] | undefined) {
351+
return ((annotations ?? []) as MyChatMessageAnnotation[])
352+
.reduce((annotation, next) => Object.assign(annotation, next), {});
353+
}

0 commit comments

Comments
 (0)