@nospt/backstage-plugin-librechat 0.1.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/README.md +109 -0
- package/dist/api.esm.js +90 -0
- package/dist/api.esm.js.map +1 -0
- package/dist/components/ChatBubble.esm.js +69 -0
- package/dist/components/ChatBubble.esm.js.map +1 -0
- package/dist/components/ChatMessage.esm.js +139 -0
- package/dist/components/ChatMessage.esm.js.map +1 -0
- package/dist/components/ChatPanel.esm.js +288 -0
- package/dist/components/ChatPanel.esm.js.map +1 -0
- package/dist/components/SettingsTab.esm.js +161 -0
- package/dist/components/SettingsTab.esm.js.map +1 -0
- package/dist/hooks/useLibreChatSettings.esm.js +50 -0
- package/dist/hooks/useLibreChatSettings.esm.js.map +1 -0
- package/dist/hooks/usePageContext.esm.js +54 -0
- package/dist/hooks/usePageContext.esm.js.map +1 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.esm.js +3 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/plugin.esm.js +28 -0
- package/dist/plugin.esm.js.map +1 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @nospt/backstage-plugin-librechat
|
|
2
|
+
|
|
3
|
+
> Frontend plugin that adds an AI chat bubble to your Backstage app, powered by the [LibreChat](https://www.librechat.ai/) Agents API.
|
|
4
|
+
|
|
5
|
+
This is the **frontend** half of the LibreChat plugin. It renders a floating chat bubble across every page of your Backstage app, captures the context of the page you're viewing, streams responses in real time, and renders them as Markdown.
|
|
6
|
+
|
|
7
|
+
It must be paired with the backend plugin, [`@nospt/backstage-plugin-librechat-backend`](../librechat-backend/README.md), which proxies requests to LibreChat and keeps your API keys server-side.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Floating chat bubble** — persistent AI chat overlay on every Backstage page.
|
|
12
|
+
- **Page context awareness** — automatically shares the current page title, path, and URL with the agent.
|
|
13
|
+
- **Streaming responses** — real-time SSE streaming from the LibreChat Agents API.
|
|
14
|
+
- **Markdown rendering** — full GitHub Flavored Markdown (code blocks, tables, lists).
|
|
15
|
+
- **In-chat settings** — users can supply their own API key, validate it, and override the default.
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
- A Backstage app using the **[new frontend system](https://backstage.io/docs/frontend-system/)** (`@backstage/frontend-defaults`).
|
|
20
|
+
- The backend plugin [`@nospt/backstage-plugin-librechat-backend`](../librechat-backend/README.md) installed and configured.
|
|
21
|
+
- A running [LibreChat](https://www.librechat.ai/) instance with the Agents API enabled.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Install the package in your Backstage app package (e.g. `packages/app`):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
yarn --cwd packages/app add @nospt/backstage-plugin-librechat
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
Register the plugin as a feature in your app entry point. With the new frontend system it is discovered automatically when listed in `package.json`, but you can also add it explicitly:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// packages/app/src/App.tsx
|
|
37
|
+
import {createApp} from "@backstage/frontend-defaults";
|
|
38
|
+
import libreChatPlugin from "@nospt/backstage-plugin-librechat";
|
|
39
|
+
|
|
40
|
+
const app = createApp({
|
|
41
|
+
features: [libreChatPlugin],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export default app.createRoot();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The plugin contributes an app-root overlay (the chat bubble), so there is no route or page to mount — once registered, the bubble appears in the bottom-right corner of every page.
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
All configuration for both the frontend and backend plugins lives under the `librechat` key in `app-config.yaml`:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
librechat:
|
|
55
|
+
# Required: URL of your LibreChat instance (used by frontend and backend)
|
|
56
|
+
baseUrl: https://your-librechat-instance.com
|
|
57
|
+
# Required: agent ID configured in LibreChat (used as the completion model)
|
|
58
|
+
agentId: agent_abc123
|
|
59
|
+
# Optional: default API key — users can override it in the chat Settings
|
|
60
|
+
apiKey: your-librechat-api-key
|
|
61
|
+
# Optional: display name shown in the chat header and message labels (default: "AI")
|
|
62
|
+
name: NOS-GPT
|
|
63
|
+
# Optional: show/hide the chat bubble (default: true)
|
|
64
|
+
enabled: true
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| Setting | Required | Visibility | Description |
|
|
68
|
+
| --------- | -------- | ---------- | ------------------------------------------------------------------- |
|
|
69
|
+
| `baseUrl` | ✅ | frontend | URL of your LibreChat instance |
|
|
70
|
+
| `agentId` | ✅ | backend | Agent ID configured in LibreChat, used as the completion model |
|
|
71
|
+
| `apiKey` | ❌ | secret | Default API key; users can override it per-request from the chat UI |
|
|
72
|
+
| `name` | ❌ | frontend | Display name in the chat header and messages (default: `AI`) |
|
|
73
|
+
| `enabled` | ❌ | frontend | Show or hide the chat bubble (default: `true`) |
|
|
74
|
+
|
|
75
|
+
> [!NOTE]
|
|
76
|
+
> `agentId` and `apiKey` are only read server-side by the backend plugin and are never exposed to the browser. `baseUrl`, `name`, and `enabled` are visible to the frontend.
|
|
77
|
+
|
|
78
|
+
## Endpoints
|
|
79
|
+
|
|
80
|
+
The backend plugin mounts under `/api/librechat`:
|
|
81
|
+
|
|
82
|
+
| Method | Path | Description |
|
|
83
|
+
| ------ | -------- | ------------------------------------------------------------------------- |
|
|
84
|
+
| `POST` | `/chat` | Proxies a chat completion to LibreChat and streams the SSE response back. |
|
|
85
|
+
| `POST` | `/check` | Validates an API key by sending a short test message to LibreChat. |
|
|
86
|
+
|
|
87
|
+
Both accept optional `x-librechat-api-key` and `x-librechat-agent-id` headers to override the configured defaults.
|
|
88
|
+
|
|
89
|
+
## How it works
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
Frontend bubble → POST /api/librechat/chat → LibreChat /api/agents/v1/chat/completions → SSE stream back
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The backend resolves the API key (a user-supplied header takes precedence over the configured default), validates the request, sanitizes the agent ID, and proxies it to `POST {baseUrl}/api/agents/v1/chat/completions`. The streamed response is piped back to the frontend untouched, where it is rendered as Markdown in real time.
|
|
96
|
+
|
|
97
|
+
## Usage
|
|
98
|
+
|
|
99
|
+
1. Open any page in Backstage.
|
|
100
|
+
2. Click the **chat bubble** in the bottom-right corner.
|
|
101
|
+
3. Open **Settings** to enter your LibreChat API key (or use the default configured by your admin).
|
|
102
|
+
4. Click the **check** button to validate your key — a confirmation message appears in the chat.
|
|
103
|
+
5. Start chatting — the AI automatically receives context about the page you're viewing.
|
|
104
|
+
|
|
105
|
+
User settings (the API key) are stored in the browser via Backstage's Storage API.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
Apache-2.0. Made with ❤️ by [NOS Inovação](https://github.com/nosportugal).
|
package/dist/api.esm.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createApiRef } from '@backstage/frontend-plugin-api';
|
|
2
|
+
|
|
3
|
+
const libreChatApiRef = createApiRef({
|
|
4
|
+
id: "plugin.librechat.api"
|
|
5
|
+
});
|
|
6
|
+
class DefaultLibreChatApi {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.fetchApi = options.fetchApi;
|
|
9
|
+
this.configApi = options.configApi;
|
|
10
|
+
}
|
|
11
|
+
get backendBaseUrl() {
|
|
12
|
+
return this.configApi.getString("backend.baseUrl").replace(/\/+$/, "");
|
|
13
|
+
}
|
|
14
|
+
async checkApiKey(apiKey) {
|
|
15
|
+
const response = await this.fetchApi.fetch(
|
|
16
|
+
`${this.backendBaseUrl}/api/librechat/check`,
|
|
17
|
+
{
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
...apiKey ? { "x-librechat-api-key": apiKey } : {}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
const data2 = await response.json().catch(() => ({}));
|
|
27
|
+
throw new Error(
|
|
28
|
+
data2.error ?? `Backend returned ${response.status}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const data = await response.json();
|
|
32
|
+
return data.reply;
|
|
33
|
+
}
|
|
34
|
+
async *sendMessage(messages, options) {
|
|
35
|
+
const response = await this.fetchApi.fetch(
|
|
36
|
+
`${this.backendBaseUrl}/api/librechat/chat`,
|
|
37
|
+
{
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: {
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
...options?.apiKey ? { "x-librechat-api-key": options.apiKey } : {}
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({ messages })
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
48
|
+
throw new Error(
|
|
49
|
+
errorBody.error ?? `Backend returned ${response.status}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const reader = response.body?.getReader();
|
|
53
|
+
if (!reader) {
|
|
54
|
+
throw new Error("No response stream available");
|
|
55
|
+
}
|
|
56
|
+
const decoder = new TextDecoder();
|
|
57
|
+
let buffer = "";
|
|
58
|
+
try {
|
|
59
|
+
while (true) {
|
|
60
|
+
const { done, value } = await reader.read();
|
|
61
|
+
if (done) break;
|
|
62
|
+
buffer += decoder.decode(value, { stream: true });
|
|
63
|
+
const lines = buffer.split("\n");
|
|
64
|
+
buffer = lines.pop() ?? "";
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
const trimmed = line.trim();
|
|
67
|
+
if (trimmed === "data: [DONE]") {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (trimmed.startsWith("data: ")) {
|
|
71
|
+
const jsonStr = trimmed.slice(6);
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(jsonStr);
|
|
74
|
+
const content = parsed?.choices?.[0]?.delta?.content;
|
|
75
|
+
if (typeof content === "string" && content.length > 0) {
|
|
76
|
+
yield content;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} finally {
|
|
84
|
+
reader.releaseLock();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { DefaultLibreChatApi, libreChatApiRef };
|
|
90
|
+
//# sourceMappingURL=api.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.esm.js","sources":["../src/api.ts"],"sourcesContent":["import {\n createApiRef,\n FetchApi,\n ConfigApi,\n} from \"@backstage/frontend-plugin-api\";\n\n/** A single chat message. */\nexport interface ChatMessage {\n role: \"user\" | \"assistant\" | \"system\";\n content: string;\n}\n\n/**\n * API for communicating with LibreChat through the backend proxy.\n *\n * @public\n */\nexport interface LibreChatApi {\n /**\n * Sends messages to LibreChat and yields streamed content chunks.\n *\n * @param messages - Conversation history\n * @param options - Optional override for apiKey\n * @returns An async generator yielding content strings as they arrive\n */\n sendMessage(\n messages: ChatMessage[],\n options?: {apiKey?: string},\n ): AsyncGenerator<string, void, unknown>;\n\n /**\n * Checks that the given API key is valid by sending a short test message.\n *\n * @param apiKey - API key to validate\n * @returns The assistant's reply\n */\n checkApiKey(apiKey: string): Promise<string>;\n}\n\n/**\n * API reference for the LibreChat API.\n *\n * @public\n */\nexport const libreChatApiRef = createApiRef<LibreChatApi>({\n id: \"plugin.librechat.api\",\n});\n\n/**\n * Default implementation of the LibreChat API.\n * Calls the backend proxy and parses the SSE stream.\n *\n * @internal\n */\nexport class DefaultLibreChatApi implements LibreChatApi {\n private readonly fetchApi: FetchApi;\n private readonly configApi: ConfigApi;\n\n constructor(options: {fetchApi: FetchApi; configApi: ConfigApi}) {\n this.fetchApi = options.fetchApi;\n this.configApi = options.configApi;\n }\n\n private get backendBaseUrl(): string {\n return this.configApi.getString(\"backend.baseUrl\").replace(/\\/+$/, \"\");\n }\n\n async checkApiKey(apiKey: string): Promise<string> {\n const response = await this.fetchApi.fetch(\n `${this.backendBaseUrl}/api/librechat/check`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(apiKey ? {\"x-librechat-api-key\": apiKey} : {}),\n },\n },\n );\n\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(\n (data as {error?: string}).error ??\n `Backend returned ${response.status}`,\n );\n }\n\n const data = (await response.json()) as {ok: boolean; reply: string};\n return data.reply;\n }\n\n async *sendMessage(\n messages: ChatMessage[],\n options?: {apiKey?: string},\n ): AsyncGenerator<string, void, unknown> {\n const response = await this.fetchApi.fetch(\n `${this.backendBaseUrl}/api/librechat/chat`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(options?.apiKey ? {\"x-librechat-api-key\": options.apiKey} : {}),\n },\n body: JSON.stringify({messages}),\n },\n );\n\n if (!response.ok) {\n const errorBody = await response.json().catch(() => ({}));\n throw new Error(\n (errorBody as {error?: string}).error ??\n `Backend returned ${response.status}`,\n );\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n throw new Error(\"No response stream available\");\n }\n\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n try {\n while (true) {\n const {done, value} = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, {stream: true});\n\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() ?? \"\";\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n if (trimmed === \"data: [DONE]\") {\n return;\n }\n\n if (trimmed.startsWith(\"data: \")) {\n const jsonStr = trimmed.slice(6);\n try {\n const parsed = JSON.parse(jsonStr);\n const content = parsed?.choices?.[0]?.delta?.content;\n if (typeof content === \"string\" && content.length > 0) {\n yield content;\n }\n } catch {\n // Skip malformed JSON lines\n }\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n}\n"],"names":["data"],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { makeStyles } from '@material-ui/core/styles';
|
|
4
|
+
import Fab from '@material-ui/core/Fab';
|
|
5
|
+
import Paper from '@material-ui/core/Paper';
|
|
6
|
+
import Slide from '@material-ui/core/Slide';
|
|
7
|
+
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
|
|
8
|
+
import ChatIcon from '@material-ui/icons/Chat';
|
|
9
|
+
import CloseIcon from '@material-ui/icons/Close';
|
|
10
|
+
import { useApi, configApiRef } from '@backstage/frontend-plugin-api';
|
|
11
|
+
import { ChatPanel } from './ChatPanel.esm.js';
|
|
12
|
+
|
|
13
|
+
const CHAT_WIDTH = 400;
|
|
14
|
+
const CHAT_HEIGHT = 560;
|
|
15
|
+
const useStyles = makeStyles((theme) => ({
|
|
16
|
+
container: {
|
|
17
|
+
position: "fixed",
|
|
18
|
+
bottom: theme.spacing(3),
|
|
19
|
+
right: theme.spacing(3),
|
|
20
|
+
zIndex: theme.zIndex.tooltip + 1
|
|
21
|
+
},
|
|
22
|
+
fab: {
|
|
23
|
+
boxShadow: theme.shadows[6]
|
|
24
|
+
},
|
|
25
|
+
panel: {
|
|
26
|
+
position: "fixed",
|
|
27
|
+
bottom: theme.spacing(3) + 56 + 12,
|
|
28
|
+
// fab height + gap
|
|
29
|
+
right: theme.spacing(3),
|
|
30
|
+
width: CHAT_WIDTH,
|
|
31
|
+
height: CHAT_HEIGHT,
|
|
32
|
+
maxHeight: "calc(100vh - 120px)",
|
|
33
|
+
borderRadius: 12,
|
|
34
|
+
overflow: "hidden",
|
|
35
|
+
display: "flex",
|
|
36
|
+
flexDirection: "column",
|
|
37
|
+
zIndex: theme.zIndex.tooltip + 1,
|
|
38
|
+
boxShadow: theme.shadows[16]
|
|
39
|
+
}
|
|
40
|
+
}));
|
|
41
|
+
function useIsEnabled() {
|
|
42
|
+
const configApi = useApi(configApiRef);
|
|
43
|
+
return configApi.getOptionalBoolean("librechat.enabled") ?? true;
|
|
44
|
+
}
|
|
45
|
+
function ChatBubble() {
|
|
46
|
+
const classes = useStyles();
|
|
47
|
+
const enabled = useIsEnabled();
|
|
48
|
+
const [open, setOpen] = useState(false);
|
|
49
|
+
if (!enabled) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return /* @__PURE__ */ jsx(ClickAwayListener, { onClickAway: () => {
|
|
53
|
+
}, children: /* @__PURE__ */ jsxs("div", { children: [
|
|
54
|
+
/* @__PURE__ */ jsx(Slide, { direction: "up", in: open, mountOnEnter: true, unmountOnExit: true, children: /* @__PURE__ */ jsx(Paper, { className: classes.panel, elevation: 16, children: /* @__PURE__ */ jsx(ChatPanel, {}) }) }),
|
|
55
|
+
/* @__PURE__ */ jsx("div", { className: classes.container, children: /* @__PURE__ */ jsx(
|
|
56
|
+
Fab,
|
|
57
|
+
{
|
|
58
|
+
color: "primary",
|
|
59
|
+
className: classes.fab,
|
|
60
|
+
onClick: () => setOpen((prev) => !prev),
|
|
61
|
+
"aria-label": open ? "Close chat" : "Open chat",
|
|
62
|
+
children: open ? /* @__PURE__ */ jsx(CloseIcon, {}) : /* @__PURE__ */ jsx(ChatIcon, {})
|
|
63
|
+
}
|
|
64
|
+
) })
|
|
65
|
+
] }) });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { ChatBubble };
|
|
69
|
+
//# sourceMappingURL=ChatBubble.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatBubble.esm.js","sources":["../../src/components/ChatBubble.tsx"],"sourcesContent":["import React, {useState} from \"react\";\nimport {makeStyles, Theme} from \"@material-ui/core/styles\";\nimport Fab from \"@material-ui/core/Fab\";\nimport Paper from \"@material-ui/core/Paper\";\nimport Slide from \"@material-ui/core/Slide\";\nimport ClickAwayListener from \"@material-ui/core/ClickAwayListener\";\nimport ChatIcon from \"@material-ui/icons/Chat\";\nimport CloseIcon from \"@material-ui/icons/Close\";\nimport {useApi, configApiRef} from \"@backstage/frontend-plugin-api\";\nimport {ChatPanel} from \"./ChatPanel\";\n\nconst CHAT_WIDTH = 400;\nconst CHAT_HEIGHT = 560;\n\nconst useStyles = makeStyles((theme: Theme) => ({\n container: {\n position: \"fixed\",\n bottom: theme.spacing(3),\n right: theme.spacing(3),\n zIndex: theme.zIndex.tooltip + 1,\n },\n fab: {\n boxShadow: theme.shadows[6],\n },\n panel: {\n position: \"fixed\",\n bottom: theme.spacing(3) + 56 + 12, // fab height + gap\n right: theme.spacing(3),\n width: CHAT_WIDTH,\n height: CHAT_HEIGHT,\n maxHeight: \"calc(100vh - 120px)\",\n borderRadius: 12,\n overflow: \"hidden\",\n display: \"flex\",\n flexDirection: \"column\",\n zIndex: theme.zIndex.tooltip + 1,\n boxShadow: theme.shadows[16],\n },\n}));\n\nfunction useIsEnabled(): boolean {\n const configApi = useApi(configApiRef);\n return configApi.getOptionalBoolean(\"librechat.enabled\") ?? true;\n}\n\nexport function ChatBubble() {\n const classes = useStyles();\n const enabled = useIsEnabled();\n const [open, setOpen] = useState(false);\n\n if (!enabled) {\n return null;\n }\n\n return (\n <ClickAwayListener onClickAway={() => {}}>\n <div>\n <Slide direction=\"up\" in={open} mountOnEnter unmountOnExit>\n <Paper className={classes.panel} elevation={16}>\n <ChatPanel />\n </Paper>\n </Slide>\n\n <div className={classes.container}>\n <Fab\n color=\"primary\"\n className={classes.fab}\n onClick={() => setOpen((prev) => !prev)}\n aria-label={open ? \"Close chat\" : \"Open chat\"}\n >\n {open ? <CloseIcon /> : <ChatIcon />}\n </Fab>\n </div>\n </div>\n </ClickAwayListener>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAWA,MAAM,UAAA,GAAa,GAAA;AACnB,MAAM,WAAA,GAAc,GAAA;AAEpB,MAAM,SAAA,GAAY,UAAA,CAAW,CAAC,KAAA,MAAkB;AAAA,EAC9C,SAAA,EAAW;AAAA,IACT,QAAA,EAAU,OAAA;AAAA,IACV,MAAA,EAAQ,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAAA,IACvB,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAAA,IACtB,MAAA,EAAQ,KAAA,CAAM,MAAA,CAAO,OAAA,GAAU;AAAA,GACjC;AAAA,EACA,GAAA,EAAK;AAAA,IACH,SAAA,EAAW,KAAA,CAAM,OAAA,CAAQ,CAAC;AAAA,GAC5B;AAAA,EACA,KAAA,EAAO;AAAA,IACL,QAAA,EAAU,OAAA;AAAA,IACV,MAAA,EAAQ,KAAA,CAAM,OAAA,CAAQ,CAAC,IAAI,EAAA,GAAK,EAAA;AAAA;AAAA,IAChC,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAAA,IACtB,KAAA,EAAO,UAAA;AAAA,IACP,MAAA,EAAQ,WAAA;AAAA,IACR,SAAA,EAAW,qBAAA;AAAA,IACX,YAAA,EAAc,EAAA;AAAA,IACd,QAAA,EAAU,QAAA;AAAA,IACV,OAAA,EAAS,MAAA;AAAA,IACT,aAAA,EAAe,QAAA;AAAA,IACf,MAAA,EAAQ,KAAA,CAAM,MAAA,CAAO,OAAA,GAAU,CAAA;AAAA,IAC/B,SAAA,EAAW,KAAA,CAAM,OAAA,CAAQ,EAAE;AAAA;AAE/B,CAAA,CAAE,CAAA;AAEF,SAAS,YAAA,GAAwB;AAC/B,EAAA,MAAM,SAAA,GAAY,OAAO,YAAY,CAAA;AACrC,EAAA,OAAO,SAAA,CAAU,kBAAA,CAAmB,mBAAmB,CAAA,IAAK,IAAA;AAC9D;AAEO,SAAS,UAAA,GAAa;AAC3B,EAAA,MAAM,UAAU,SAAA,EAAU;AAC1B,EAAA,MAAM,UAAU,YAAA,EAAa;AAC7B,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAI,SAAS,KAAK,CAAA;AAEtC,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,uBACE,GAAA,CAAC,iBAAA,EAAA,EAAkB,WAAA,EAAa,MAAM;AAAA,EAAC,CAAA,EACrC,+BAAC,KAAA,EAAA,EACC,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,SAAM,SAAA,EAAU,IAAA,EAAK,IAAI,IAAA,EAAM,YAAA,EAAY,MAAC,aAAA,EAAa,IAAA,EACxD,8BAAC,KAAA,EAAA,EAAM,SAAA,EAAW,QAAQ,KAAA,EAAO,SAAA,EAAW,IAC1C,QAAA,kBAAA,GAAA,CAAC,SAAA,EAAA,EAAU,GACb,CAAA,EACF,CAAA;AAAA,oBAEA,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAA,CAAQ,SAAA,EACtB,QAAA,kBAAA,GAAA;AAAA,MAAC,GAAA;AAAA,MAAA;AAAA,QACC,KAAA,EAAM,SAAA;AAAA,QACN,WAAW,OAAA,CAAQ,GAAA;AAAA,QACnB,SAAS,MAAM,OAAA,CAAQ,CAAC,IAAA,KAAS,CAAC,IAAI,CAAA;AAAA,QACtC,YAAA,EAAY,OAAO,YAAA,GAAe,WAAA;AAAA,QAEjC,QAAA,EAAA,IAAA,mBAAO,GAAA,CAAC,SAAA,EAAA,EAAU,CAAA,uBAAM,QAAA,EAAA,EAAS;AAAA;AAAA,KACpC,EACF;AAAA,GAAA,EACF,CAAA,EACF,CAAA;AAEJ;;;;"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import ReactMarkdown from 'react-markdown';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import { makeStyles } from '@material-ui/core/styles';
|
|
5
|
+
import Typography from '@material-ui/core/Typography';
|
|
6
|
+
|
|
7
|
+
const useStyles = makeStyles((theme) => ({
|
|
8
|
+
root: {
|
|
9
|
+
display: "flex",
|
|
10
|
+
flexDirection: "column",
|
|
11
|
+
marginBottom: theme.spacing(1.5)
|
|
12
|
+
},
|
|
13
|
+
userRow: {
|
|
14
|
+
alignItems: "flex-end"
|
|
15
|
+
},
|
|
16
|
+
assistantRow: {
|
|
17
|
+
alignItems: "flex-start"
|
|
18
|
+
},
|
|
19
|
+
label: {
|
|
20
|
+
fontSize: "0.7rem",
|
|
21
|
+
fontWeight: 600,
|
|
22
|
+
textTransform: "uppercase",
|
|
23
|
+
color: theme.palette.text.secondary,
|
|
24
|
+
marginBottom: 2,
|
|
25
|
+
paddingLeft: theme.spacing(0.5),
|
|
26
|
+
paddingRight: theme.spacing(0.5)
|
|
27
|
+
},
|
|
28
|
+
bubble: {
|
|
29
|
+
maxWidth: "85%",
|
|
30
|
+
padding: theme.spacing(1, 1.5),
|
|
31
|
+
borderRadius: 12,
|
|
32
|
+
wordBreak: "break-word",
|
|
33
|
+
"& p": {
|
|
34
|
+
margin: 0
|
|
35
|
+
},
|
|
36
|
+
"& p + p": {
|
|
37
|
+
marginTop: theme.spacing(1)
|
|
38
|
+
},
|
|
39
|
+
"& pre": {
|
|
40
|
+
background: theme.palette.type === "dark" ? "#1e1e1e" : "#f5f5f5",
|
|
41
|
+
borderRadius: 6,
|
|
42
|
+
padding: theme.spacing(1),
|
|
43
|
+
overflowX: "auto",
|
|
44
|
+
fontSize: "0.85em"
|
|
45
|
+
},
|
|
46
|
+
"& code": {
|
|
47
|
+
fontSize: "0.85em",
|
|
48
|
+
fontFamily: '"Roboto Mono", monospace'
|
|
49
|
+
},
|
|
50
|
+
"& ul, & ol": {
|
|
51
|
+
marginTop: theme.spacing(0.5),
|
|
52
|
+
marginBottom: theme.spacing(0.5),
|
|
53
|
+
paddingLeft: theme.spacing(2.5)
|
|
54
|
+
},
|
|
55
|
+
"& a": {
|
|
56
|
+
color: theme.palette.primary.main
|
|
57
|
+
},
|
|
58
|
+
"& table": {
|
|
59
|
+
borderCollapse: "collapse",
|
|
60
|
+
"& th, & td": {
|
|
61
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
62
|
+
padding: theme.spacing(0.5, 1)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
userBubble: {
|
|
67
|
+
background: theme.palette.primary.main,
|
|
68
|
+
color: theme.palette.primary.contrastText,
|
|
69
|
+
borderBottomRightRadius: 4
|
|
70
|
+
},
|
|
71
|
+
assistantBubble: {
|
|
72
|
+
background: theme.palette.type === "dark" ? "#2d2d2d" : "#f0f0f0",
|
|
73
|
+
color: theme.palette.text.primary,
|
|
74
|
+
borderBottomLeftRadius: 4
|
|
75
|
+
},
|
|
76
|
+
"@keyframes typingBounce": {
|
|
77
|
+
"0%, 80%, 100%": { transform: "translateY(0)", opacity: 0.35 },
|
|
78
|
+
"40%": { transform: "translateY(-4px)", opacity: 1 }
|
|
79
|
+
},
|
|
80
|
+
typing: {
|
|
81
|
+
display: "inline-flex",
|
|
82
|
+
alignItems: "center",
|
|
83
|
+
gap: 4,
|
|
84
|
+
padding: theme.spacing(0.5, 0)
|
|
85
|
+
},
|
|
86
|
+
dot: {
|
|
87
|
+
width: 6,
|
|
88
|
+
height: 6,
|
|
89
|
+
borderRadius: "50%",
|
|
90
|
+
background: theme.palette.text.secondary,
|
|
91
|
+
animation: "$typingBounce 1.2s infinite ease-in-out both"
|
|
92
|
+
},
|
|
93
|
+
dot2: {
|
|
94
|
+
animationDelay: "0.2s"
|
|
95
|
+
},
|
|
96
|
+
dot3: {
|
|
97
|
+
animationDelay: "0.4s"
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
100
|
+
function ChatMessage({
|
|
101
|
+
message,
|
|
102
|
+
agentName = "AI",
|
|
103
|
+
loading = false
|
|
104
|
+
}) {
|
|
105
|
+
const classes = useStyles();
|
|
106
|
+
const isUser = message.role === "user";
|
|
107
|
+
const showTyping = !isUser && loading && !message.content;
|
|
108
|
+
let bubbleContent;
|
|
109
|
+
if (isUser) {
|
|
110
|
+
bubbleContent = /* @__PURE__ */ jsx(Typography, { variant: "body2", children: message.content });
|
|
111
|
+
} else if (showTyping) {
|
|
112
|
+
bubbleContent = /* @__PURE__ */ jsxs("div", { className: classes.typing, "aria-label": `${agentName} is typing`, children: [
|
|
113
|
+
/* @__PURE__ */ jsx("span", { className: classes.dot }),
|
|
114
|
+
/* @__PURE__ */ jsx("span", { className: `${classes.dot} ${classes.dot2}` }),
|
|
115
|
+
/* @__PURE__ */ jsx("span", { className: `${classes.dot} ${classes.dot3}` })
|
|
116
|
+
] });
|
|
117
|
+
} else {
|
|
118
|
+
bubbleContent = /* @__PURE__ */ jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: message.content });
|
|
119
|
+
}
|
|
120
|
+
return /* @__PURE__ */ jsxs(
|
|
121
|
+
"div",
|
|
122
|
+
{
|
|
123
|
+
className: `${classes.root} ${isUser ? classes.userRow : classes.assistantRow}`,
|
|
124
|
+
children: [
|
|
125
|
+
/* @__PURE__ */ jsx(Typography, { className: classes.label, children: isUser ? "You" : agentName }),
|
|
126
|
+
/* @__PURE__ */ jsx(
|
|
127
|
+
"div",
|
|
128
|
+
{
|
|
129
|
+
className: `${classes.bubble} ${isUser ? classes.userBubble : classes.assistantBubble}`,
|
|
130
|
+
children: bubbleContent
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { ChatMessage };
|
|
139
|
+
//# sourceMappingURL=ChatMessage.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatMessage.esm.js","sources":["../../src/components/ChatMessage.tsx"],"sourcesContent":["import React from \"react\";\nimport ReactMarkdown from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\nimport {makeStyles, Theme} from \"@material-ui/core/styles\";\nimport Typography from \"@material-ui/core/Typography\";\nimport type {ChatMessage as ChatMessageType} from \"../api\";\n\nconst useStyles = makeStyles((theme: Theme) => ({\n root: {\n display: \"flex\",\n flexDirection: \"column\",\n marginBottom: theme.spacing(1.5),\n },\n userRow: {\n alignItems: \"flex-end\",\n },\n assistantRow: {\n alignItems: \"flex-start\",\n },\n label: {\n fontSize: \"0.7rem\",\n fontWeight: 600,\n textTransform: \"uppercase\",\n color: theme.palette.text.secondary,\n marginBottom: 2,\n paddingLeft: theme.spacing(0.5),\n paddingRight: theme.spacing(0.5),\n },\n bubble: {\n maxWidth: \"85%\",\n padding: theme.spacing(1, 1.5),\n borderRadius: 12,\n wordBreak: \"break-word\",\n \"& p\": {\n margin: 0,\n },\n \"& p + p\": {\n marginTop: theme.spacing(1),\n },\n \"& pre\": {\n background: theme.palette.type === \"dark\" ? \"#1e1e1e\" : \"#f5f5f5\",\n borderRadius: 6,\n padding: theme.spacing(1),\n overflowX: \"auto\",\n fontSize: \"0.85em\",\n },\n \"& code\": {\n fontSize: \"0.85em\",\n fontFamily: '\"Roboto Mono\", monospace',\n },\n \"& ul, & ol\": {\n marginTop: theme.spacing(0.5),\n marginBottom: theme.spacing(0.5),\n paddingLeft: theme.spacing(2.5),\n },\n \"& a\": {\n color: theme.palette.primary.main,\n },\n \"& table\": {\n borderCollapse: \"collapse\",\n \"& th, & td\": {\n border: `1px solid ${theme.palette.divider}`,\n padding: theme.spacing(0.5, 1),\n },\n },\n },\n userBubble: {\n background: theme.palette.primary.main,\n color: theme.palette.primary.contrastText,\n borderBottomRightRadius: 4,\n },\n assistantBubble: {\n background: theme.palette.type === \"dark\" ? \"#2d2d2d\" : \"#f0f0f0\",\n color: theme.palette.text.primary,\n borderBottomLeftRadius: 4,\n },\n \"@keyframes typingBounce\": {\n \"0%, 80%, 100%\": {transform: \"translateY(0)\", opacity: 0.35},\n \"40%\": {transform: \"translateY(-4px)\", opacity: 1},\n },\n typing: {\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: 4,\n padding: theme.spacing(0.5, 0),\n },\n dot: {\n width: 6,\n height: 6,\n borderRadius: \"50%\",\n background: theme.palette.text.secondary,\n animation: \"$typingBounce 1.2s infinite ease-in-out both\",\n },\n dot2: {\n animationDelay: \"0.2s\",\n },\n dot3: {\n animationDelay: \"0.4s\",\n },\n}));\n\ninterface ChatMessageProps {\n message: ChatMessageType;\n agentName?: string;\n loading?: boolean;\n}\n\nexport function ChatMessage({\n message,\n agentName = \"AI\",\n loading = false,\n}: ChatMessageProps) {\n const classes = useStyles();\n const isUser = message.role === \"user\";\n const showTyping = !isUser && loading && !message.content;\n\n let bubbleContent: React.ReactNode;\n if (isUser) {\n bubbleContent = <Typography variant=\"body2\">{message.content}</Typography>;\n } else if (showTyping) {\n bubbleContent = (\n <div className={classes.typing} aria-label={`${agentName} is typing`}>\n <span className={classes.dot} />\n <span className={`${classes.dot} ${classes.dot2}`} />\n <span className={`${classes.dot} ${classes.dot3}`} />\n </div>\n );\n } else {\n bubbleContent = (\n <ReactMarkdown remarkPlugins={[remarkGfm]}>\n {message.content}\n </ReactMarkdown>\n );\n }\n\n return (\n <div\n className={`${classes.root} ${\n isUser ? classes.userRow : classes.assistantRow\n }`}\n >\n <Typography className={classes.label}>\n {isUser ? \"You\" : agentName}\n </Typography>\n <div\n className={`${classes.bubble} ${\n isUser ? classes.userBubble : classes.assistantBubble\n }`}\n >\n {bubbleContent}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;AAOA,MAAM,SAAA,GAAY,UAAA,CAAW,CAAC,KAAA,MAAkB;AAAA,EAC9C,IAAA,EAAM;AAAA,IACJ,OAAA,EAAS,MAAA;AAAA,IACT,aAAA,EAAe,QAAA;AAAA,IACf,YAAA,EAAc,KAAA,CAAM,OAAA,CAAQ,GAAG;AAAA,GACjC;AAAA,EACA,OAAA,EAAS;AAAA,IACP,UAAA,EAAY;AAAA,GACd;AAAA,EACA,YAAA,EAAc;AAAA,IACZ,UAAA,EAAY;AAAA,GACd;AAAA,EACA,KAAA,EAAO;AAAA,IACL,QAAA,EAAU,QAAA;AAAA,IACV,UAAA,EAAY,GAAA;AAAA,IACZ,aAAA,EAAe,WAAA;AAAA,IACf,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,IAAA,CAAK,SAAA;AAAA,IAC1B,YAAA,EAAc,CAAA;AAAA,IACd,WAAA,EAAa,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAAA,IAC9B,YAAA,EAAc,KAAA,CAAM,OAAA,CAAQ,GAAG;AAAA,GACjC;AAAA,EACA,MAAA,EAAQ;AAAA,IACN,QAAA,EAAU,KAAA;AAAA,IACV,OAAA,EAAS,KAAA,CAAM,OAAA,CAAQ,CAAA,EAAG,GAAG,CAAA;AAAA,IAC7B,YAAA,EAAc,EAAA;AAAA,IACd,SAAA,EAAW,YAAA;AAAA,IACX,KAAA,EAAO;AAAA,MACL,MAAA,EAAQ;AAAA,KACV;AAAA,IACA,SAAA,EAAW;AAAA,MACT,SAAA,EAAW,KAAA,CAAM,OAAA,CAAQ,CAAC;AAAA,KAC5B;AAAA,IACA,OAAA,EAAS;AAAA,MACP,UAAA,EAAY,KAAA,CAAM,OAAA,CAAQ,IAAA,KAAS,SAAS,SAAA,GAAY,SAAA;AAAA,MACxD,YAAA,EAAc,CAAA;AAAA,MACd,OAAA,EAAS,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAAA,MACxB,SAAA,EAAW,MAAA;AAAA,MACX,QAAA,EAAU;AAAA,KACZ;AAAA,IACA,QAAA,EAAU;AAAA,MACR,QAAA,EAAU,QAAA;AAAA,MACV,UAAA,EAAY;AAAA,KACd;AAAA,IACA,YAAA,EAAc;AAAA,MACZ,SAAA,EAAW,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAAA,MAC5B,YAAA,EAAc,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAAA,MAC/B,WAAA,EAAa,KAAA,CAAM,OAAA,CAAQ,GAAG;AAAA,KAChC;AAAA,IACA,KAAA,EAAO;AAAA,MACL,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,OAAA,CAAQ;AAAA,KAC/B;AAAA,IACA,SAAA,EAAW;AAAA,MACT,cAAA,EAAgB,UAAA;AAAA,MAChB,YAAA,EAAc;AAAA,QACZ,MAAA,EAAQ,CAAA,UAAA,EAAa,KAAA,CAAM,OAAA,CAAQ,OAAO,CAAA,CAAA;AAAA,QAC1C,OAAA,EAAS,KAAA,CAAM,OAAA,CAAQ,GAAA,EAAK,CAAC;AAAA;AAC/B;AACF,GACF;AAAA,EACA,UAAA,EAAY;AAAA,IACV,UAAA,EAAY,KAAA,CAAM,OAAA,CAAQ,OAAA,CAAQ,IAAA;AAAA,IAClC,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,OAAA,CAAQ,YAAA;AAAA,IAC7B,uBAAA,EAAyB;AAAA,GAC3B;AAAA,EACA,eAAA,EAAiB;AAAA,IACf,UAAA,EAAY,KAAA,CAAM,OAAA,CAAQ,IAAA,KAAS,SAAS,SAAA,GAAY,SAAA;AAAA,IACxD,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,IAAA,CAAK,OAAA;AAAA,IAC1B,sBAAA,EAAwB;AAAA,GAC1B;AAAA,EACA,yBAAA,EAA2B;AAAA,IACzB,eAAA,EAAiB,EAAC,SAAA,EAAW,eAAA,EAAiB,SAAS,IAAA,EAAI;AAAA,IAC3D,KAAA,EAAO,EAAC,SAAA,EAAW,kBAAA,EAAoB,SAAS,CAAA;AAAC,GACnD;AAAA,EACA,MAAA,EAAQ;AAAA,IACN,OAAA,EAAS,aAAA;AAAA,IACT,UAAA,EAAY,QAAA;AAAA,IACZ,GAAA,EAAK,CAAA;AAAA,IACL,OAAA,EAAS,KAAA,CAAM,OAAA,CAAQ,GAAA,EAAK,CAAC;AAAA,GAC/B;AAAA,EACA,GAAA,EAAK;AAAA,IACH,KAAA,EAAO,CAAA;AAAA,IACP,MAAA,EAAQ,CAAA;AAAA,IACR,YAAA,EAAc,KAAA;AAAA,IACd,UAAA,EAAY,KAAA,CAAM,OAAA,CAAQ,IAAA,CAAK,SAAA;AAAA,IAC/B,SAAA,EAAW;AAAA,GACb;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,cAAA,EAAgB;AAAA,GAClB;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,cAAA,EAAgB;AAAA;AAEpB,CAAA,CAAE,CAAA;AAQK,SAAS,WAAA,CAAY;AAAA,EAC1B,OAAA;AAAA,EACA,SAAA,GAAY,IAAA;AAAA,EACZ,OAAA,GAAU;AACZ,CAAA,EAAqB;AACnB,EAAA,MAAM,UAAU,SAAA,EAAU;AAC1B,EAAA,MAAM,MAAA,GAAS,QAAQ,IAAA,KAAS,MAAA;AAChC,EAAA,MAAM,UAAA,GAAa,CAAC,MAAA,IAAU,OAAA,IAAW,CAAC,OAAA,CAAQ,OAAA;AAElD,EAAA,IAAI,aAAA;AACJ,EAAA,IAAI,MAAA,EAAQ;AACV,IAAA,aAAA,mBAAgB,GAAA,CAAC,UAAA,EAAA,EAAW,OAAA,EAAQ,OAAA,EAAS,kBAAQ,OAAA,EAAQ,CAAA;AAAA,EAC/D,WAAW,UAAA,EAAY;AACrB,IAAA,aAAA,mBACE,IAAA,CAAC,SAAI,SAAA,EAAW,OAAA,CAAQ,QAAQ,YAAA,EAAY,CAAA,EAAG,SAAS,CAAA,UAAA,CAAA,EACtD,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAW,OAAA,CAAQ,GAAA,EAAK,CAAA;AAAA,sBAC9B,GAAA,CAAC,UAAK,SAAA,EAAW,CAAA,EAAG,QAAQ,GAAG,CAAA,CAAA,EAAI,OAAA,CAAQ,IAAI,CAAA,CAAA,EAAI,CAAA;AAAA,sBACnD,GAAA,CAAC,UAAK,SAAA,EAAW,CAAA,EAAG,QAAQ,GAAG,CAAA,CAAA,EAAI,OAAA,CAAQ,IAAI,CAAA,CAAA,EAAI;AAAA,KAAA,EACrD,CAAA;AAAA,EAEJ,CAAA,MAAO;AACL,IAAA,aAAA,uBACG,aAAA,EAAA,EAAc,aAAA,EAAe,CAAC,SAAS,CAAA,EACrC,kBAAQ,OAAA,EACX,CAAA;AAAA,EAEJ;AAEA,EAAA,uBACE,IAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,SAAA,EAAW,GAAG,OAAA,CAAQ,IAAI,IACxB,MAAA,GAAS,OAAA,CAAQ,OAAA,GAAU,OAAA,CAAQ,YACrC,CAAA,CAAA;AAAA,MAEA,QAAA,EAAA;AAAA,wBAAA,GAAA,CAAC,cAAW,SAAA,EAAW,OAAA,CAAQ,KAAA,EAC5B,QAAA,EAAA,MAAA,GAAS,QAAQ,SAAA,EACpB,CAAA;AAAA,wBACA,GAAA;AAAA,UAAC,KAAA;AAAA,UAAA;AAAA,YACC,SAAA,EAAW,GAAG,OAAA,CAAQ,MAAM,IAC1B,MAAA,GAAS,OAAA,CAAQ,UAAA,GAAa,OAAA,CAAQ,eACxC,CAAA,CAAA;AAAA,YAEC,QAAA,EAAA;AAAA;AAAA;AACH;AAAA;AAAA,GACF;AAEJ;;;;"}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
3
|
+
import { flushSync } from 'react-dom';
|
|
4
|
+
import { makeStyles } from '@material-ui/core/styles';
|
|
5
|
+
import IconButton from '@material-ui/core/IconButton';
|
|
6
|
+
import TextField from '@material-ui/core/TextField';
|
|
7
|
+
import Typography from '@material-ui/core/Typography';
|
|
8
|
+
import CircularProgress from '@material-ui/core/CircularProgress';
|
|
9
|
+
import SendIcon from '@material-ui/icons/Send';
|
|
10
|
+
import SettingsIcon from '@material-ui/icons/Settings';
|
|
11
|
+
import DeleteSweepIcon from '@material-ui/icons/DeleteSweep';
|
|
12
|
+
import LinkIcon from '@material-ui/icons/Link';
|
|
13
|
+
import { useApi, configApiRef } from '@backstage/frontend-plugin-api';
|
|
14
|
+
import { libreChatApiRef } from '../api.esm.js';
|
|
15
|
+
import { ChatMessage } from './ChatMessage.esm.js';
|
|
16
|
+
import { SettingsTab } from './SettingsTab.esm.js';
|
|
17
|
+
import { useLibreChatSettings } from '../hooks/useLibreChatSettings.esm.js';
|
|
18
|
+
import { usePageContext } from '../hooks/usePageContext.esm.js';
|
|
19
|
+
|
|
20
|
+
const useStyles = makeStyles((theme) => ({
|
|
21
|
+
root: {
|
|
22
|
+
display: "flex",
|
|
23
|
+
flexDirection: "column",
|
|
24
|
+
height: "100%",
|
|
25
|
+
background: theme.palette.background.paper
|
|
26
|
+
},
|
|
27
|
+
header: {
|
|
28
|
+
display: "flex",
|
|
29
|
+
alignItems: "center",
|
|
30
|
+
justifyContent: "space-between",
|
|
31
|
+
padding: theme.spacing(1, 2),
|
|
32
|
+
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
33
|
+
minHeight: 48
|
|
34
|
+
},
|
|
35
|
+
headerTitle: {
|
|
36
|
+
fontWeight: 600,
|
|
37
|
+
fontSize: "0.95rem"
|
|
38
|
+
},
|
|
39
|
+
headerActions: {
|
|
40
|
+
display: "flex",
|
|
41
|
+
gap: theme.spacing(0.5)
|
|
42
|
+
},
|
|
43
|
+
messages: {
|
|
44
|
+
flex: 1,
|
|
45
|
+
overflowY: "auto",
|
|
46
|
+
padding: theme.spacing(2),
|
|
47
|
+
display: "flex",
|
|
48
|
+
flexDirection: "column"
|
|
49
|
+
},
|
|
50
|
+
emptyState: {
|
|
51
|
+
display: "flex",
|
|
52
|
+
flex: 1,
|
|
53
|
+
alignItems: "center",
|
|
54
|
+
justifyContent: "center",
|
|
55
|
+
color: theme.palette.text.secondary,
|
|
56
|
+
textAlign: "center",
|
|
57
|
+
padding: theme.spacing(3)
|
|
58
|
+
},
|
|
59
|
+
inputArea: {
|
|
60
|
+
display: "flex",
|
|
61
|
+
alignItems: "flex-end",
|
|
62
|
+
padding: theme.spacing(1, 2, 2),
|
|
63
|
+
gap: theme.spacing(1),
|
|
64
|
+
borderTop: `1px solid ${theme.palette.divider}`
|
|
65
|
+
},
|
|
66
|
+
textField: {
|
|
67
|
+
flex: 1
|
|
68
|
+
},
|
|
69
|
+
error: {
|
|
70
|
+
margin: theme.spacing(1, 2),
|
|
71
|
+
padding: theme.spacing(1),
|
|
72
|
+
background: theme.palette.error.light,
|
|
73
|
+
color: theme.palette.error.contrastText,
|
|
74
|
+
borderRadius: 6,
|
|
75
|
+
fontSize: "0.85rem"
|
|
76
|
+
},
|
|
77
|
+
contextBar: {
|
|
78
|
+
display: "flex",
|
|
79
|
+
alignItems: "center",
|
|
80
|
+
gap: theme.spacing(0.5),
|
|
81
|
+
padding: theme.spacing(0.5, 2),
|
|
82
|
+
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
83
|
+
background: theme.palette.type === "dark" ? "#1a1a2e" : "#f5f7ff",
|
|
84
|
+
fontSize: "0.75rem",
|
|
85
|
+
color: theme.palette.text.secondary,
|
|
86
|
+
overflow: "hidden",
|
|
87
|
+
whiteSpace: "nowrap",
|
|
88
|
+
textOverflow: "ellipsis"
|
|
89
|
+
},
|
|
90
|
+
contextIcon: {
|
|
91
|
+
fontSize: 14,
|
|
92
|
+
opacity: 0.6
|
|
93
|
+
}
|
|
94
|
+
}));
|
|
95
|
+
function ChatPanel() {
|
|
96
|
+
const classes = useStyles();
|
|
97
|
+
const libreChatApi = useApi(libreChatApiRef);
|
|
98
|
+
const configApi = useApi(configApiRef);
|
|
99
|
+
const { settings } = useLibreChatSettings();
|
|
100
|
+
const pageContext = usePageContext();
|
|
101
|
+
const agentName = configApi.getOptionalString("librechat.name") ?? "AI";
|
|
102
|
+
const [messages, setMessages] = useState([]);
|
|
103
|
+
const [input, setInput] = useState("");
|
|
104
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
105
|
+
const [error, setError] = useState(null);
|
|
106
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
107
|
+
const messagesEndRef = useRef(null);
|
|
108
|
+
const abortRef = useRef(false);
|
|
109
|
+
const scrollToBottom = useCallback(() => {
|
|
110
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
111
|
+
}, []);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
scrollToBottom();
|
|
114
|
+
}, [messages, scrollToBottom]);
|
|
115
|
+
const handleSend = useCallback(async () => {
|
|
116
|
+
const trimmed = input.trim();
|
|
117
|
+
if (!trimmed || isStreaming) return;
|
|
118
|
+
setError(null);
|
|
119
|
+
const userMessage = { role: "user", content: trimmed };
|
|
120
|
+
const updatedMessages = [...messages, userMessage];
|
|
121
|
+
setMessages(updatedMessages);
|
|
122
|
+
setInput("");
|
|
123
|
+
setIsStreaming(true);
|
|
124
|
+
abortRef.current = false;
|
|
125
|
+
const assistantMessage = {
|
|
126
|
+
role: "assistant",
|
|
127
|
+
content: ""
|
|
128
|
+
};
|
|
129
|
+
setMessages([...updatedMessages, assistantMessage]);
|
|
130
|
+
try {
|
|
131
|
+
const contextSuffix = [
|
|
132
|
+
"",
|
|
133
|
+
"[Page context]",
|
|
134
|
+
`Title: ${pageContext.title}`,
|
|
135
|
+
`Path: ${pageContext.path}`,
|
|
136
|
+
`URL: ${pageContext.url}`
|
|
137
|
+
].join("\n");
|
|
138
|
+
const messagesWithContext = updatedMessages.map(
|
|
139
|
+
(msg, idx) => idx === updatedMessages.length - 1 && msg.role === "user" ? { ...msg, content: `${msg.content}
|
|
140
|
+
${contextSuffix}` } : msg
|
|
141
|
+
);
|
|
142
|
+
const stream = libreChatApi.sendMessage(messagesWithContext, {
|
|
143
|
+
apiKey: settings.apiKey || void 0
|
|
144
|
+
});
|
|
145
|
+
let accumulated = "";
|
|
146
|
+
for await (const chunk of stream) {
|
|
147
|
+
if (abortRef.current) break;
|
|
148
|
+
accumulated += chunk;
|
|
149
|
+
const content = accumulated;
|
|
150
|
+
await new Promise((resolve) => {
|
|
151
|
+
flushSync(() => {
|
|
152
|
+
setMessages((prev) => {
|
|
153
|
+
const updated = [...prev];
|
|
154
|
+
updated[updated.length - 1] = {
|
|
155
|
+
role: "assistant",
|
|
156
|
+
content
|
|
157
|
+
};
|
|
158
|
+
return updated;
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
requestAnimationFrame(() => resolve());
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (!accumulated) {
|
|
165
|
+
setMessages(updatedMessages);
|
|
166
|
+
}
|
|
167
|
+
} catch (err) {
|
|
168
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
169
|
+
setError(msg);
|
|
170
|
+
setMessages(updatedMessages);
|
|
171
|
+
} finally {
|
|
172
|
+
setIsStreaming(false);
|
|
173
|
+
}
|
|
174
|
+
}, [input, isStreaming, messages, libreChatApi, settings, pageContext]);
|
|
175
|
+
const handleKeyDown = useCallback(
|
|
176
|
+
(e) => {
|
|
177
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
handleSend();
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
[handleSend]
|
|
183
|
+
);
|
|
184
|
+
const handleClear = useCallback(() => {
|
|
185
|
+
setMessages([]);
|
|
186
|
+
setError(null);
|
|
187
|
+
abortRef.current = true;
|
|
188
|
+
}, []);
|
|
189
|
+
const handleCheckResult = useCallback(
|
|
190
|
+
(reply, checkError) => {
|
|
191
|
+
if (checkError) {
|
|
192
|
+
setMessages((prev) => [
|
|
193
|
+
...prev,
|
|
194
|
+
{
|
|
195
|
+
role: "assistant",
|
|
196
|
+
content: `\u26A0\uFE0F API key check failed: ${checkError}`
|
|
197
|
+
}
|
|
198
|
+
]);
|
|
199
|
+
} else {
|
|
200
|
+
setMessages((prev) => [
|
|
201
|
+
...prev,
|
|
202
|
+
{ role: "assistant", content: `\u2705 API key is valid!
|
|
203
|
+
|
|
204
|
+
${reply}` }
|
|
205
|
+
]);
|
|
206
|
+
}
|
|
207
|
+
setShowSettings(false);
|
|
208
|
+
},
|
|
209
|
+
[]
|
|
210
|
+
);
|
|
211
|
+
if (showSettings) {
|
|
212
|
+
return /* @__PURE__ */ jsx("div", { className: classes.root, children: /* @__PURE__ */ jsx(
|
|
213
|
+
SettingsTab,
|
|
214
|
+
{
|
|
215
|
+
onBack: () => setShowSettings(false),
|
|
216
|
+
onCheckResult: handleCheckResult
|
|
217
|
+
}
|
|
218
|
+
) });
|
|
219
|
+
}
|
|
220
|
+
return /* @__PURE__ */ jsxs("div", { className: classes.root, children: [
|
|
221
|
+
/* @__PURE__ */ jsxs("div", { className: classes.header, children: [
|
|
222
|
+
/* @__PURE__ */ jsxs(Typography, { className: classes.headerTitle, children: [
|
|
223
|
+
agentName,
|
|
224
|
+
" Chat"
|
|
225
|
+
] }),
|
|
226
|
+
/* @__PURE__ */ jsxs("div", { className: classes.headerActions, children: [
|
|
227
|
+
/* @__PURE__ */ jsx(IconButton, { size: "small", onClick: handleClear, title: "Clear chat", children: /* @__PURE__ */ jsx(DeleteSweepIcon, { fontSize: "small" }) }),
|
|
228
|
+
/* @__PURE__ */ jsx(
|
|
229
|
+
IconButton,
|
|
230
|
+
{
|
|
231
|
+
size: "small",
|
|
232
|
+
onClick: () => setShowSettings(true),
|
|
233
|
+
title: "Settings",
|
|
234
|
+
children: /* @__PURE__ */ jsx(SettingsIcon, { fontSize: "small" })
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
] })
|
|
238
|
+
] }),
|
|
239
|
+
/* @__PURE__ */ jsxs("div", { className: classes.contextBar, title: pageContext.url, children: [
|
|
240
|
+
/* @__PURE__ */ jsx(LinkIcon, { className: classes.contextIcon }),
|
|
241
|
+
/* @__PURE__ */ jsx("span", { children: pageContext.title || pageContext.path })
|
|
242
|
+
] }),
|
|
243
|
+
/* @__PURE__ */ jsxs("div", { className: classes.messages, children: [
|
|
244
|
+
messages.length === 0 ? /* @__PURE__ */ jsx("div", { className: classes.emptyState, children: /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "textSecondary", children: "Send a message to start chatting with AI" }) }) : messages.map((msg, idx) => /* @__PURE__ */ jsx(
|
|
245
|
+
ChatMessage,
|
|
246
|
+
{
|
|
247
|
+
message: msg,
|
|
248
|
+
agentName,
|
|
249
|
+
loading: isStreaming && idx === messages.length - 1 && msg.role === "assistant"
|
|
250
|
+
},
|
|
251
|
+
idx
|
|
252
|
+
)),
|
|
253
|
+
/* @__PURE__ */ jsx("div", { ref: messagesEndRef })
|
|
254
|
+
] }),
|
|
255
|
+
error && /* @__PURE__ */ jsx("div", { className: classes.error, children: error }),
|
|
256
|
+
/* @__PURE__ */ jsxs("div", { className: classes.inputArea, children: [
|
|
257
|
+
/* @__PURE__ */ jsx(
|
|
258
|
+
TextField,
|
|
259
|
+
{
|
|
260
|
+
className: classes.textField,
|
|
261
|
+
variant: "outlined",
|
|
262
|
+
size: "small",
|
|
263
|
+
placeholder: "Type a message...",
|
|
264
|
+
multiline: true,
|
|
265
|
+
maxRows: 4,
|
|
266
|
+
value: input,
|
|
267
|
+
onChange: (e) => setInput(e.target.value),
|
|
268
|
+
onKeyDown: handleKeyDown,
|
|
269
|
+
disabled: isStreaming,
|
|
270
|
+
autoFocus: true
|
|
271
|
+
}
|
|
272
|
+
),
|
|
273
|
+
/* @__PURE__ */ jsx(
|
|
274
|
+
IconButton,
|
|
275
|
+
{
|
|
276
|
+
color: "primary",
|
|
277
|
+
onClick: handleSend,
|
|
278
|
+
disabled: isStreaming || !input.trim(),
|
|
279
|
+
title: "Send message",
|
|
280
|
+
children: isStreaming ? /* @__PURE__ */ jsx(CircularProgress, { size: 24 }) : /* @__PURE__ */ jsx(SendIcon, {})
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
] })
|
|
284
|
+
] });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export { ChatPanel };
|
|
288
|
+
//# sourceMappingURL=ChatPanel.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatPanel.esm.js","sources":["../../src/components/ChatPanel.tsx"],"sourcesContent":["import React, {useState, useRef, useEffect, useCallback} from \"react\";\nimport {flushSync} from \"react-dom\";\nimport {makeStyles, Theme} from \"@material-ui/core/styles\";\nimport IconButton from \"@material-ui/core/IconButton\";\nimport TextField from \"@material-ui/core/TextField\";\nimport Typography from \"@material-ui/core/Typography\";\nimport CircularProgress from \"@material-ui/core/CircularProgress\";\nimport SendIcon from \"@material-ui/icons/Send\";\nimport SettingsIcon from \"@material-ui/icons/Settings\";\nimport DeleteSweepIcon from \"@material-ui/icons/DeleteSweep\";\nimport LinkIcon from \"@material-ui/icons/Link\";\nimport {useApi, configApiRef} from \"@backstage/frontend-plugin-api\";\nimport {libreChatApiRef, ChatMessage as ChatMessageType} from \"../api\";\nimport {ChatMessage} from \"./ChatMessage\";\nimport {SettingsTab} from \"./SettingsTab\";\nimport {useLibreChatSettings} from \"../hooks/useLibreChatSettings\";\nimport {usePageContext} from \"../hooks/usePageContext\";\n\nconst useStyles = makeStyles((theme: Theme) => ({\n root: {\n display: \"flex\",\n flexDirection: \"column\",\n height: \"100%\",\n background: theme.palette.background.paper,\n },\n header: {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n padding: theme.spacing(1, 2),\n borderBottom: `1px solid ${theme.palette.divider}`,\n minHeight: 48,\n },\n headerTitle: {\n fontWeight: 600,\n fontSize: \"0.95rem\",\n },\n headerActions: {\n display: \"flex\",\n gap: theme.spacing(0.5),\n },\n messages: {\n flex: 1,\n overflowY: \"auto\",\n padding: theme.spacing(2),\n display: \"flex\",\n flexDirection: \"column\",\n },\n emptyState: {\n display: \"flex\",\n flex: 1,\n alignItems: \"center\",\n justifyContent: \"center\",\n color: theme.palette.text.secondary,\n textAlign: \"center\",\n padding: theme.spacing(3),\n },\n inputArea: {\n display: \"flex\",\n alignItems: \"flex-end\",\n padding: theme.spacing(1, 2, 2),\n gap: theme.spacing(1),\n borderTop: `1px solid ${theme.palette.divider}`,\n },\n textField: {\n flex: 1,\n },\n error: {\n margin: theme.spacing(1, 2),\n padding: theme.spacing(1),\n background: theme.palette.error.light,\n color: theme.palette.error.contrastText,\n borderRadius: 6,\n fontSize: \"0.85rem\",\n },\n contextBar: {\n display: \"flex\",\n alignItems: \"center\",\n gap: theme.spacing(0.5),\n padding: theme.spacing(0.5, 2),\n borderBottom: `1px solid ${theme.palette.divider}`,\n background: theme.palette.type === \"dark\" ? \"#1a1a2e\" : \"#f5f7ff\",\n fontSize: \"0.75rem\",\n color: theme.palette.text.secondary,\n overflow: \"hidden\",\n whiteSpace: \"nowrap\" as const,\n textOverflow: \"ellipsis\",\n },\n contextIcon: {\n fontSize: 14,\n opacity: 0.6,\n },\n}));\n\nexport function ChatPanel() {\n const classes = useStyles();\n const libreChatApi = useApi(libreChatApiRef);\n const configApi = useApi(configApiRef);\n const {settings} = useLibreChatSettings();\n const pageContext = usePageContext();\n const agentName = configApi.getOptionalString(\"librechat.name\") ?? \"AI\";\n\n const [messages, setMessages] = useState<ChatMessageType[]>([]);\n const [input, setInput] = useState(\"\");\n const [isStreaming, setIsStreaming] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [showSettings, setShowSettings] = useState(false);\n\n const messagesEndRef = useRef<HTMLDivElement>(null);\n const abortRef = useRef(false);\n\n const scrollToBottom = useCallback(() => {\n messagesEndRef.current?.scrollIntoView({behavior: \"smooth\"});\n }, []);\n\n useEffect(() => {\n scrollToBottom();\n }, [messages, scrollToBottom]);\n\n const handleSend = useCallback(async () => {\n const trimmed = input.trim();\n if (!trimmed || isStreaming) return;\n\n setError(null);\n const userMessage: ChatMessageType = {role: \"user\", content: trimmed};\n const updatedMessages = [...messages, userMessage];\n setMessages(updatedMessages);\n setInput(\"\");\n setIsStreaming(true);\n abortRef.current = false;\n\n // Add placeholder for assistant response\n const assistantMessage: ChatMessageType = {\n role: \"assistant\",\n content: \"\",\n };\n setMessages([...updatedMessages, assistantMessage]);\n\n try {\n // Inject page context into the latest user message\n const contextSuffix = [\n \"\",\n \"[Page context]\",\n `Title: ${pageContext.title}`,\n `Path: ${pageContext.path}`,\n `URL: ${pageContext.url}`,\n ].join(\"\\n\");\n\n const messagesWithContext = updatedMessages.map((msg, idx) =>\n idx === updatedMessages.length - 1 && msg.role === \"user\"\n ? {...msg, content: `${msg.content}\\n${contextSuffix}`}\n : msg,\n );\n\n const stream = libreChatApi.sendMessage(messagesWithContext, {\n apiKey: settings.apiKey || undefined,\n });\n\n let accumulated = \"\";\n for await (const chunk of stream) {\n if (abortRef.current) break;\n accumulated += chunk;\n const content = accumulated;\n // Force synchronous render + wait for browser paint\n await new Promise<void>((resolve) => {\n flushSync(() => {\n setMessages((prev) => {\n const updated = [...prev];\n updated[updated.length - 1] = {\n role: \"assistant\",\n content,\n };\n return updated;\n });\n });\n requestAnimationFrame(() => resolve());\n });\n }\n\n // If accumulated is empty, remove the empty assistant message\n if (!accumulated) {\n setMessages(updatedMessages);\n }\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : \"Unknown error\";\n setError(msg);\n // Remove the empty assistant placeholder on error\n setMessages(updatedMessages);\n } finally {\n setIsStreaming(false);\n }\n }, [input, isStreaming, messages, libreChatApi, settings, pageContext]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n },\n [handleSend],\n );\n\n const handleClear = useCallback(() => {\n setMessages([]);\n setError(null);\n abortRef.current = true;\n }, []);\n\n const handleCheckResult = useCallback(\n (reply: string, checkError?: string) => {\n if (checkError) {\n setMessages((prev) => [\n ...prev,\n {\n role: \"assistant\",\n content: `⚠️ API key check failed: ${checkError}`,\n },\n ]);\n } else {\n setMessages((prev) => [\n ...prev,\n {role: \"assistant\", content: `✅ API key is valid!\\n\\n${reply}`},\n ]);\n }\n setShowSettings(false);\n },\n [],\n );\n\n if (showSettings) {\n return (\n <div className={classes.root}>\n <SettingsTab\n onBack={() => setShowSettings(false)}\n onCheckResult={handleCheckResult}\n />\n </div>\n );\n }\n\n return (\n <div className={classes.root}>\n <div className={classes.header}>\n <Typography className={classes.headerTitle}>\n {agentName} Chat\n </Typography>\n <div className={classes.headerActions}>\n <IconButton size=\"small\" onClick={handleClear} title=\"Clear chat\">\n <DeleteSweepIcon fontSize=\"small\" />\n </IconButton>\n <IconButton\n size=\"small\"\n onClick={() => setShowSettings(true)}\n title=\"Settings\"\n >\n <SettingsIcon fontSize=\"small\" />\n </IconButton>\n </div>\n </div>\n\n <div className={classes.contextBar} title={pageContext.url}>\n <LinkIcon className={classes.contextIcon} />\n <span>{pageContext.title || pageContext.path}</span>\n </div>\n\n <div className={classes.messages}>\n {messages.length === 0 ? (\n <div className={classes.emptyState}>\n <Typography variant=\"body2\" color=\"textSecondary\">\n Send a message to start chatting with AI\n </Typography>\n </div>\n ) : (\n messages.map((msg, idx) => (\n <ChatMessage\n key={idx}\n message={msg}\n agentName={agentName}\n loading={\n isStreaming &&\n idx === messages.length - 1 &&\n msg.role === \"assistant\"\n }\n />\n ))\n )}\n <div ref={messagesEndRef} />\n </div>\n\n {error && <div className={classes.error}>{error}</div>}\n\n <div className={classes.inputArea}>\n <TextField\n className={classes.textField}\n variant=\"outlined\"\n size=\"small\"\n placeholder=\"Type a message...\"\n multiline\n maxRows={4}\n value={input}\n onChange={(e) => setInput(e.target.value)}\n onKeyDown={handleKeyDown}\n disabled={isStreaming}\n // eslint-disable-next-line jsx-a11y/no-autofocus\n autoFocus\n />\n <IconButton\n color=\"primary\"\n onClick={handleSend}\n disabled={isStreaming || !input.trim()}\n title=\"Send message\"\n >\n {isStreaming ? <CircularProgress size={24} /> : <SendIcon />}\n </IconButton>\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { makeStyles } from '@material-ui/core/styles';
|
|
4
|
+
import TextField from '@material-ui/core/TextField';
|
|
5
|
+
import Button from '@material-ui/core/Button';
|
|
6
|
+
import IconButton from '@material-ui/core/IconButton';
|
|
7
|
+
import Typography from '@material-ui/core/Typography';
|
|
8
|
+
import CircularProgress from '@material-ui/core/CircularProgress';
|
|
9
|
+
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
|
|
10
|
+
import CheckIcon from '@material-ui/icons/Check';
|
|
11
|
+
import { useApi } from '@backstage/frontend-plugin-api';
|
|
12
|
+
import { libreChatApiRef } from '../api.esm.js';
|
|
13
|
+
import { useLibreChatSettings } from '../hooks/useLibreChatSettings.esm.js';
|
|
14
|
+
|
|
15
|
+
const useStyles = makeStyles((theme) => ({
|
|
16
|
+
root: {
|
|
17
|
+
display: "flex",
|
|
18
|
+
flexDirection: "column",
|
|
19
|
+
height: "100%"
|
|
20
|
+
},
|
|
21
|
+
header: {
|
|
22
|
+
display: "flex",
|
|
23
|
+
alignItems: "center",
|
|
24
|
+
padding: theme.spacing(1, 2),
|
|
25
|
+
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
26
|
+
gap: theme.spacing(1),
|
|
27
|
+
minHeight: 48
|
|
28
|
+
},
|
|
29
|
+
headerTitle: {
|
|
30
|
+
fontWeight: 600,
|
|
31
|
+
fontSize: "0.95rem"
|
|
32
|
+
},
|
|
33
|
+
content: {
|
|
34
|
+
flex: 1,
|
|
35
|
+
padding: theme.spacing(2),
|
|
36
|
+
display: "flex",
|
|
37
|
+
flexDirection: "column",
|
|
38
|
+
gap: theme.spacing(2),
|
|
39
|
+
overflow: "auto"
|
|
40
|
+
},
|
|
41
|
+
description: {
|
|
42
|
+
fontSize: "0.85rem",
|
|
43
|
+
color: theme.palette.text.secondary,
|
|
44
|
+
marginBottom: theme.spacing(1)
|
|
45
|
+
},
|
|
46
|
+
apiKeyRow: {
|
|
47
|
+
display: "flex",
|
|
48
|
+
gap: theme.spacing(1),
|
|
49
|
+
alignItems: "flex-start"
|
|
50
|
+
},
|
|
51
|
+
apiKeyField: {
|
|
52
|
+
flex: 1
|
|
53
|
+
},
|
|
54
|
+
testButton: {
|
|
55
|
+
marginTop: 2,
|
|
56
|
+
minWidth: 40,
|
|
57
|
+
width: 40,
|
|
58
|
+
height: 40,
|
|
59
|
+
padding: 0
|
|
60
|
+
},
|
|
61
|
+
actions: {
|
|
62
|
+
display: "flex",
|
|
63
|
+
gap: theme.spacing(1),
|
|
64
|
+
marginTop: theme.spacing(1)
|
|
65
|
+
},
|
|
66
|
+
success: {
|
|
67
|
+
color: theme.palette.success?.main ?? "#4caf50",
|
|
68
|
+
fontSize: "0.85rem",
|
|
69
|
+
marginTop: theme.spacing(1)
|
|
70
|
+
}
|
|
71
|
+
}));
|
|
72
|
+
function SettingsTab({ onBack, onCheckResult }) {
|
|
73
|
+
const classes = useStyles();
|
|
74
|
+
const libreChatApi = useApi(libreChatApiRef);
|
|
75
|
+
const { settings, saveSettings, clearSettings } = useLibreChatSettings();
|
|
76
|
+
const [apiKey, setApiKey] = useState(settings.apiKey);
|
|
77
|
+
const [saved, setSaved] = useState(false);
|
|
78
|
+
const [testing, setTesting] = useState(false);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
setApiKey(settings.apiKey);
|
|
81
|
+
}, [settings]);
|
|
82
|
+
const handleTest = async () => {
|
|
83
|
+
if (!apiKey.trim()) return;
|
|
84
|
+
setTesting(true);
|
|
85
|
+
try {
|
|
86
|
+
const reply = await libreChatApi.checkApiKey(apiKey.trim());
|
|
87
|
+
onCheckResult?.(reply);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : "Connection failed";
|
|
90
|
+
onCheckResult?.("", message);
|
|
91
|
+
} finally {
|
|
92
|
+
setTesting(false);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const handleSave = async () => {
|
|
96
|
+
await saveSettings({ apiKey });
|
|
97
|
+
setSaved(true);
|
|
98
|
+
setTimeout(() => setSaved(false), 2e3);
|
|
99
|
+
};
|
|
100
|
+
const handleClear = async () => {
|
|
101
|
+
await clearSettings();
|
|
102
|
+
setApiKey("");
|
|
103
|
+
setSaved(true);
|
|
104
|
+
setTimeout(() => setSaved(false), 2e3);
|
|
105
|
+
};
|
|
106
|
+
return /* @__PURE__ */ jsxs("div", { className: classes.root, children: [
|
|
107
|
+
/* @__PURE__ */ jsxs("div", { className: classes.header, children: [
|
|
108
|
+
/* @__PURE__ */ jsx(IconButton, { size: "small", onClick: onBack, title: "Back to chat", children: /* @__PURE__ */ jsx(ArrowBackIcon, { fontSize: "small" }) }),
|
|
109
|
+
/* @__PURE__ */ jsx(Typography, { className: classes.headerTitle, children: "Settings" })
|
|
110
|
+
] }),
|
|
111
|
+
/* @__PURE__ */ jsxs("div", { className: classes.content, children: [
|
|
112
|
+
/* @__PURE__ */ jsx(Typography, { className: classes.description, children: "Enter your LibreChat API key. Leave blank to use the default key set by your Backstage administrator." }),
|
|
113
|
+
/* @__PURE__ */ jsxs("div", { className: classes.apiKeyRow, children: [
|
|
114
|
+
/* @__PURE__ */ jsx(
|
|
115
|
+
TextField,
|
|
116
|
+
{
|
|
117
|
+
className: classes.apiKeyField,
|
|
118
|
+
label: "API Key",
|
|
119
|
+
variant: "outlined",
|
|
120
|
+
size: "small",
|
|
121
|
+
type: "password",
|
|
122
|
+
value: apiKey,
|
|
123
|
+
onChange: (e) => setApiKey(e.target.value),
|
|
124
|
+
placeholder: "Your LibreChat API key",
|
|
125
|
+
helperText: "Overrides the server-configured API key"
|
|
126
|
+
}
|
|
127
|
+
),
|
|
128
|
+
/* @__PURE__ */ jsx(
|
|
129
|
+
Button,
|
|
130
|
+
{
|
|
131
|
+
className: classes.testButton,
|
|
132
|
+
variant: "outlined",
|
|
133
|
+
color: "primary",
|
|
134
|
+
size: "small",
|
|
135
|
+
disabled: !apiKey.trim() || testing,
|
|
136
|
+
onClick: handleTest,
|
|
137
|
+
title: "Check API key \u2014 sends a test message",
|
|
138
|
+
children: testing ? /* @__PURE__ */ jsx(CircularProgress, { size: 20 }) : /* @__PURE__ */ jsx(CheckIcon, {})
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
] }),
|
|
142
|
+
/* @__PURE__ */ jsxs("div", { className: classes.actions, children: [
|
|
143
|
+
/* @__PURE__ */ jsx(
|
|
144
|
+
Button,
|
|
145
|
+
{
|
|
146
|
+
variant: "contained",
|
|
147
|
+
color: "primary",
|
|
148
|
+
size: "small",
|
|
149
|
+
onClick: handleSave,
|
|
150
|
+
children: "Save"
|
|
151
|
+
}
|
|
152
|
+
),
|
|
153
|
+
/* @__PURE__ */ jsx(Button, { variant: "outlined", size: "small", onClick: handleClear, children: "Clear" })
|
|
154
|
+
] }),
|
|
155
|
+
saved && /* @__PURE__ */ jsx(Typography, { className: classes.success, children: "Settings saved!" })
|
|
156
|
+
] })
|
|
157
|
+
] });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export { SettingsTab };
|
|
161
|
+
//# sourceMappingURL=SettingsTab.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SettingsTab.esm.js","sources":["../../src/components/SettingsTab.tsx"],"sourcesContent":["import React, {useState, useEffect} from \"react\";\nimport {makeStyles, Theme} from \"@material-ui/core/styles\";\nimport TextField from \"@material-ui/core/TextField\";\nimport Button from \"@material-ui/core/Button\";\nimport IconButton from \"@material-ui/core/IconButton\";\nimport Typography from \"@material-ui/core/Typography\";\nimport CircularProgress from \"@material-ui/core/CircularProgress\";\nimport ArrowBackIcon from \"@material-ui/icons/ArrowBack\";\nimport CheckIcon from \"@material-ui/icons/Check\";\nimport {useApi} from \"@backstage/frontend-plugin-api\";\nimport {libreChatApiRef} from \"../api\";\nimport {useLibreChatSettings} from \"../hooks/useLibreChatSettings\";\n\nconst useStyles = makeStyles((theme: Theme) => ({\n root: {\n display: \"flex\",\n flexDirection: \"column\",\n height: \"100%\",\n },\n header: {\n display: \"flex\",\n alignItems: \"center\",\n padding: theme.spacing(1, 2),\n borderBottom: `1px solid ${theme.palette.divider}`,\n gap: theme.spacing(1),\n minHeight: 48,\n },\n headerTitle: {\n fontWeight: 600,\n fontSize: \"0.95rem\",\n },\n content: {\n flex: 1,\n padding: theme.spacing(2),\n display: \"flex\",\n flexDirection: \"column\",\n gap: theme.spacing(2),\n overflow: \"auto\",\n },\n description: {\n fontSize: \"0.85rem\",\n color: theme.palette.text.secondary,\n marginBottom: theme.spacing(1),\n },\n apiKeyRow: {\n display: \"flex\",\n gap: theme.spacing(1),\n alignItems: \"flex-start\",\n },\n apiKeyField: {\n flex: 1,\n },\n testButton: {\n marginTop: 2,\n minWidth: 40,\n width: 40,\n height: 40,\n padding: 0,\n },\n actions: {\n display: \"flex\",\n gap: theme.spacing(1),\n marginTop: theme.spacing(1),\n },\n success: {\n color: theme.palette.success?.main ?? \"#4caf50\",\n fontSize: \"0.85rem\",\n marginTop: theme.spacing(1),\n },\n}));\n\ninterface SettingsTabProps {\n onBack: () => void;\n onCheckResult?: (reply: string, error?: string) => void;\n}\n\nexport function SettingsTab({onBack, onCheckResult}: SettingsTabProps) {\n const classes = useStyles();\n const libreChatApi = useApi(libreChatApiRef);\n const {settings, saveSettings, clearSettings} = useLibreChatSettings();\n\n const [apiKey, setApiKey] = useState(settings.apiKey);\n const [saved, setSaved] = useState(false);\n const [testing, setTesting] = useState(false);\n\n useEffect(() => {\n setApiKey(settings.apiKey);\n }, [settings]);\n\n const handleTest = async () => {\n if (!apiKey.trim()) return;\n\n setTesting(true);\n try {\n const reply = await libreChatApi.checkApiKey(apiKey.trim());\n onCheckResult?.(reply);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : \"Connection failed\";\n onCheckResult?.(\"\", message);\n } finally {\n setTesting(false);\n }\n };\n\n const handleSave = async () => {\n await saveSettings({apiKey});\n setSaved(true);\n setTimeout(() => setSaved(false), 2000);\n };\n\n const handleClear = async () => {\n await clearSettings();\n setApiKey(\"\");\n setSaved(true);\n setTimeout(() => setSaved(false), 2000);\n };\n\n return (\n <div className={classes.root}>\n <div className={classes.header}>\n <IconButton size=\"small\" onClick={onBack} title=\"Back to chat\">\n <ArrowBackIcon fontSize=\"small\" />\n </IconButton>\n <Typography className={classes.headerTitle}>Settings</Typography>\n </div>\n\n <div className={classes.content}>\n <Typography className={classes.description}>\n Enter your LibreChat API key. Leave blank to use the default key set\n by your Backstage administrator.\n </Typography>\n\n <div className={classes.apiKeyRow}>\n <TextField\n className={classes.apiKeyField}\n label=\"API Key\"\n variant=\"outlined\"\n size=\"small\"\n type=\"password\"\n value={apiKey}\n onChange={(e) => setApiKey(e.target.value)}\n placeholder=\"Your LibreChat API key\"\n helperText=\"Overrides the server-configured API key\"\n />\n <Button\n className={classes.testButton}\n variant=\"outlined\"\n color=\"primary\"\n size=\"small\"\n disabled={!apiKey.trim() || testing}\n onClick={handleTest}\n title=\"Check API key — sends a test message\"\n >\n {testing ? <CircularProgress size={20} /> : <CheckIcon />}\n </Button>\n </div>\n\n <div className={classes.actions}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n onClick={handleSave}\n >\n Save\n </Button>\n <Button variant=\"outlined\" size=\"small\" onClick={handleClear}>\n Clear\n </Button>\n </div>\n\n {saved && (\n <Typography className={classes.success}>Settings saved!</Typography>\n )}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;AAaA,MAAM,SAAA,GAAY,UAAA,CAAW,CAAC,KAAA,MAAkB;AAAA,EAC9C,IAAA,EAAM;AAAA,IACJ,OAAA,EAAS,MAAA;AAAA,IACT,aAAA,EAAe,QAAA;AAAA,IACf,MAAA,EAAQ;AAAA,GACV;AAAA,EACA,MAAA,EAAQ;AAAA,IACN,OAAA,EAAS,MAAA;AAAA,IACT,UAAA,EAAY,QAAA;AAAA,IACZ,OAAA,EAAS,KAAA,CAAM,OAAA,CAAQ,CAAA,EAAG,CAAC,CAAA;AAAA,IAC3B,YAAA,EAAc,CAAA,UAAA,EAAa,KAAA,CAAM,OAAA,CAAQ,OAAO,CAAA,CAAA;AAAA,IAChD,GAAA,EAAK,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAAA,IACpB,SAAA,EAAW;AAAA,GACb;AAAA,EACA,WAAA,EAAa;AAAA,IACX,UAAA,EAAY,GAAA;AAAA,IACZ,QAAA,EAAU;AAAA,GACZ;AAAA,EACA,OAAA,EAAS;AAAA,IACP,IAAA,EAAM,CAAA;AAAA,IACN,OAAA,EAAS,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAAA,IACxB,OAAA,EAAS,MAAA;AAAA,IACT,aAAA,EAAe,QAAA;AAAA,IACf,GAAA,EAAK,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAAA,IACpB,QAAA,EAAU;AAAA,GACZ;AAAA,EACA,WAAA,EAAa;AAAA,IACX,QAAA,EAAU,SAAA;AAAA,IACV,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,IAAA,CAAK,SAAA;AAAA,IAC1B,YAAA,EAAc,KAAA,CAAM,OAAA,CAAQ,CAAC;AAAA,GAC/B;AAAA,EACA,SAAA,EAAW;AAAA,IACT,OAAA,EAAS,MAAA;AAAA,IACT,GAAA,EAAK,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAAA,IACpB,UAAA,EAAY;AAAA,GACd;AAAA,EACA,WAAA,EAAa;AAAA,IACX,IAAA,EAAM;AAAA,GACR;AAAA,EACA,UAAA,EAAY;AAAA,IACV,SAAA,EAAW,CAAA;AAAA,IACX,QAAA,EAAU,EAAA;AAAA,IACV,KAAA,EAAO,EAAA;AAAA,IACP,MAAA,EAAQ,EAAA;AAAA,IACR,OAAA,EAAS;AAAA,GACX;AAAA,EACA,OAAA,EAAS;AAAA,IACP,OAAA,EAAS,MAAA;AAAA,IACT,GAAA,EAAK,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAAA,IACpB,SAAA,EAAW,KAAA,CAAM,OAAA,CAAQ,CAAC;AAAA,GAC5B;AAAA,EACA,OAAA,EAAS;AAAA,IACP,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,OAAA,EAAS,IAAA,IAAQ,SAAA;AAAA,IACtC,QAAA,EAAU,SAAA;AAAA,IACV,SAAA,EAAW,KAAA,CAAM,OAAA,CAAQ,CAAC;AAAA;AAE9B,CAAA,CAAE,CAAA;AAOK,SAAS,WAAA,CAAY,EAAC,MAAA,EAAQ,aAAA,EAAa,EAAqB;AACrE,EAAA,MAAM,UAAU,SAAA,EAAU;AAC1B,EAAA,MAAM,YAAA,GAAe,OAAO,eAAe,CAAA;AAC3C,EAAA,MAAM,EAAC,QAAA,EAAU,YAAA,EAAc,aAAA,KAAiB,oBAAA,EAAqB;AAErE,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,CAAA,GAAI,QAAA,CAAS,SAAS,MAAM,CAAA;AACpD,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAAS,KAAK,CAAA;AACxC,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,KAAK,CAAA;AAE5C,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,SAAA,CAAU,SAAS,MAAM,CAAA;AAAA,EAC3B,CAAA,EAAG,CAAC,QAAQ,CAAC,CAAA;AAEb,EAAA,MAAM,aAAa,YAAY;AAC7B,IAAA,IAAI,CAAC,MAAA,CAAO,IAAA,EAAK,EAAG;AAEpB,IAAA,UAAA,CAAW,IAAI,CAAA;AACf,IAAA,IAAI;AACF,MAAA,MAAM,QAAQ,MAAM,YAAA,CAAa,WAAA,CAAY,MAAA,CAAO,MAAM,CAAA;AAC1D,MAAA,aAAA,GAAgB,KAAK,CAAA;AAAA,IACvB,SAAS,GAAA,EAAc;AACrB,MAAA,MAAM,OAAA,GAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,mBAAA;AACrD,MAAA,aAAA,GAAgB,IAAI,OAAO,CAAA;AAAA,IAC7B,CAAA,SAAE;AACA,MAAA,UAAA,CAAW,KAAK,CAAA;AAAA,IAClB;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,aAAa,YAAY;AAC7B,IAAA,MAAM,YAAA,CAAa,EAAC,MAAA,EAAO,CAAA;AAC3B,IAAA,QAAA,CAAS,IAAI,CAAA;AACb,IAAA,UAAA,CAAW,MAAM,QAAA,CAAS,KAAK,CAAA,EAAG,GAAI,CAAA;AAAA,EACxC,CAAA;AAEA,EAAA,MAAM,cAAc,YAAY;AAC9B,IAAA,MAAM,aAAA,EAAc;AACpB,IAAA,SAAA,CAAU,EAAE,CAAA;AACZ,IAAA,QAAA,CAAS,IAAI,CAAA;AACb,IAAA,UAAA,CAAW,MAAM,QAAA,CAAS,KAAK,CAAA,EAAG,GAAI,CAAA;AAAA,EACxC,CAAA;AAEA,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAA,CAAQ,IAAA,EACtB,QAAA,EAAA;AAAA,oBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAA,CAAQ,MAAA,EACtB,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,UAAA,EAAA,EAAW,IAAA,EAAK,OAAA,EAAQ,OAAA,EAAS,MAAA,EAAQ,KAAA,EAAM,cAAA,EAC9C,QAAA,kBAAA,GAAA,CAAC,aAAA,EAAA,EAAc,QAAA,EAAS,OAAA,EAAQ,CAAA,EAClC,CAAA;AAAA,sBACA,GAAA,CAAC,UAAA,EAAA,EAAW,SAAA,EAAW,OAAA,CAAQ,aAAa,QAAA,EAAA,UAAA,EAAQ;AAAA,KAAA,EACtD,CAAA;AAAA,oBAEA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAA,CAAQ,OAAA,EACtB,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,UAAA,EAAA,EAAW,SAAA,EAAW,OAAA,CAAQ,WAAA,EAAa,QAAA,EAAA,uGAAA,EAG5C,CAAA;AAAA,sBAEA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAA,CAAQ,SAAA,EACtB,QAAA,EAAA;AAAA,wBAAA,GAAA;AAAA,UAAC,SAAA;AAAA,UAAA;AAAA,YACC,WAAW,OAAA,CAAQ,WAAA;AAAA,YACnB,KAAA,EAAM,SAAA;AAAA,YACN,OAAA,EAAQ,UAAA;AAAA,YACR,IAAA,EAAK,OAAA;AAAA,YACL,IAAA,EAAK,UAAA;AAAA,YACL,KAAA,EAAO,MAAA;AAAA,YACP,UAAU,CAAC,CAAA,KAAM,SAAA,CAAU,CAAA,CAAE,OAAO,KAAK,CAAA;AAAA,YACzC,WAAA,EAAY,wBAAA;AAAA,YACZ,UAAA,EAAW;AAAA;AAAA,SACb;AAAA,wBACA,GAAA;AAAA,UAAC,MAAA;AAAA,UAAA;AAAA,YACC,WAAW,OAAA,CAAQ,UAAA;AAAA,YACnB,OAAA,EAAQ,UAAA;AAAA,YACR,KAAA,EAAM,SAAA;AAAA,YACN,IAAA,EAAK,OAAA;AAAA,YACL,QAAA,EAAU,CAAC,MAAA,CAAO,IAAA,EAAK,IAAK,OAAA;AAAA,YAC5B,OAAA,EAAS,UAAA;AAAA,YACT,KAAA,EAAM,2CAAA;AAAA,YAEL,oCAAU,GAAA,CAAC,gBAAA,EAAA,EAAiB,MAAM,EAAA,EAAI,CAAA,uBAAM,SAAA,EAAA,EAAU;AAAA;AAAA;AACzD,OAAA,EACF,CAAA;AAAA,sBAEA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAA,CAAQ,OAAA,EACtB,QAAA,EAAA;AAAA,wBAAA,GAAA;AAAA,UAAC,MAAA;AAAA,UAAA;AAAA,YACC,OAAA,EAAQ,WAAA;AAAA,YACR,KAAA,EAAM,SAAA;AAAA,YACN,IAAA,EAAK,OAAA;AAAA,YACL,OAAA,EAAS,UAAA;AAAA,YACV,QAAA,EAAA;AAAA;AAAA,SAED;AAAA,wBACA,GAAA,CAAC,UAAO,OAAA,EAAQ,UAAA,EAAW,MAAK,OAAA,EAAQ,OAAA,EAAS,aAAa,QAAA,EAAA,OAAA,EAE9D;AAAA,OAAA,EACF,CAAA;AAAA,MAEC,yBACC,GAAA,CAAC,UAAA,EAAA,EAAW,SAAA,EAAW,OAAA,CAAQ,SAAS,QAAA,EAAA,iBAAA,EAAe;AAAA,KAAA,EAE3D;AAAA,GAAA,EACF,CAAA;AAEJ;;;;"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { useApi, storageApiRef } from '@backstage/frontend-plugin-api';
|
|
3
|
+
|
|
4
|
+
const STORAGE_BUCKET = "librechat-settings";
|
|
5
|
+
const API_KEY_KEY = "apiKey";
|
|
6
|
+
function useLibreChatSettings() {
|
|
7
|
+
const storageApi = useApi(storageApiRef);
|
|
8
|
+
const bucket = storageApi.forBucket(STORAGE_BUCKET);
|
|
9
|
+
const [settings, setSettings] = useState({
|
|
10
|
+
apiKey: ""
|
|
11
|
+
});
|
|
12
|
+
const safeGet = useCallback(
|
|
13
|
+
(key) => {
|
|
14
|
+
try {
|
|
15
|
+
return bucket.snapshot(key).value ?? "";
|
|
16
|
+
} catch {
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
[bucket]
|
|
21
|
+
);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setSettings({
|
|
24
|
+
apiKey: safeGet(API_KEY_KEY)
|
|
25
|
+
});
|
|
26
|
+
const sub = bucket.observe$(API_KEY_KEY).subscribe((next) => {
|
|
27
|
+
setSettings((prev) => ({ ...prev, apiKey: next.value ?? "" }));
|
|
28
|
+
});
|
|
29
|
+
return () => {
|
|
30
|
+
sub.unsubscribe();
|
|
31
|
+
};
|
|
32
|
+
}, [bucket, safeGet]);
|
|
33
|
+
const saveSettings = useCallback(
|
|
34
|
+
async (newSettings) => {
|
|
35
|
+
if (newSettings.apiKey) {
|
|
36
|
+
await bucket.set(API_KEY_KEY, newSettings.apiKey);
|
|
37
|
+
} else {
|
|
38
|
+
await bucket.remove(API_KEY_KEY);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
[bucket]
|
|
42
|
+
);
|
|
43
|
+
const clearSettings = useCallback(async () => {
|
|
44
|
+
await bucket.remove(API_KEY_KEY);
|
|
45
|
+
}, [bucket]);
|
|
46
|
+
return { settings, saveSettings, clearSettings };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { useLibreChatSettings };
|
|
50
|
+
//# sourceMappingURL=useLibreChatSettings.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLibreChatSettings.esm.js","sources":["../../src/hooks/useLibreChatSettings.ts"],"sourcesContent":["import {useState, useEffect, useCallback} from \"react\";\nimport {useApi, storageApiRef} from \"@backstage/frontend-plugin-api\";\n\nconst STORAGE_BUCKET = \"librechat-settings\";\nconst API_KEY_KEY = \"apiKey\";\n\nexport interface LibreChatSettings {\n apiKey: string;\n}\n\nexport function useLibreChatSettings() {\n const storageApi = useApi(storageApiRef);\n const bucket = storageApi.forBucket(STORAGE_BUCKET);\n\n const [settings, setSettings] = useState<LibreChatSettings>({\n apiKey: \"\",\n });\n\n const safeGet = useCallback(\n (key: string): string => {\n try {\n return bucket.snapshot<string>(key).value ?? \"\";\n } catch {\n return \"\";\n }\n },\n [bucket],\n );\n\n // Load settings on mount\n useEffect(() => {\n setSettings({\n apiKey: safeGet(API_KEY_KEY),\n });\n\n // Subscribe to changes\n const sub = bucket.observe$<string>(API_KEY_KEY).subscribe((next) => {\n setSettings((prev) => ({...prev, apiKey: next.value ?? \"\"}));\n });\n\n return () => {\n sub.unsubscribe();\n };\n }, [bucket, safeGet]);\n\n const saveSettings = useCallback(\n async (newSettings: LibreChatSettings) => {\n if (newSettings.apiKey) {\n await bucket.set(API_KEY_KEY, newSettings.apiKey);\n } else {\n await bucket.remove(API_KEY_KEY);\n }\n },\n [bucket],\n );\n\n const clearSettings = useCallback(async () => {\n await bucket.remove(API_KEY_KEY);\n }, [bucket]);\n\n return {settings, saveSettings, clearSettings};\n}\n"],"names":[],"mappings":";;;AAGA,MAAM,cAAA,GAAiB,oBAAA;AACvB,MAAM,WAAA,GAAc,QAAA;AAMb,SAAS,oBAAA,GAAuB;AACrC,EAAA,MAAM,UAAA,GAAa,OAAO,aAAa,CAAA;AACvC,EAAA,MAAM,MAAA,GAAS,UAAA,CAAW,SAAA,CAAU,cAAc,CAAA;AAElD,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAI,QAAA,CAA4B;AAAA,IAC1D,MAAA,EAAQ;AAAA,GACT,CAAA;AAED,EAAA,MAAM,OAAA,GAAU,WAAA;AAAA,IACd,CAAC,GAAA,KAAwB;AACvB,MAAA,IAAI;AACF,QAAA,OAAO,MAAA,CAAO,QAAA,CAAiB,GAAG,CAAA,CAAE,KAAA,IAAS,EAAA;AAAA,MAC/C,CAAA,CAAA,MAAQ;AACN,QAAA,OAAO,EAAA;AAAA,MACT;AAAA,IACF,CAAA;AAAA,IACA,CAAC,MAAM;AAAA,GACT;AAGA,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,WAAA,CAAY;AAAA,MACV,MAAA,EAAQ,QAAQ,WAAW;AAAA,KAC5B,CAAA;AAGD,IAAA,MAAM,MAAM,MAAA,CAAO,QAAA,CAAiB,WAAW,CAAA,CAAE,SAAA,CAAU,CAAC,IAAA,KAAS;AACnE,MAAA,WAAA,CAAY,CAAC,UAAU,EAAC,GAAG,MAAM,MAAA,EAAQ,IAAA,CAAK,KAAA,IAAS,EAAA,EAAE,CAAE,CAAA;AAAA,IAC7D,CAAC,CAAA;AAED,IAAA,OAAO,MAAM;AACX,MAAA,GAAA,CAAI,WAAA,EAAY;AAAA,IAClB,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,MAAA,EAAQ,OAAO,CAAC,CAAA;AAEpB,EAAA,MAAM,YAAA,GAAe,WAAA;AAAA,IACnB,OAAO,WAAA,KAAmC;AACxC,MAAA,IAAI,YAAY,MAAA,EAAQ;AACtB,QAAA,MAAM,MAAA,CAAO,GAAA,CAAI,WAAA,EAAa,WAAA,CAAY,MAAM,CAAA;AAAA,MAClD,CAAA,MAAO;AACL,QAAA,MAAM,MAAA,CAAO,OAAO,WAAW,CAAA;AAAA,MACjC;AAAA,IACF,CAAA;AAAA,IACA,CAAC,MAAM;AAAA,GACT;AAEA,EAAA,MAAM,aAAA,GAAgB,YAAY,YAAY;AAC5C,IAAA,MAAM,MAAA,CAAO,OAAO,WAAW,CAAA;AAAA,EACjC,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEX,EAAA,OAAO,EAAC,QAAA,EAAU,YAAA,EAAc,aAAA,EAAa;AAC/C;;;;"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
function normalizeTitle(title) {
|
|
4
|
+
const parts = title.split("|").map((part) => part.trim()).filter(Boolean);
|
|
5
|
+
const deduped = parts.filter((part, index) => part !== parts[index - 1]);
|
|
6
|
+
return deduped.join(" | ");
|
|
7
|
+
}
|
|
8
|
+
function usePageContext() {
|
|
9
|
+
const [context, setContext] = useState(() => ({
|
|
10
|
+
url: window.location.href,
|
|
11
|
+
path: window.location.pathname,
|
|
12
|
+
title: normalizeTitle(document.title)
|
|
13
|
+
}));
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const update = () => {
|
|
16
|
+
setContext({
|
|
17
|
+
url: window.location.href,
|
|
18
|
+
path: window.location.pathname,
|
|
19
|
+
title: normalizeTitle(document.title)
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
window.addEventListener("popstate", update);
|
|
23
|
+
const titleEl = document.querySelector("title");
|
|
24
|
+
let titleObserver;
|
|
25
|
+
if (titleEl) {
|
|
26
|
+
titleObserver = new MutationObserver(update);
|
|
27
|
+
titleObserver.observe(titleEl, {
|
|
28
|
+
childList: true,
|
|
29
|
+
characterData: true,
|
|
30
|
+
subtree: true
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const origPush = history.pushState.bind(history);
|
|
34
|
+
const origReplace = history.replaceState.bind(history);
|
|
35
|
+
history.pushState = (...args) => {
|
|
36
|
+
origPush(...args);
|
|
37
|
+
update();
|
|
38
|
+
};
|
|
39
|
+
history.replaceState = (...args) => {
|
|
40
|
+
origReplace(...args);
|
|
41
|
+
update();
|
|
42
|
+
};
|
|
43
|
+
return () => {
|
|
44
|
+
window.removeEventListener("popstate", update);
|
|
45
|
+
titleObserver?.disconnect();
|
|
46
|
+
history.pushState = origPush;
|
|
47
|
+
history.replaceState = origReplace;
|
|
48
|
+
};
|
|
49
|
+
}, []);
|
|
50
|
+
return context;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { usePageContext };
|
|
54
|
+
//# sourceMappingURL=usePageContext.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePageContext.esm.js","sources":["../../src/hooks/usePageContext.ts"],"sourcesContent":["import {useState, useEffect} from \"react\";\n\nexport interface PageContext {\n url: string;\n path: string;\n title: string;\n}\n\n/**\n * Normalizes a browser document title by collapsing repeated segments.\n *\n * Backstage builds titles as `<page> | <app.title>`. On pages without a\n * dedicated title the page segment falls back to the app title, producing\n * duplicates like `Backstage | Backstage`. This collapses adjacent repeats.\n */\nfunction normalizeTitle(title: string): string {\n const parts = title\n .split(\"|\")\n .map((part) => part.trim())\n .filter(Boolean);\n const deduped = parts.filter((part, index) => part !== parts[index - 1]);\n return deduped.join(\" | \");\n}\n\n/**\n * Hook that tracks the current Backstage page context.\n * Captures URL, path, and document title so it can be sent\n * alongside chat messages for contextual answers.\n */\nexport function usePageContext(): PageContext {\n const [context, setContext] = useState<PageContext>(() => ({\n url: window.location.href,\n path: window.location.pathname,\n title: normalizeTitle(document.title),\n }));\n\n useEffect(() => {\n const update = () => {\n setContext({\n url: window.location.href,\n path: window.location.pathname,\n title: normalizeTitle(document.title),\n });\n };\n\n // Listen for navigation changes (pushState / popState)\n window.addEventListener(\"popstate\", update);\n\n // Observe title changes via MutationObserver\n const titleEl = document.querySelector(\"title\");\n let titleObserver: MutationObserver | undefined;\n if (titleEl) {\n titleObserver = new MutationObserver(update);\n titleObserver.observe(titleEl, {\n childList: true,\n characterData: true,\n subtree: true,\n });\n }\n\n // Patch pushState/replaceState to detect SPA navigation\n const origPush = history.pushState.bind(history);\n const origReplace = history.replaceState.bind(history);\n history.pushState = (...args) => {\n origPush(...args);\n update();\n };\n history.replaceState = (...args) => {\n origReplace(...args);\n update();\n };\n\n return () => {\n window.removeEventListener(\"popstate\", update);\n titleObserver?.disconnect();\n history.pushState = origPush;\n history.replaceState = origReplace;\n };\n }, []);\n\n return context;\n}\n"],"names":[],"mappings":";;AAeA,SAAS,eAAe,KAAA,EAAuB;AAC7C,EAAA,MAAM,KAAA,GAAQ,KAAA,CACX,KAAA,CAAM,GAAG,CAAA,CACT,GAAA,CAAI,CAAC,IAAA,KAAS,IAAA,CAAK,IAAA,EAAM,CAAA,CACzB,OAAO,OAAO,CAAA;AACjB,EAAA,MAAM,OAAA,GAAU,KAAA,CAAM,MAAA,CAAO,CAAC,IAAA,EAAM,UAAU,IAAA,KAAS,KAAA,CAAM,KAAA,GAAQ,CAAC,CAAC,CAAA;AACvE,EAAA,OAAO,OAAA,CAAQ,KAAK,KAAK,CAAA;AAC3B;AAOO,SAAS,cAAA,GAA8B;AAC5C,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAsB,OAAO;AAAA,IACzD,GAAA,EAAK,OAAO,QAAA,CAAS,IAAA;AAAA,IACrB,IAAA,EAAM,OAAO,QAAA,CAAS,QAAA;AAAA,IACtB,KAAA,EAAO,cAAA,CAAe,QAAA,CAAS,KAAK;AAAA,GACtC,CAAE,CAAA;AAEF,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,SAAS,MAAM;AACnB,MAAA,UAAA,CAAW;AAAA,QACT,GAAA,EAAK,OAAO,QAAA,CAAS,IAAA;AAAA,QACrB,IAAA,EAAM,OAAO,QAAA,CAAS,QAAA;AAAA,QACtB,KAAA,EAAO,cAAA,CAAe,QAAA,CAAS,KAAK;AAAA,OACrC,CAAA;AAAA,IACH,CAAA;AAGA,IAAA,MAAA,CAAO,gBAAA,CAAiB,YAAY,MAAM,CAAA;AAG1C,IAAA,MAAM,OAAA,GAAU,QAAA,CAAS,aAAA,CAAc,OAAO,CAAA;AAC9C,IAAA,IAAI,aAAA;AACJ,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,aAAA,GAAgB,IAAI,iBAAiB,MAAM,CAAA;AAC3C,MAAA,aAAA,CAAc,QAAQ,OAAA,EAAS;AAAA,QAC7B,SAAA,EAAW,IAAA;AAAA,QACX,aAAA,EAAe,IAAA;AAAA,QACf,OAAA,EAAS;AAAA,OACV,CAAA;AAAA,IACH;AAGA,IAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,SAAA,CAAU,IAAA,CAAK,OAAO,CAAA;AAC/C,IAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,YAAA,CAAa,IAAA,CAAK,OAAO,CAAA;AACrD,IAAA,OAAA,CAAQ,SAAA,GAAY,IAAI,IAAA,KAAS;AAC/B,MAAA,QAAA,CAAS,GAAG,IAAI,CAAA;AAChB,MAAA,MAAA,EAAO;AAAA,IACT,CAAA;AACA,IAAA,OAAA,CAAQ,YAAA,GAAe,IAAI,IAAA,KAAS;AAClC,MAAA,WAAA,CAAY,GAAG,IAAI,CAAA;AACnB,MAAA,MAAA,EAAO;AAAA,IACT,CAAA;AAEA,IAAA,OAAO,MAAM;AACX,MAAA,MAAA,CAAO,mBAAA,CAAoB,YAAY,MAAM,CAAA;AAC7C,MAAA,aAAA,EAAe,UAAA,EAAW;AAC1B,MAAA,OAAA,CAAQ,SAAA,GAAY,QAAA;AACpB,MAAA,OAAA,CAAQ,YAAA,GAAe,WAAA;AAAA,IACzB,CAAA;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO,OAAA;AACT;;;;"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as _backstage_frontend_plugin_api from '@backstage/frontend-plugin-api';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The LibreChat frontend plugin.
|
|
6
|
+
*
|
|
7
|
+
* Provides an AI chat bubble overlay powered by LibreChat's Agents API.
|
|
8
|
+
* Controlled via `librechat.enabled` config (defaults to true).
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
declare const libreChatPlugin: _backstage_frontend_plugin_api.OverridableFrontendPlugin<{}, {}, {
|
|
13
|
+
"api:librechat/librechat": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
|
|
14
|
+
kind: "api";
|
|
15
|
+
name: "librechat";
|
|
16
|
+
config: {};
|
|
17
|
+
configInput: {};
|
|
18
|
+
output: _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.AnyApiFactory, "core.api.factory", {}>;
|
|
19
|
+
inputs: {};
|
|
20
|
+
params: <TApi, TImpl extends TApi, TDeps extends {
|
|
21
|
+
[x: string]: unknown;
|
|
22
|
+
}>(params: _backstage_frontend_plugin_api.ApiFactory<TApi, TImpl, TDeps>) => _backstage_frontend_plugin_api.ExtensionBlueprintParams<_backstage_frontend_plugin_api.AnyApiFactory>;
|
|
23
|
+
}>;
|
|
24
|
+
"app-root-element:librechat/chat-bubble": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
|
|
25
|
+
kind: "app-root-element";
|
|
26
|
+
name: "chat-bubble";
|
|
27
|
+
config: {};
|
|
28
|
+
configInput: {};
|
|
29
|
+
output: _backstage_frontend_plugin_api.ExtensionDataRef<React.JSX.Element, "core.reactElement", {}>;
|
|
30
|
+
inputs: {};
|
|
31
|
+
params: {
|
|
32
|
+
element: JSX.Element;
|
|
33
|
+
};
|
|
34
|
+
}>;
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
/** A single chat message. */
|
|
38
|
+
interface ChatMessage {
|
|
39
|
+
role: "user" | "assistant" | "system";
|
|
40
|
+
content: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* API for communicating with LibreChat through the backend proxy.
|
|
44
|
+
*
|
|
45
|
+
* @public
|
|
46
|
+
*/
|
|
47
|
+
interface LibreChatApi {
|
|
48
|
+
/**
|
|
49
|
+
* Sends messages to LibreChat and yields streamed content chunks.
|
|
50
|
+
*
|
|
51
|
+
* @param messages - Conversation history
|
|
52
|
+
* @param options - Optional override for apiKey
|
|
53
|
+
* @returns An async generator yielding content strings as they arrive
|
|
54
|
+
*/
|
|
55
|
+
sendMessage(messages: ChatMessage[], options?: {
|
|
56
|
+
apiKey?: string;
|
|
57
|
+
}): AsyncGenerator<string, void, unknown>;
|
|
58
|
+
/**
|
|
59
|
+
* Checks that the given API key is valid by sending a short test message.
|
|
60
|
+
*
|
|
61
|
+
* @param apiKey - API key to validate
|
|
62
|
+
* @returns The assistant's reply
|
|
63
|
+
*/
|
|
64
|
+
checkApiKey(apiKey: string): Promise<string>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* API reference for the LibreChat API.
|
|
68
|
+
*
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
declare const libreChatApiRef: _backstage_frontend_plugin_api.ApiRef<LibreChatApi> & {
|
|
72
|
+
readonly $$type: "@backstage/ApiRef";
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export { libreChatPlugin as default, libreChatApiRef };
|
|
76
|
+
export type { ChatMessage, LibreChatApi };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { ApiBlueprint, createApiFactory, configApiRef, fetchApiRef, AppRootElementBlueprint, createFrontendPlugin } from '@backstage/frontend-plugin-api';
|
|
3
|
+
import { DefaultLibreChatApi, libreChatApiRef } from './api.esm.js';
|
|
4
|
+
import { ChatBubble } from './components/ChatBubble.esm.js';
|
|
5
|
+
|
|
6
|
+
const libreChatApi = ApiBlueprint.make({
|
|
7
|
+
name: "librechat",
|
|
8
|
+
params: (defineParams) => defineParams(
|
|
9
|
+
createApiFactory({
|
|
10
|
+
api: libreChatApiRef,
|
|
11
|
+
deps: { fetchApi: fetchApiRef, configApi: configApiRef },
|
|
12
|
+
factory: ({ fetchApi, configApi }) => new DefaultLibreChatApi({ fetchApi, configApi })
|
|
13
|
+
})
|
|
14
|
+
)
|
|
15
|
+
});
|
|
16
|
+
const chatBubbleRootElement = AppRootElementBlueprint.make({
|
|
17
|
+
name: "chat-bubble",
|
|
18
|
+
params: {
|
|
19
|
+
element: /* @__PURE__ */ jsx(ChatBubble, {})
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const libreChatPlugin = createFrontendPlugin({
|
|
23
|
+
pluginId: "librechat",
|
|
24
|
+
extensions: [libreChatApi, chatBubbleRootElement]
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export { libreChatPlugin };
|
|
28
|
+
//# sourceMappingURL=plugin.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.esm.js","sources":["../src/plugin.tsx"],"sourcesContent":["import React from \"react\";\nimport {\n createFrontendPlugin,\n ApiBlueprint,\n AppRootElementBlueprint,\n fetchApiRef,\n configApiRef,\n createApiFactory,\n} from \"@backstage/frontend-plugin-api\";\nimport {libreChatApiRef, DefaultLibreChatApi} from \"./api\";\nimport {ChatBubble} from \"./components/ChatBubble\";\n\n/** Utility API extension providing the LibreChat API client. */\nconst libreChatApi = ApiBlueprint.make({\n name: \"librechat\",\n params: defineParams =>\n defineParams(\n createApiFactory({\n api: libreChatApiRef,\n deps: {fetchApi: fetchApiRef, configApi: configApiRef},\n factory: ({fetchApi, configApi}) =>\n new DefaultLibreChatApi({fetchApi, configApi}),\n }),\n ),\n});\n\n/** Global chat bubble overlay rendered across all pages. */\nconst chatBubbleRootElement = AppRootElementBlueprint.make({\n name: \"chat-bubble\",\n params: {\n element: <ChatBubble />,\n },\n});\n\n/**\n * The LibreChat frontend plugin.\n *\n * Provides an AI chat bubble overlay powered by LibreChat's Agents API.\n * Controlled via `librechat.enabled` config (defaults to true).\n *\n * @public\n */\nexport const libreChatPlugin = createFrontendPlugin({\n pluginId: \"librechat\",\n extensions: [libreChatApi, chatBubbleRootElement],\n});\n"],"names":[],"mappings":";;;;;AAaA,MAAM,YAAA,GAAe,aAAa,IAAA,CAAK;AAAA,EACrC,IAAA,EAAM,WAAA;AAAA,EACN,QAAQ,CAAA,YAAA,KACN,YAAA;AAAA,IACE,gBAAA,CAAiB;AAAA,MACf,GAAA,EAAK,eAAA;AAAA,MACL,IAAA,EAAM,EAAC,QAAA,EAAU,WAAA,EAAa,WAAW,YAAA,EAAY;AAAA,MACrD,OAAA,EAAS,CAAC,EAAC,QAAA,EAAU,SAAA,EAAS,KAC5B,IAAI,mBAAA,CAAoB,EAAC,QAAA,EAAU,SAAA,EAAU;AAAA,KAChD;AAAA;AAEP,CAAC,CAAA;AAGD,MAAM,qBAAA,GAAwB,wBAAwB,IAAA,CAAK;AAAA,EACzD,IAAA,EAAM,aAAA;AAAA,EACN,MAAA,EAAQ;AAAA,IACN,OAAA,sBAAU,UAAA,EAAA,EAAW;AAAA;AAEzB,CAAC,CAAA;AAUM,MAAM,kBAAkB,oBAAA,CAAqB;AAAA,EAClD,QAAA,EAAU,WAAA;AAAA,EACV,UAAA,EAAY,CAAC,YAAA,EAAc,qBAAqB;AAClD,CAAC;;;;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nospt/backstage-plugin-librechat",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Backstage frontend plugin adding an AI chat bubble powered by LibreChat Agents API",
|
|
5
|
+
"main": "dist/index.esm.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"backstage": {
|
|
12
|
+
"role": "frontend-plugin",
|
|
13
|
+
"pluginId": "librechat",
|
|
14
|
+
"pluginPackages": [
|
|
15
|
+
"@nospt/backstage-plugin-librechat",
|
|
16
|
+
"@nospt/backstage-plugin-librechat-backend"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"configSchema": "config.d.ts",
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"scripts": {
|
|
22
|
+
"start": "backstage-cli package start",
|
|
23
|
+
"build": "backstage-cli package build",
|
|
24
|
+
"tsc": "tsc",
|
|
25
|
+
"lint": "backstage-cli package lint",
|
|
26
|
+
"clean": "backstage-cli package clean",
|
|
27
|
+
"test": "backstage-cli package test"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@backstage/core-components": "^0.18.0",
|
|
31
|
+
"@backstage/frontend-plugin-api": "^0.15.0",
|
|
32
|
+
"@backstage/theme": "^0.7.0",
|
|
33
|
+
"@material-ui/core": "^4.12.4",
|
|
34
|
+
"@material-ui/icons": "^4.11.3",
|
|
35
|
+
"lodash": "^4.17.21",
|
|
36
|
+
"react-markdown": "^9.0.1",
|
|
37
|
+
"react-router-dom": "^6.20.0",
|
|
38
|
+
"remark-gfm": "^4.0.0",
|
|
39
|
+
"use-sync-external-store": "^1.2.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@backstage/cli": "^0.36.0",
|
|
43
|
+
"@backstage/config": "^1.3.0",
|
|
44
|
+
"@backstage/core-app-api": "^1.15.0",
|
|
45
|
+
"@backstage/frontend-dev-utils": "^0.1.0",
|
|
46
|
+
"@jest/environment-jsdom-abstract": "^30.0.0",
|
|
47
|
+
"@types/react": "^18.2.0",
|
|
48
|
+
"@types/react-dom": "^18.2.0",
|
|
49
|
+
"i18next": "^23.0.0",
|
|
50
|
+
"jest": "^29.0.0 || ^30.0.0",
|
|
51
|
+
"jsdom": "^27.1.0",
|
|
52
|
+
"prop-types": "^15.8.1",
|
|
53
|
+
"react": "^18.2.0",
|
|
54
|
+
"react-dom": "^18.2.0",
|
|
55
|
+
"typescript": "~5.4.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"react": "^17.0.0 || ^18.0.0",
|
|
59
|
+
"react-dom": "^17.0.0 || ^18.0.0",
|
|
60
|
+
"react-router-dom": "^6.0.0"
|
|
61
|
+
},
|
|
62
|
+
"files": [
|
|
63
|
+
"dist"
|
|
64
|
+
]
|
|
65
|
+
}
|