@greenstones/qui-ai 0.0.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/.storybook/main.js +25 -0
- package/.storybook/preview.js +13 -0
- package/index.ts +1 -0
- package/package.json +41 -0
- package/src/chat/AssistantButton.tsx +95 -0
- package/src/chat/ChatForm.css +12 -0
- package/src/chat/ChatForm.tsx +201 -0
- package/src/chat/ChatMessages.tsx +83 -0
- package/src/chat/ChatPage.tsx +33 -0
- package/src/chat/ChatPanel.tsx +133 -0
- package/src/chat/MessageItem.ts +3 -0
- package/src/chat/TextContent.tsx +15 -0
- package/src/chat/index.ts +8 -0
- package/src/chat/messages/AssistantMessage.tsx +28 -0
- package/src/chat/messages/ChatMessage.tsx +135 -0
- package/src/chat/messages/FunctionCall.tsx +65 -0
- package/src/chat/messages/FunctionCallResult.tsx +22 -0
- package/src/chat/messages/UserMessage.tsx +28 -0
- package/src/chat/use-chat.tsx +404 -0
- package/src/chat/widgets/BarChartWidget.tsx +63 -0
- package/src/chat/widgets/PieChartWidget.tsx +44 -0
- package/src/index.ts +4 -0
- package/src/md/BootstrapStyles.tsx +37 -0
- package/src/md/MarkdownContent.tsx +29 -0
- package/src/md/index.ts +2 -0
- package/src/session/LocalSession.tsx +45 -0
- package/src/session/index.ts +2 -0
- package/src/session/use-session.tsx +6 -0
- package/src/use-model.tsx +58 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/stringify.ts +7 -0
- package/src/utils/usage.ts +11 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { join, dirname } from "path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This function is used to resolve the absolute path of a package.
|
|
5
|
+
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
|
|
6
|
+
*/
|
|
7
|
+
function getAbsolutePath(value) {
|
|
8
|
+
return dirname(require.resolve(join(value, "package.json")));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
|
12
|
+
const config = {
|
|
13
|
+
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
|
14
|
+
addons: [
|
|
15
|
+
getAbsolutePath("@storybook/addon-onboarding"),
|
|
16
|
+
getAbsolutePath("@storybook/addon-essentials"),
|
|
17
|
+
getAbsolutePath("@chromatic-com/storybook"),
|
|
18
|
+
getAbsolutePath("@storybook/addon-interactions"),
|
|
19
|
+
],
|
|
20
|
+
framework: {
|
|
21
|
+
name: getAbsolutePath("@storybook/react-vite"),
|
|
22
|
+
options: {},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
export default config;
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src";
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@greenstones/qui-ai",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"lint": "eslint .",
|
|
9
|
+
"storybook": "storybook dev -p 6006",
|
|
10
|
+
"build-storybook": "storybook build"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@greenstones/qui-bootstrap": "0.1.0",
|
|
14
|
+
"@greenstones/qui-core": "0.1.0-alpha.1",
|
|
15
|
+
"chroma-js": "^3.2.0",
|
|
16
|
+
"classnames": "^2.5.1",
|
|
17
|
+
"lucide-react": "^0.500",
|
|
18
|
+
"react-markdown": "^10.1.0",
|
|
19
|
+
"recharts": "^3",
|
|
20
|
+
"rehype-raw": "^7",
|
|
21
|
+
"remark-gfm": "^4"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/react": "^19",
|
|
25
|
+
"@types/react-dom": "^19"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"date-fns": "^4.1",
|
|
29
|
+
"react": "^19",
|
|
30
|
+
"react-dom": "^19",
|
|
31
|
+
"react-bootstrap": "^2.10",
|
|
32
|
+
"react-router-dom": "^7",
|
|
33
|
+
"react-hook-form": "^7",
|
|
34
|
+
"@ai-sdk/amazon-bedrock": "^4",
|
|
35
|
+
"@ai-sdk/anthropic": "^3",
|
|
36
|
+
"@ai-sdk/openai": "^3",
|
|
37
|
+
"@openai/agents": "^0.7",
|
|
38
|
+
"@openai/agents-extensions": "^0.7"
|
|
39
|
+
},
|
|
40
|
+
"sideEffects": false
|
|
41
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Bot, ChevronDown } from "lucide-react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Button, Offcanvas, OverlayTrigger, Tooltip } from "react-bootstrap";
|
|
4
|
+
import { ChatPanel, EmptyPlaceholder } from "./ChatPanel";
|
|
5
|
+
import { type Chat } from "./use-chat";
|
|
6
|
+
|
|
7
|
+
export const AssistantButton = ({
|
|
8
|
+
chat,
|
|
9
|
+
type = "window",
|
|
10
|
+
}: {
|
|
11
|
+
chat: Chat;
|
|
12
|
+
type?: "window" | "offcanvas";
|
|
13
|
+
}) => {
|
|
14
|
+
const [show, setShow] = useState(false);
|
|
15
|
+
const handleClose = () => setShow(false);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div>
|
|
19
|
+
<div style={{ position: "absolute", bottom: "1.2rem", right: "1.2rem" }}>
|
|
20
|
+
<OverlayTrigger
|
|
21
|
+
overlay={<Tooltip>Open Assistant</Tooltip>}
|
|
22
|
+
placement="left"
|
|
23
|
+
>
|
|
24
|
+
<Button
|
|
25
|
+
variant="primary"
|
|
26
|
+
onClick={(e) => {
|
|
27
|
+
setShow((s) => !s);
|
|
28
|
+
}}
|
|
29
|
+
style={{ borderRadius: 32, padding: 12 }}
|
|
30
|
+
>
|
|
31
|
+
{!show ? (
|
|
32
|
+
<Bot width={24} height={24} />
|
|
33
|
+
) : (
|
|
34
|
+
<ChevronDown width={24} height={24} />
|
|
35
|
+
)}
|
|
36
|
+
</Button>
|
|
37
|
+
</OverlayTrigger>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
{type === "offcanvas" && (
|
|
41
|
+
<Offcanvas
|
|
42
|
+
show={show}
|
|
43
|
+
onHide={handleClose}
|
|
44
|
+
placement="end"
|
|
45
|
+
scroll={true}
|
|
46
|
+
backdrop={false}
|
|
47
|
+
className="shadow"
|
|
48
|
+
>
|
|
49
|
+
<Offcanvas.Header closeButton>
|
|
50
|
+
<Offcanvas.Title>Assistant</Offcanvas.Title>
|
|
51
|
+
</Offcanvas.Header>
|
|
52
|
+
<Offcanvas.Body className="flex-fill d-flex flex-column">
|
|
53
|
+
<ChatPanel
|
|
54
|
+
chat={chat}
|
|
55
|
+
examples={[
|
|
56
|
+
"What’s the weather like in Frankfurt?",
|
|
57
|
+
"Create a table listing the current weather in the 5 largest German cities",
|
|
58
|
+
]}
|
|
59
|
+
userMessagePosition="end"
|
|
60
|
+
centerOnEmpty={false}
|
|
61
|
+
chatMessagesClassName="gap-1"
|
|
62
|
+
empty={<EmptyPlaceholder className="text-secondary" />}
|
|
63
|
+
/>
|
|
64
|
+
</Offcanvas.Body>
|
|
65
|
+
</Offcanvas>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
{type === "window" && show && (
|
|
69
|
+
<div
|
|
70
|
+
className="bg-white shadow-lg border rounded-4 p-3 d-flex flex-column"
|
|
71
|
+
style={{
|
|
72
|
+
height: "50%",
|
|
73
|
+
minHeight: 600,
|
|
74
|
+
position: "absolute",
|
|
75
|
+
bottom: "5.4rem",
|
|
76
|
+
right: "1.2rem",
|
|
77
|
+
width: "28rem",
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<ChatPanel
|
|
81
|
+
chat={chat}
|
|
82
|
+
examples={[
|
|
83
|
+
"What’s the weather like in Frankfurt?",
|
|
84
|
+
"Create a table listing the current weather in the 5 largest German cities",
|
|
85
|
+
]}
|
|
86
|
+
userMessagePosition="end"
|
|
87
|
+
centerOnEmpty={false}
|
|
88
|
+
chatMessagesClassName="gap-1"
|
|
89
|
+
empty={<EmptyPlaceholder className="text-secondary" />}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import classNames from "classnames";
|
|
2
|
+
import { ArrowUp, Loader, Trash, WandSparkles } from "lucide-react";
|
|
3
|
+
import { useRef, type ReactNode, type Ref } from "react";
|
|
4
|
+
import { Button, ButtonToolbar, Dropdown, Form } from "react-bootstrap";
|
|
5
|
+
import { useForm } from "react-hook-form";
|
|
6
|
+
import type { Chat } from "./use-chat";
|
|
7
|
+
import "./ChatForm.css";
|
|
8
|
+
|
|
9
|
+
export interface ChatFormProps {
|
|
10
|
+
chat: Chat;
|
|
11
|
+
className?: string;
|
|
12
|
+
tools?: ReactNode;
|
|
13
|
+
examples?: string[];
|
|
14
|
+
variant?: undefined | "bordered" | "standard";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ChatForm({
|
|
18
|
+
chat,
|
|
19
|
+
examples,
|
|
20
|
+
tools,
|
|
21
|
+
className,
|
|
22
|
+
variant,
|
|
23
|
+
}: ChatFormProps) {
|
|
24
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
25
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
26
|
+
|
|
27
|
+
const { register, handleSubmit, setValue } = useForm<{ message: string }>({
|
|
28
|
+
defaultValues: { message: "" },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const field = register("message", { required: true });
|
|
32
|
+
const onSubmit = async (data) => {
|
|
33
|
+
const msg = data.message;
|
|
34
|
+
if (msg) {
|
|
35
|
+
chat.callChat(msg, {
|
|
36
|
+
monitor: (state: string) => {
|
|
37
|
+
if (state === "start") {
|
|
38
|
+
inputRef.current!.value = "";
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
46
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
if (!chat.isRunning) formRef.current?.requestSubmit();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let variantProps;
|
|
53
|
+
switch (variant) {
|
|
54
|
+
case "bordered":
|
|
55
|
+
variantProps = {
|
|
56
|
+
mainClassName: "border rounded-4 px-3 py-3 mb-3 shadow-sm",
|
|
57
|
+
inputStyle: {
|
|
58
|
+
border: "none",
|
|
59
|
+
borderRadius: 0,
|
|
60
|
+
padding: 0,
|
|
61
|
+
minHeight: "auto",
|
|
62
|
+
boxShadow: "none",
|
|
63
|
+
background: "none",
|
|
64
|
+
marginTop: ".375rem",
|
|
65
|
+
},
|
|
66
|
+
rows: 1,
|
|
67
|
+
};
|
|
68
|
+
break;
|
|
69
|
+
|
|
70
|
+
default:
|
|
71
|
+
variantProps = {
|
|
72
|
+
mainClassName: "mt-1",
|
|
73
|
+
inputStyle: undefined,
|
|
74
|
+
rows: 2,
|
|
75
|
+
//minHeight: "1rem",
|
|
76
|
+
};
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(variantProps);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className={classNames(variantProps.mainClassName, className)}>
|
|
84
|
+
<form
|
|
85
|
+
className="d-grid gap-2"
|
|
86
|
+
ref={formRef}
|
|
87
|
+
onSubmit={handleSubmit(onSubmit)}
|
|
88
|
+
>
|
|
89
|
+
<div className="px-0">
|
|
90
|
+
<Form.Group className="" controlId="message">
|
|
91
|
+
<Form.Control
|
|
92
|
+
style={variantProps.inputStyle}
|
|
93
|
+
as="textarea"
|
|
94
|
+
{...{
|
|
95
|
+
...field,
|
|
96
|
+
ref: ((r: Ref<HTMLTextAreaElement>) => {
|
|
97
|
+
field.ref?.(r);
|
|
98
|
+
inputRef.current = r as any;
|
|
99
|
+
return;
|
|
100
|
+
}) as any,
|
|
101
|
+
}}
|
|
102
|
+
autoFocus
|
|
103
|
+
rows={variantProps.rows}
|
|
104
|
+
onKeyDown={handleKeyDown}
|
|
105
|
+
onInput={autoresize(inputRef)}
|
|
106
|
+
// placeholder={
|
|
107
|
+
// history.length > 0
|
|
108
|
+
// ? "Type your message or use ↑/↓ to browse input history"
|
|
109
|
+
// : "Type your message..."
|
|
110
|
+
// }
|
|
111
|
+
placeholder={"Type your message..."}
|
|
112
|
+
/>
|
|
113
|
+
</Form.Group>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<ButtonToolbar className="gap-2">
|
|
117
|
+
<Dropdown className="">
|
|
118
|
+
<Dropdown.Toggle variant="light" size="sm">
|
|
119
|
+
<WandSparkles size={16} />
|
|
120
|
+
</Dropdown.Toggle>
|
|
121
|
+
<Dropdown.Menu>
|
|
122
|
+
{examples?.map((c) => (
|
|
123
|
+
<Dropdown.Item
|
|
124
|
+
key={c}
|
|
125
|
+
onClick={() => {
|
|
126
|
+
setValue("message", c);
|
|
127
|
+
formRef.current?.requestSubmit();
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{c}
|
|
131
|
+
</Dropdown.Item>
|
|
132
|
+
))}
|
|
133
|
+
</Dropdown.Menu>
|
|
134
|
+
</Dropdown>
|
|
135
|
+
|
|
136
|
+
<Button
|
|
137
|
+
variant={"light"}
|
|
138
|
+
title="Clear"
|
|
139
|
+
onClick={(e) => {
|
|
140
|
+
chat.clearMessages();
|
|
141
|
+
}}
|
|
142
|
+
size="sm"
|
|
143
|
+
>
|
|
144
|
+
<Trash size={16} />
|
|
145
|
+
</Button>
|
|
146
|
+
|
|
147
|
+
{tools}
|
|
148
|
+
|
|
149
|
+
<Button
|
|
150
|
+
type="submit"
|
|
151
|
+
variant="primary"
|
|
152
|
+
size="sm"
|
|
153
|
+
className="ms-auto"
|
|
154
|
+
disabled={chat.isRunning}
|
|
155
|
+
>
|
|
156
|
+
{!chat.isRunning && (
|
|
157
|
+
<ArrowUp
|
|
158
|
+
color="white"
|
|
159
|
+
size={18}
|
|
160
|
+
strokeWidth={3}
|
|
161
|
+
style={{ marginTop: -2 }}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
164
|
+
{chat.isRunning && (
|
|
165
|
+
<Loader
|
|
166
|
+
color="white"
|
|
167
|
+
size={18}
|
|
168
|
+
strokeWidth={3}
|
|
169
|
+
style={{ marginTop: -2 }}
|
|
170
|
+
className="spin"
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
173
|
+
</Button>
|
|
174
|
+
</ButtonToolbar>
|
|
175
|
+
</form>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function _autoresize(ref: React.RefObject<HTMLTextAreaElement | null>) {
|
|
181
|
+
const el = ref.current;
|
|
182
|
+
if (el) {
|
|
183
|
+
el.style.height = "auto";
|
|
184
|
+
el.style.height = Math.min(el.scrollHeight, 250) + "px";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function autoresize(ref: React.RefObject<HTMLTextAreaElement | null>) {
|
|
189
|
+
return () => {
|
|
190
|
+
_autoresize(ref);
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function waitAndAutoresize(ref: React.RefObject<HTMLTextAreaElement | null>) {
|
|
195
|
+
const el = ref.current;
|
|
196
|
+
if (el) {
|
|
197
|
+
setTimeout(() => {
|
|
198
|
+
_autoresize(ref);
|
|
199
|
+
}, 100);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AssistantMessageItem,
|
|
3
|
+
FunctionCallItem,
|
|
4
|
+
FunctionCallResultItem,
|
|
5
|
+
UnknownItem,
|
|
6
|
+
UserMessageItem,
|
|
7
|
+
} from "@openai/agents";
|
|
8
|
+
import { Fragment, type ReactNode } from "react";
|
|
9
|
+
import type { MessageItem } from "./MessageItem";
|
|
10
|
+
|
|
11
|
+
export function ChatMessages({
|
|
12
|
+
messages,
|
|
13
|
+
className,
|
|
14
|
+
renderUserMessage,
|
|
15
|
+
renderAssistantMessage,
|
|
16
|
+
renderFunctionCall,
|
|
17
|
+
}: {
|
|
18
|
+
messages: MessageItem[];
|
|
19
|
+
className: string;
|
|
20
|
+
renderUserMessage: (message: UserMessageItem) => ReactNode;
|
|
21
|
+
renderAssistantMessage: (message: AssistantMessageItem) => ReactNode;
|
|
22
|
+
renderFunctionCall?: (
|
|
23
|
+
call: FunctionCallItem,
|
|
24
|
+
result?: FunctionCallResultItem,
|
|
25
|
+
) => ReactNode;
|
|
26
|
+
}) {
|
|
27
|
+
const functionResultItems = messages
|
|
28
|
+
.filter((m) => isFunctionCallResult(m))
|
|
29
|
+
.reduce(
|
|
30
|
+
(p, c) => {
|
|
31
|
+
p[c.callId] = c;
|
|
32
|
+
return p;
|
|
33
|
+
},
|
|
34
|
+
{} as Record<string, FunctionCallResultItem>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
function renderMessage(m: MessageItem) {
|
|
38
|
+
if (isUserMessage(m)) {
|
|
39
|
+
return renderUserMessage(m);
|
|
40
|
+
}
|
|
41
|
+
if (isAssistantMessage(m)) {
|
|
42
|
+
return renderAssistantMessage(m);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (isFunctionCall(m)) {
|
|
46
|
+
return renderFunctionCall?.(m, functionResultItems[m.callId]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className={className}>
|
|
54
|
+
{messages.map((m, index) => (
|
|
55
|
+
<Fragment key={index}>{renderMessage(m)}</Fragment>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const isUserMessage = (m: MessageItem): m is UserMessageItem => {
|
|
62
|
+
return (m as UserMessageItem).role === "user";
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const isAssistantMessage = (
|
|
66
|
+
m: MessageItem,
|
|
67
|
+
): m is AssistantMessageItem => {
|
|
68
|
+
return (m as AssistantMessageItem).role === "assistant";
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const isFunctionCallResult = (
|
|
72
|
+
m: MessageItem,
|
|
73
|
+
): m is FunctionCallResultItem => {
|
|
74
|
+
return (m as FunctionCallResultItem).type === "function_call_result";
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const isFunctionCall = (m: MessageItem): m is FunctionCallItem => {
|
|
78
|
+
return (m as FunctionCallItem).type === "function_call";
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const isUnknown = (m: MessageItem): m is UnknownItem => {
|
|
82
|
+
return (m as UnknownItem).type === "unknown";
|
|
83
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Page, type PageOptions } from "@greenstones/qui-bootstrap";
|
|
2
|
+
import { ChatPanel, EmptyPlaceholder, type ChatPanelProps } from "./ChatPanel";
|
|
3
|
+
|
|
4
|
+
export function ChatPage({
|
|
5
|
+
chat,
|
|
6
|
+
empty = <EmptyPlaceholder />,
|
|
7
|
+
examples,
|
|
8
|
+
tools,
|
|
9
|
+
variant,
|
|
10
|
+
formClassName,
|
|
11
|
+
userMessagePosition,
|
|
12
|
+
userMessageClassName,
|
|
13
|
+
children,
|
|
14
|
+
...pageOptions
|
|
15
|
+
}: ChatPanelProps & PageOptions) {
|
|
16
|
+
return (
|
|
17
|
+
<Page {...pageOptions} scrollBody={false}>
|
|
18
|
+
<ChatPanel
|
|
19
|
+
{...{
|
|
20
|
+
chat,
|
|
21
|
+
userMessagePosition,
|
|
22
|
+
userMessageClassName,
|
|
23
|
+
empty,
|
|
24
|
+
formClassName,
|
|
25
|
+
tools,
|
|
26
|
+
examples,
|
|
27
|
+
variant,
|
|
28
|
+
}}
|
|
29
|
+
/>
|
|
30
|
+
{children}
|
|
31
|
+
</Page>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import classNames from "classnames";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { Spinner } from "react-bootstrap";
|
|
4
|
+
import { BootstrapStylesComponents } from "../md/BootstrapStyles";
|
|
5
|
+
import { MarkdownContent } from "../md/MarkdownContent";
|
|
6
|
+
import { ChatForm } from "./ChatForm";
|
|
7
|
+
import { ChatMessages } from "./ChatMessages";
|
|
8
|
+
import { AssistantMessage } from "./messages/AssistantMessage";
|
|
9
|
+
import { FunctionCall } from "./messages/FunctionCall";
|
|
10
|
+
import { UserMessage } from "./messages/UserMessage";
|
|
11
|
+
import { MessageContent } from "./TextContent";
|
|
12
|
+
import type { Chat } from "./use-chat";
|
|
13
|
+
|
|
14
|
+
export interface ChatPanelProps {
|
|
15
|
+
chat: Chat;
|
|
16
|
+
empty?: ReactNode;
|
|
17
|
+
examples?: string[];
|
|
18
|
+
tools?: ReactNode;
|
|
19
|
+
variant?: undefined | "bordered" | "standard";
|
|
20
|
+
formClassName?: string;
|
|
21
|
+
userMessagePosition?: undefined | "start" | "end";
|
|
22
|
+
userMessageClassName?: string;
|
|
23
|
+
centerOnEmpty?: boolean;
|
|
24
|
+
chatMessagesClassName?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ChatPanel({
|
|
28
|
+
chat,
|
|
29
|
+
userMessagePosition,
|
|
30
|
+
userMessageClassName,
|
|
31
|
+
empty,
|
|
32
|
+
formClassName,
|
|
33
|
+
tools,
|
|
34
|
+
examples,
|
|
35
|
+
variant,
|
|
36
|
+
centerOnEmpty = true,
|
|
37
|
+
chatMessagesClassName = "gap-3",
|
|
38
|
+
}: ChatPanelProps) {
|
|
39
|
+
const isEmptyChat = !chat.messages || chat.messages.length === 0;
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex-fill d-flex flex-column" style={{ height: 1 }}>
|
|
42
|
+
{!isEmptyChat && (
|
|
43
|
+
<div className="overflow-auto d-flex flex-column flex-fill">
|
|
44
|
+
<ChatMessages
|
|
45
|
+
className={classNames("d-flex flex-column", chatMessagesClassName)}
|
|
46
|
+
messages={chat.messages}
|
|
47
|
+
renderUserMessage={(message) => (
|
|
48
|
+
<UserMessage
|
|
49
|
+
message={message}
|
|
50
|
+
renderContent={(c) => (
|
|
51
|
+
<MessageContent
|
|
52
|
+
content={c}
|
|
53
|
+
wrapperClassName={classNames("d-flex", {
|
|
54
|
+
"justify-content-end": userMessagePosition === "end",
|
|
55
|
+
})}
|
|
56
|
+
className={
|
|
57
|
+
userMessageClassName || "bg-light py-3 px-3 rounded-4 "
|
|
58
|
+
}
|
|
59
|
+
/>
|
|
60
|
+
)}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
renderAssistantMessage={(message) => (
|
|
64
|
+
<AssistantMessage
|
|
65
|
+
message={message}
|
|
66
|
+
renderContent={(c) => (
|
|
67
|
+
<MarkdownContent
|
|
68
|
+
content={c}
|
|
69
|
+
wrapperClassName=""
|
|
70
|
+
components={{
|
|
71
|
+
...BootstrapStylesComponents,
|
|
72
|
+
...chat.widgets,
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
renderFunctionCall={(call, result) => (
|
|
79
|
+
<FunctionCall call={call} result={result} />
|
|
80
|
+
)}
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
{chat.isRunning && (
|
|
84
|
+
<Spinner
|
|
85
|
+
className="ms-2 mt-3"
|
|
86
|
+
animation="grow"
|
|
87
|
+
variant="secondary"
|
|
88
|
+
size="sm"
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
<div
|
|
92
|
+
ref={chat.endRef}
|
|
93
|
+
style={{ height: "10rem", minHeight: "10rem" }}
|
|
94
|
+
>
|
|
95
|
+
{" "}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{isEmptyChat && !centerOnEmpty && empty && (
|
|
101
|
+
<div className="flex-fill1 my-auto">{empty}</div>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
<div
|
|
105
|
+
className={classNames("", {
|
|
106
|
+
" my-auto ": centerOnEmpty && isEmptyChat,
|
|
107
|
+
})}
|
|
108
|
+
>
|
|
109
|
+
{isEmptyChat && centerOnEmpty && empty}
|
|
110
|
+
<ChatForm
|
|
111
|
+
chat={chat}
|
|
112
|
+
className={formClassName}
|
|
113
|
+
tools={tools}
|
|
114
|
+
examples={examples}
|
|
115
|
+
variant={variant}
|
|
116
|
+
/>
|
|
117
|
+
{centerOnEmpty && isEmptyChat && (
|
|
118
|
+
<div style={{ height: "20rem" }}></div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function EmptyPlaceholder({ className }: { className?: string }) {
|
|
126
|
+
return (
|
|
127
|
+
<div>
|
|
128
|
+
<div className={classNames("h4 fw-normal text-center mb-4", className)}>
|
|
129
|
+
How can I help you today?
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function MessageContent({
|
|
2
|
+
content,
|
|
3
|
+
wrapperClassName,
|
|
4
|
+
className,
|
|
5
|
+
}: {
|
|
6
|
+
content: string;
|
|
7
|
+
wrapperClassName?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
}) {
|
|
10
|
+
return (
|
|
11
|
+
<div className={wrapperClassName}>
|
|
12
|
+
<div className={className}>{content}</div>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|