@parallelworks/ui 0.21.2 → 0.23.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/dist/ai/adapter/mock/mockAdapter.js +2 -1
- package/dist/ai/adapter/openai/openaiAdapter.js +6 -3
- package/dist/ai/adapter/openai/wire.d.ts +2 -1
- package/dist/ai/adapter/types.d.ts +2 -1
- package/dist/ai/components/ChatEmptyState.js +2 -0
- package/dist/ai/components/ChatInput.d.ts +4 -2
- package/dist/ai/components/ChatInput.js +36 -10
- package/dist/ai/components/ChatMessage.js +9 -2
- package/dist/ai/components/ChatMessageList.d.ts +3 -1
- package/dist/ai/components/ChatMessageList.js +2 -2
- package/dist/ai/components/ChatThread.js +2 -0
- package/dist/ai/components/ModelSelector.js +94 -62
- package/dist/ai/components/PastedText.d.ts +15 -0
- package/dist/ai/components/PastedText.js +59 -0
- package/dist/ai/components/ProviderIssueBanner.d.ts +1 -0
- package/dist/ai/components/ProviderIssueBanner.js +30 -0
- package/dist/ai/components/usePasteCards.d.ts +30 -0
- package/dist/ai/components/usePasteCards.js +129 -0
- package/dist/ai/core/ChatProvider.d.ts +2 -1
- package/dist/ai/core/ChatProvider.js +7 -3
- package/dist/ai/core/chatReducer.d.ts +3 -1
- package/dist/ai/core/chatReducer.js +2 -0
- package/dist/ai/core/pastes.d.ts +14 -0
- package/dist/ai/core/pastes.js +43 -0
- package/dist/ai/core/providerIssues.d.ts +2 -0
- package/dist/ai/core/providerIssues.js +7 -0
- package/dist/ai/index.d.ts +4 -0
- package/dist/ai/index.js +4 -1
- package/dist/ai/strings.d.ts +14 -0
- package/dist/ai/strings.js +14 -0
- package/dist/ai/types.d.ts +18 -0
- package/dist/components/Provider.d.ts +1 -0
- package/dist/components/Provider.js +1 -0
- package/dist/file-explorer/FileExplorer.d.ts +7 -1
- package/dist/file-explorer/FileExplorer.js +3 -1
- package/dist/file-explorer/FilePreview/Viewer.js +33 -18
- package/dist/file-explorer/FilePreview/previewType.d.ts +3 -1
- package/dist/file-explorer/FilePreview/previewType.js +18 -1
- package/dist/file-explorer/FilePreview/readCappedBytes.d.ts +4 -0
- package/dist/file-explorer/FilePreview/readCappedBytes.js +7 -1
- package/dist/file-explorer/FilePreview/useObjectText.js +3 -3
- package/dist/file-explorer/index.d.ts +1 -1
- package/dist/file-explorer/lib/types.d.ts +11 -0
- package/dist/list/UserHoverCard.js +28 -9
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -45,7 +45,8 @@ function createMockChatAdapter(options) {
|
|
|
45
45
|
provider: "Mock Provider",
|
|
46
46
|
tool_calling_mode: "none"
|
|
47
47
|
}],
|
|
48
|
-
unreachableSessions: []
|
|
48
|
+
unreachableSessions: [],
|
|
49
|
+
providerIssues: []
|
|
49
50
|
};
|
|
50
51
|
} },
|
|
51
52
|
streamCompletion: store.recordingStream(async (req, handlers, signal) => {
|
|
@@ -31,16 +31,19 @@ function createOpenAIChatAdapter(options) {
|
|
|
31
31
|
const providerLabel = providerLabelFor(baseUrl);
|
|
32
32
|
if (options.models?.length) return {
|
|
33
33
|
models: options.models.map((id) => staticModel(id, providerLabel)),
|
|
34
|
-
unreachableSessions: []
|
|
34
|
+
unreachableSessions: [],
|
|
35
|
+
providerIssues: []
|
|
35
36
|
};
|
|
36
37
|
const res = await doFetch(`${baseUrl}/models`, { headers });
|
|
37
38
|
if (!res.ok) throw new ChatAdapterError(await errorMessageFromResponse(res), { status: res.status });
|
|
39
|
+
const body = await res.json();
|
|
38
40
|
return {
|
|
39
|
-
models: (
|
|
41
|
+
models: (body.data ?? []).map((m) => ({
|
|
40
42
|
...staticModel(m.id, providerLabel),
|
|
41
43
|
...m
|
|
42
44
|
})),
|
|
43
|
-
unreachableSessions: []
|
|
45
|
+
unreachableSessions: [],
|
|
46
|
+
providerIssues: body.provider_issues ?? []
|
|
44
47
|
};
|
|
45
48
|
} },
|
|
46
49
|
streamCompletion: store.recordingStream(createOpenAICompatibleStream({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatModel, TokenUsage, ToolCall, ToolCallDelta, UnreachableSession } from '../../types.js';
|
|
1
|
+
import type { ChatModel, ProviderIssue, TokenUsage, ToolCall, ToolCallDelta, UnreachableSession } from '../../types.js';
|
|
2
2
|
export interface CompletionMessage {
|
|
3
3
|
role: string;
|
|
4
4
|
content?: unknown;
|
|
@@ -37,4 +37,5 @@ export interface ModelsWireResponse {
|
|
|
37
37
|
object: string;
|
|
38
38
|
data: ChatModel[] | null;
|
|
39
39
|
unreachable_sessions?: UnreachableSession[] | null;
|
|
40
|
+
provider_issues?: ProviderIssue[] | null;
|
|
40
41
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Allocation, ApprovalPart, AttachmentRef, ChatModel, Conversation, ConversationSummary, ProviderInfo, ShareGroup, SharePermission, SharePermissionLevel, TodoItem, ToolCall, UnreachableSession } from '../types.js';
|
|
1
|
+
import type { Allocation, ApprovalPart, AttachmentRef, ChatModel, Conversation, ConversationSummary, ProviderInfo, ProviderIssue, ShareGroup, SharePermission, SharePermissionLevel, TodoItem, ToolCall, UnreachableSession } from '../types.js';
|
|
2
2
|
import type { CompletionMessage } from './openai/wire.js';
|
|
3
3
|
export declare class ChatAdapterError extends Error {
|
|
4
4
|
status?: number | undefined;
|
|
@@ -10,6 +10,7 @@ export declare class ChatAdapterError extends Error {
|
|
|
10
10
|
export interface ModelsList {
|
|
11
11
|
models: ChatModel[];
|
|
12
12
|
unreachableSessions: UnreachableSession[];
|
|
13
|
+
providerIssues: ProviderIssue[];
|
|
13
14
|
}
|
|
14
15
|
export interface StreamRequest {
|
|
15
16
|
model: string;
|
|
@@ -11,6 +11,7 @@ import useDragDrop from "../core/useDragDrop.js";
|
|
|
11
11
|
import { ComposerControls, ConnectToolsLink } from "./ComposerChrome.js";
|
|
12
12
|
import DragOverlay from "./DragOverlay.js";
|
|
13
13
|
import ChatInput from "./ChatInput.js";
|
|
14
|
+
import ProviderIssueBanner from "./ProviderIssueBanner.js";
|
|
14
15
|
import cx from "classnames";
|
|
15
16
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
16
17
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -75,6 +76,7 @@ function ChatEmptyState({ targetSession }) {
|
|
|
75
76
|
className: "flex-1 flex flex-col min-w-0",
|
|
76
77
|
children: [
|
|
77
78
|
/* @__PURE__ */ jsx(BlockedGroupBanner, {}),
|
|
79
|
+
/* @__PURE__ */ jsx(ProviderIssueBanner, {}),
|
|
78
80
|
sessionUnreachable && !sessionBannerDismissed && /* @__PURE__ */ jsxs("div", {
|
|
79
81
|
className: "mx-4 mt-2 flex items-start gap-3 p-3 rounded-lg bg-amber-500/10 border border-amber-500/20",
|
|
80
82
|
children: [
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
-
import type { AttachmentMeta } from '../types.js';
|
|
2
|
+
import type { AttachmentMeta, MessagePaste } from '../types.js';
|
|
3
3
|
import { type SlashMenuConfig } from './SlashMenu.js';
|
|
4
|
+
import { type ComposerPastes } from './usePasteCards.js';
|
|
4
5
|
interface ChatInputProps {
|
|
5
|
-
onSend: (content: string, attachmentIds?: string[], attachmentMeta?: AttachmentMeta) => void;
|
|
6
|
+
onSend: (content: string, attachmentIds?: string[], attachmentMeta?: AttachmentMeta, pastes?: MessagePaste[]) => void;
|
|
6
7
|
disabled?: boolean;
|
|
7
8
|
placeholder?: string | undefined;
|
|
8
9
|
conversationId?: string;
|
|
@@ -33,6 +34,7 @@ interface ChatInputProps {
|
|
|
33
34
|
onStop: () => void;
|
|
34
35
|
stopping?: boolean;
|
|
35
36
|
};
|
|
37
|
+
pastes?: ComposerPastes | undefined;
|
|
36
38
|
}
|
|
37
39
|
export interface ChatInputHandle {
|
|
38
40
|
addFiles: (files: FileList | File[]) => void;
|
|
@@ -7,7 +7,9 @@ import AttachmentUpload from "./AttachmentUpload.js";
|
|
|
7
7
|
import useDragDrop from "../core/useDragDrop.js";
|
|
8
8
|
import { ComposerContext, ComposerSettings } from "./ComposerChrome.js";
|
|
9
9
|
import DragOverlay from "./DragOverlay.js";
|
|
10
|
+
import { PasteFileCard } from "./PastedText.js";
|
|
10
11
|
import { SlashMenu, useSlashMenu } from "./SlashMenu.js";
|
|
12
|
+
import { usePasteCards } from "./usePasteCards.js";
|
|
11
13
|
import cx from "classnames";
|
|
12
14
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
13
15
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -84,8 +86,12 @@ function streamControls(chat, turn) {
|
|
|
84
86
|
queuedCount: chat.queuedMessages.length
|
|
85
87
|
};
|
|
86
88
|
}
|
|
87
|
-
var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeholder, conversationId, onEditLastMessage, flush = false, allowEmpty = false, attachments: attachmentsWanted = true, requireModel = true, inputLabel, sendLabel, sendTestId, slash, context, settingsLeft, settingsRight, turn }, ref) {
|
|
89
|
+
var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeholder, conversationId, onEditLastMessage, flush = false, allowEmpty = false, attachments: attachmentsWanted = true, requireModel = true, inputLabel, sendLabel, sendTestId, slash, context, settingsLeft, settingsRight, turn, pastes }, ref) {
|
|
88
90
|
const [input, setInput] = useState(() => readDraft(conversationId));
|
|
91
|
+
const { cards, handleTextPaste, removeCard, takeCards, blocking: pastesBlocking } = usePasteCards(pastes, input, (value) => {
|
|
92
|
+
setInput(value);
|
|
93
|
+
storeDraft(conversationId, value);
|
|
94
|
+
});
|
|
89
95
|
const [attachments, setAttachments] = useState([]);
|
|
90
96
|
const [showAttachments, setShowAttachments] = useState(false);
|
|
91
97
|
const textareaRef = useRef(null);
|
|
@@ -118,8 +124,9 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
|
|
|
118
124
|
if (previousConversationRef.current !== conversationId) {
|
|
119
125
|
previousConversationRef.current = conversationId;
|
|
120
126
|
setInput(readDraft(conversationId));
|
|
127
|
+
takeCards();
|
|
121
128
|
}
|
|
122
|
-
}, [conversationId]);
|
|
129
|
+
}, [conversationId, takeCards]);
|
|
123
130
|
const setInputAndPersist = (value) => {
|
|
124
131
|
setInput(value);
|
|
125
132
|
storeDraft(conversationId, value);
|
|
@@ -128,16 +135,16 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
|
|
|
128
135
|
const pendingCount = attachments.length - uploadedCount;
|
|
129
136
|
const canFlushQueue = !isStreaming && queuedCount > 0 && !disabled;
|
|
130
137
|
const { canSend, emphasised } = sendability({
|
|
131
|
-
hasText: input.trim().length > 0,
|
|
138
|
+
hasText: input.trim().length > 0 || cards.length > 0,
|
|
132
139
|
canFlushQueue,
|
|
133
140
|
allowEmpty,
|
|
134
141
|
disabled,
|
|
135
142
|
modelReady: isSelectedModelAvailable || !requireModel,
|
|
136
|
-
pendingCount
|
|
143
|
+
pendingCount: pendingCount + (pastesBlocking ? 1 : 0)
|
|
137
144
|
});
|
|
138
145
|
const handleSubmit = () => {
|
|
139
146
|
if (!canSend) return;
|
|
140
|
-
if (!input.trim() && canFlushQueue) {
|
|
147
|
+
if (!input.trim() && cards.length === 0 && canFlushQueue) {
|
|
141
148
|
flushQueuedMessages();
|
|
142
149
|
return;
|
|
143
150
|
}
|
|
@@ -152,7 +159,12 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
|
|
|
152
159
|
size: a.size
|
|
153
160
|
};
|
|
154
161
|
}
|
|
155
|
-
|
|
162
|
+
const sentPastes = takeCards();
|
|
163
|
+
const content = [input.trim(), ...sentPastes.map((p) => p.placeholder)].filter(Boolean).join("\n\n");
|
|
164
|
+
const ids = attachmentIds.length > 0 ? attachmentIds : void 0;
|
|
165
|
+
const attachmentMeta = attachmentIds.length > 0 ? meta : void 0;
|
|
166
|
+
if (sentPastes.length > 0) onSend(content, ids, attachmentMeta, sentPastes);
|
|
167
|
+
else onSend(content, ids, attachmentMeta);
|
|
156
168
|
setInput("");
|
|
157
169
|
clearDraft(conversationId);
|
|
158
170
|
setAttachments([]);
|
|
@@ -177,11 +189,13 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
|
|
|
177
189
|
}, 0);
|
|
178
190
|
}, []);
|
|
179
191
|
const handlePaste = (e) => {
|
|
180
|
-
if (!attachmentsAvailable) return;
|
|
181
192
|
const files = Array.from(e.clipboardData?.files ?? []);
|
|
182
|
-
if (files.length
|
|
183
|
-
|
|
184
|
-
|
|
193
|
+
if (attachmentsAvailable && files.length > 0) {
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
handleFilesDropped(files);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (pastes && handleTextPaste(e.clipboardData.getData("text/plain"))) e.preventDefault();
|
|
185
199
|
};
|
|
186
200
|
const { isDragging, dragHandlers } = useDragDrop({ onFilesDropped: handleFilesDropped });
|
|
187
201
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -201,6 +215,18 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
|
|
|
201
215
|
})
|
|
202
216
|
}),
|
|
203
217
|
context && /* @__PURE__ */ jsx(ComposerContext, { children: context }),
|
|
218
|
+
cards.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
219
|
+
className: "mb-2 flex flex-wrap gap-2",
|
|
220
|
+
"data-testid": "composer-pastes",
|
|
221
|
+
children: cards.map((card) => /* @__PURE__ */ jsx(PasteFileCard, {
|
|
222
|
+
id: card.paste?.id,
|
|
223
|
+
lines: card.lines,
|
|
224
|
+
bytes: card.bytes,
|
|
225
|
+
saving: !card.paste && !card.error,
|
|
226
|
+
error: card.error,
|
|
227
|
+
onRemove: () => removeCard(card.key)
|
|
228
|
+
}, card.key))
|
|
229
|
+
}),
|
|
204
230
|
/* @__PURE__ */ jsxs("div", {
|
|
205
231
|
className: "relative",
|
|
206
232
|
children: [slash && slashMenu.open && /* @__PURE__ */ jsx(SlashMenu, {
|
|
@@ -15,6 +15,7 @@ import Markdown from "../ui/Markdown.js";
|
|
|
15
15
|
import { chatProseClasses } from "../ui/prose.js";
|
|
16
16
|
import AgentMessageParts from "./agent/AgentMessageParts.js";
|
|
17
17
|
import BranchNavigator from "./BranchNavigator.js";
|
|
18
|
+
import { UserMessageText } from "./PastedText.js";
|
|
18
19
|
import { Avatar } from "../ui/Avatar.js";
|
|
19
20
|
import cx from "classnames";
|
|
20
21
|
import { memo, useEffect, useRef, useState } from "react";
|
|
@@ -190,11 +191,17 @@ function ChatMessage({ message, allMessages, isStreaming = false, isEditing = fa
|
|
|
190
191
|
className: "flex-shrink-0 mb-0.5"
|
|
191
192
|
}), /* @__PURE__ */ jsx("div", {
|
|
192
193
|
className: "px-4 py-2.5 chat-ink rounded-2xl rounded-bl-md max-w-[70%] inline-block whitespace-pre-wrap text-base leading-6 bg-[color-mix(in_oklab,var(--theme-panel)_6%,transparent)]",
|
|
193
|
-
children:
|
|
194
|
+
children: /* @__PURE__ */ jsx(UserMessageText, {
|
|
195
|
+
content: message.content,
|
|
196
|
+
pastes: message.pastes
|
|
197
|
+
})
|
|
194
198
|
})]
|
|
195
199
|
}) : isUser ? /* @__PURE__ */ jsx("div", {
|
|
196
200
|
className: "px-4 py-2.5 chat-ink rounded-2xl rounded-br-md max-w-[70%] inline-block whitespace-pre-wrap text-base leading-6 bg-[color-mix(in_oklab,var(--theme-panel)_6%,transparent)]",
|
|
197
|
-
children:
|
|
201
|
+
children: /* @__PURE__ */ jsx(UserMessageText, {
|
|
202
|
+
content: message.content,
|
|
203
|
+
pastes: message.pastes
|
|
204
|
+
})
|
|
198
205
|
}) : /* @__PURE__ */ jsx("div", {
|
|
199
206
|
ref: contentRef,
|
|
200
207
|
className: cx(chatProseClasses, collapsed && "chat-message-clamp"),
|
|
@@ -6,6 +6,8 @@ interface ChatMessageListProps {
|
|
|
6
6
|
streamingMessage?: Message | null;
|
|
7
7
|
queuedMessages?: Message[];
|
|
8
8
|
onRemoveQueued?: (id: string) => void;
|
|
9
|
+
/** Which queued messages can still be taken back; all of them when unset. */
|
|
10
|
+
canRemoveQueued?: (id: string) => boolean;
|
|
9
11
|
editingMessageId?: string | null | undefined;
|
|
10
12
|
showAuthor?: boolean;
|
|
11
13
|
currentUsername?: string;
|
|
@@ -31,5 +33,5 @@ interface ChatMessageListProps {
|
|
|
31
33
|
* for the first frame is not announced twice. */
|
|
32
34
|
workingIndicator?: boolean;
|
|
33
35
|
}
|
|
34
|
-
export default function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessages, onRemoveQueued, editingMessageId, showAuthor, currentUsername, onNavigateBranch, onEditMessage, onSaveEdit, onCancelEdit, onRegenerateMessage, onRetryMessage, onOpenReasoning, onApprovalAnswer, inputElement, isStreaming: isStreamingProp, flush, label, footer, workingIndicator, }: ChatMessageListProps): import("react").JSX.Element;
|
|
36
|
+
export default function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessages, onRemoveQueued, canRemoveQueued, editingMessageId, showAuthor, currentUsername, onNavigateBranch, onEditMessage, onSaveEdit, onCancelEdit, onRegenerateMessage, onRetryMessage, onOpenReasoning, onApprovalAnswer, inputElement, isStreaming: isStreamingProp, flush, label, footer, workingIndicator, }: ChatMessageListProps): import("react").JSX.Element;
|
|
35
37
|
export {};
|
|
@@ -11,7 +11,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
|
|
|
11
11
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
12
|
import { Duration } from "luxon";
|
|
13
13
|
//#region src/ai/components/ChatMessageList.tsx
|
|
14
|
-
function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessages, onRemoveQueued, editingMessageId, showAuthor, currentUsername, onNavigateBranch, onEditMessage, onSaveEdit, onCancelEdit, onRegenerateMessage, onRetryMessage, onOpenReasoning, onApprovalAnswer, inputElement, isStreaming: isStreamingProp, flush, label, footer, workingIndicator = true }) {
|
|
14
|
+
function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessages, onRemoveQueued, canRemoveQueued, editingMessageId, showAuthor, currentUsername, onNavigateBranch, onEditMessage, onSaveEdit, onCancelEdit, onRegenerateMessage, onRetryMessage, onOpenReasoning, onApprovalAnswer, inputElement, isStreaming: isStreamingProp, flush, label, footer, workingIndicator = true }) {
|
|
15
15
|
const { adapter, isStreaming: providerStreaming, isThinking, streamingReasoning, streamingParts, thinkingStartTime } = useChat();
|
|
16
16
|
const isStreaming = isStreamingProp ?? providerStreaming;
|
|
17
17
|
const { strings } = useChatConfig();
|
|
@@ -266,7 +266,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
|
|
|
266
266
|
flush,
|
|
267
267
|
attachmentDownloadUrl,
|
|
268
268
|
onOpenAttachment
|
|
269
|
-
}), onRemoveQueued && /* @__PURE__ */ jsx("button", {
|
|
269
|
+
}), onRemoveQueued && (canRemoveQueued?.(msg.id) ?? true) && /* @__PURE__ */ jsx("button", {
|
|
270
270
|
type: "button",
|
|
271
271
|
"aria-label": strings.queue.remove,
|
|
272
272
|
title: strings.queue.remove,
|
|
@@ -8,6 +8,7 @@ import useDragDrop from "../core/useDragDrop.js";
|
|
|
8
8
|
import { ComposerControls, ConnectToolsLink } from "./ComposerChrome.js";
|
|
9
9
|
import DragOverlay from "./DragOverlay.js";
|
|
10
10
|
import ChatInput from "./ChatInput.js";
|
|
11
|
+
import ProviderIssueBanner from "./ProviderIssueBanner.js";
|
|
11
12
|
import { ShareButton } from "./ShareDialog.js";
|
|
12
13
|
import ChatMessageList from "./ChatMessageList.js";
|
|
13
14
|
import cx from "classnames";
|
|
@@ -118,6 +119,7 @@ function ChatThread({ conversationId }) {
|
|
|
118
119
|
children: [
|
|
119
120
|
isDragging && attachmentsAvailable && /* @__PURE__ */ jsx(DragOverlay, { className: "rounded-lg" }),
|
|
120
121
|
/* @__PURE__ */ jsx(BlockedGroupBanner, {}),
|
|
122
|
+
/* @__PURE__ */ jsx(ProviderIssueBanner, {}),
|
|
121
123
|
/* @__PURE__ */ jsx(ChatMessageList, {
|
|
122
124
|
messages,
|
|
123
125
|
allMessages: messages,
|
|
@@ -2,9 +2,11 @@ import { FaCheck } from "../../icons/fa/FaCheck.js";
|
|
|
2
2
|
import { FaChevronDown } from "../../icons/fa/FaChevronDown.js";
|
|
3
3
|
import { FaCloud } from "../../icons/fa/FaCloud.js";
|
|
4
4
|
import { FaCog } from "../../icons/fa/FaCog.js";
|
|
5
|
+
import { FaExclamationTriangle } from "../../icons/fa/FaExclamationTriangle.js";
|
|
5
6
|
import { FaRobot } from "../../icons/fa/FaRobot.js";
|
|
6
7
|
import { FaUndoAlt } from "../../icons/fa/FaUndoAlt.js";
|
|
7
8
|
import { useChatConfig } from "../core/config.js";
|
|
9
|
+
import { providerIssueFor } from "../core/providerIssues.js";
|
|
8
10
|
import { useChat } from "../core/ChatProvider.js";
|
|
9
11
|
import cx from "classnames";
|
|
10
12
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -46,8 +48,8 @@ function groupModelsByProvider(models) {
|
|
|
46
48
|
}
|
|
47
49
|
function ModelSelector({ targetSession, variant = "default" }) {
|
|
48
50
|
const bare = variant === "bare";
|
|
49
|
-
const { providers, models, unreachableSessions, selectedProvider, setSelectedProvider, isLoadingModels, hasLoadedModels, modelsError, refreshModels, currentUser } = useChat();
|
|
50
|
-
const { extraLinks, LinkComponent } = useChatConfig();
|
|
51
|
+
const { providers, models, unreachableSessions, providerIssues, selectedProvider, setSelectedProvider, isLoadingModels, hasLoadedModels, modelsError, refreshModels, currentUser } = useChat();
|
|
52
|
+
const { extraLinks, LinkComponent, strings } = useChatConfig();
|
|
51
53
|
const [isOpen, setIsOpen] = useState(false);
|
|
52
54
|
const [searchQuery, setSearchQuery] = useState("");
|
|
53
55
|
const [dropdownPosition, setDropdownPosition] = useState({
|
|
@@ -115,6 +117,10 @@ function ModelSelector({ targetSession, variant = "default" }) {
|
|
|
115
117
|
const selectedModelInfo = models.find((m) => m.id === selectedProvider);
|
|
116
118
|
const selectedModelName = selectedModelInfo ? getModelLabel(selectedModelInfo) : null;
|
|
117
119
|
const selectedProviderName = selectedModelInfo?.provider || null;
|
|
120
|
+
const issueLabel = (issue) => issue.status === "unauthorized" ? strings.providerIssue.keyRejected : strings.providerIssue.unreachable;
|
|
121
|
+
const issueDetail = (issue) => issue.message || (issue.status === "unauthorized" ? strings.providerIssue.keyRejectedHint : strings.providerIssue.unreachableHint);
|
|
122
|
+
const issueSummary = (issue) => `${issueLabel(issue)} \u2014 ${issueDetail(issue)}`;
|
|
123
|
+
const selectedModelIssue = providerIssueFor(providerIssues, selectedModelInfo);
|
|
118
124
|
const handleSelect = (modelId) => {
|
|
119
125
|
setSelectedProvider(modelId);
|
|
120
126
|
setIsOpen(false);
|
|
@@ -183,15 +189,22 @@ function ModelSelector({ targetSession, variant = "default" }) {
|
|
|
183
189
|
className: "flex-1 min-w-0 text-left",
|
|
184
190
|
children: selectedModelName ? /* @__PURE__ */ jsxs("div", {
|
|
185
191
|
className: "flex items-baseline gap-1.5 min-w-0",
|
|
186
|
-
children: [
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
children: [
|
|
193
|
+
/* @__PURE__ */ jsx("span", {
|
|
194
|
+
title: selectedModelName,
|
|
195
|
+
className: cx("truncate", bare ? "text-[11px] theme-muted-text" : "text-sm font-medium text-(--theme-panel)"),
|
|
196
|
+
children: selectedModelName
|
|
197
|
+
}),
|
|
198
|
+
selectedProviderName && /* @__PURE__ */ jsx("span", {
|
|
199
|
+
title: selectedProviderName,
|
|
200
|
+
className: cx("theme-muted-text truncate flex-shrink-0 max-w-[45%]", bare ? "text-[10px]" : "text-xs"),
|
|
201
|
+
children: selectedProviderName
|
|
202
|
+
}),
|
|
203
|
+
selectedModelIssue && /* @__PURE__ */ jsx(FaExclamationTriangle, {
|
|
204
|
+
title: issueSummary(selectedModelIssue),
|
|
205
|
+
className: "h-3.5 w-3.5 text-amber-500 flex-shrink-0 self-center"
|
|
206
|
+
})
|
|
207
|
+
]
|
|
195
208
|
}) : /* @__PURE__ */ jsx("span", {
|
|
196
209
|
className: cx(bare ? "text-[11px]" : "text-sm", targetSessionStatus === "connecting" ? "text-amber-600" : "theme-muted-text"),
|
|
197
210
|
children: triggerText
|
|
@@ -287,58 +300,77 @@ function ModelSelector({ targetSession, variant = "default" }) {
|
|
|
287
300
|
const platformSuffix = providerInfo && platform && platform.toLowerCase() !== providerName.toLowerCase() ? platform : null;
|
|
288
301
|
const isOwner = providerInfo?.user === currentUser.username;
|
|
289
302
|
const settingsHref = providerInfo && extraLinks.providerSettings ? extraLinks.providerSettings(providerInfo.user, providerInfo.name) : null;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
className: "
|
|
294
|
-
children:
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
"
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
303
|
+
const issue = providerIssueFor(providerIssues, providerModels[0]);
|
|
304
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
305
|
+
/* @__PURE__ */ jsx("div", {
|
|
306
|
+
className: "px-3 py-1.5 theme-muted-panel sticky top-0",
|
|
307
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
308
|
+
className: "flex items-center gap-2",
|
|
309
|
+
children: [
|
|
310
|
+
/* @__PURE__ */ jsx(CspIcon, {
|
|
311
|
+
cspKind: providerInfo?.cspKind ?? "other",
|
|
312
|
+
className: "h-3 w-3"
|
|
313
|
+
}),
|
|
314
|
+
/* @__PURE__ */ jsx("span", {
|
|
315
|
+
className: "text-xs font-semibold theme-muted-text uppercase tracking-wide",
|
|
316
|
+
children: providerName
|
|
317
|
+
}),
|
|
318
|
+
/* @__PURE__ */ jsx("span", {
|
|
319
|
+
className: "min-w-0 flex-1 truncate text-xs theme-muted-text",
|
|
320
|
+
children: platformSuffix ? `· ${platformSuffix}` : ""
|
|
321
|
+
}),
|
|
322
|
+
/* @__PURE__ */ jsxs("span", {
|
|
323
|
+
className: "text-xs theme-muted-text",
|
|
324
|
+
children: [
|
|
325
|
+
"(",
|
|
326
|
+
providerModels.length,
|
|
327
|
+
")"
|
|
328
|
+
]
|
|
329
|
+
}),
|
|
330
|
+
issue && /* @__PURE__ */ jsx("span", {
|
|
331
|
+
className: "text-[10px] text-amber-600 bg-amber-500/10 px-1.5 py-0.5 rounded-full flex-shrink-0",
|
|
332
|
+
children: issueLabel(issue)
|
|
333
|
+
}),
|
|
334
|
+
isOwner && settingsHref && /* @__PURE__ */ jsx(LinkComponent, {
|
|
335
|
+
target: {
|
|
336
|
+
kind: "external",
|
|
337
|
+
href: settingsHref
|
|
338
|
+
},
|
|
339
|
+
onClick: (e) => e.stopPropagation(),
|
|
340
|
+
className: "p-0.5 rounded hover:theme-hover theme-muted-text hover:text-blue-600 transition-colors",
|
|
341
|
+
title: "Manage provider",
|
|
342
|
+
children: /* @__PURE__ */ jsx(FaCog, { className: "h-3 w-3" })
|
|
343
|
+
})
|
|
344
|
+
]
|
|
345
|
+
})
|
|
346
|
+
}),
|
|
347
|
+
issue && /* @__PURE__ */ jsxs("div", {
|
|
348
|
+
className: "flex items-start gap-2 px-4 py-1.5 bg-amber-500/5",
|
|
349
|
+
children: [/* @__PURE__ */ jsx(FaExclamationTriangle, { className: "h-3 w-3 text-amber-500 flex-shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("span", {
|
|
350
|
+
className: "text-xs text-amber-600 line-clamp-2",
|
|
351
|
+
children: issueDetail(issue)
|
|
352
|
+
})]
|
|
353
|
+
}),
|
|
354
|
+
providerModels.map((model) => {
|
|
355
|
+
const modelName = getModelLabel(model);
|
|
356
|
+
const isSelected = model.id === selectedProvider;
|
|
357
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
358
|
+
type: "button",
|
|
359
|
+
"aria-disabled": !!issue,
|
|
360
|
+
title: issue ? issueSummary(issue) : void 0,
|
|
361
|
+
onClick: () => {
|
|
362
|
+
if (!issue) handleSelect(model.id);
|
|
363
|
+
},
|
|
364
|
+
className: cx("w-full flex items-center gap-3 px-4 py-2 text-left", issue ? "opacity-40 cursor-not-allowed" : "cursor-pointer hover:bg-blue-500/5 transition-colors", isSelected && "bg-blue-500/10"),
|
|
365
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
366
|
+
title: modelName,
|
|
367
|
+
className: cx("flex-1 min-w-0 text-sm truncate", isSelected ? "text-blue-600 font-medium" : ""),
|
|
368
|
+
style: !isSelected ? { color: "var(--theme-panel, #1f2937)" } : void 0,
|
|
369
|
+
children: modelName
|
|
370
|
+
}), isSelected && /* @__PURE__ */ jsx(FaCheck, { className: "h-3.5 w-3.5 text-blue-600 flex-shrink-0" })]
|
|
371
|
+
}, model.id);
|
|
326
372
|
})
|
|
327
|
-
}
|
|
328
|
-
const modelName = getModelLabel(model);
|
|
329
|
-
const isSelected = model.id === selectedProvider;
|
|
330
|
-
return /* @__PURE__ */ jsxs("button", {
|
|
331
|
-
type: "button",
|
|
332
|
-
onClick: () => handleSelect(model.id),
|
|
333
|
-
className: cx("w-full flex items-center gap-3 px-4 py-2 text-left cursor-pointer", "hover:bg-blue-500/5 transition-colors", isSelected && "bg-blue-500/10"),
|
|
334
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
335
|
-
title: modelName,
|
|
336
|
-
className: cx("flex-1 min-w-0 text-sm truncate", isSelected ? "text-blue-600 font-medium" : ""),
|
|
337
|
-
style: !isSelected ? { color: "var(--theme-panel, #1f2937)" } : void 0,
|
|
338
|
-
children: modelName
|
|
339
|
-
}), isSelected && /* @__PURE__ */ jsx(FaCheck, { className: "h-3.5 w-3.5 text-blue-600 flex-shrink-0" })]
|
|
340
|
-
}, model.id);
|
|
341
|
-
})] }, providerKey);
|
|
373
|
+
] }, providerKey);
|
|
342
374
|
})
|
|
343
375
|
]
|
|
344
376
|
})]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MessagePaste } from '../types.js';
|
|
2
|
+
/** A paste sent, or about to be sent, as its saved file. */
|
|
3
|
+
export declare function PasteFileCard({ id, lines, bytes, saving, error, onRemove, }: {
|
|
4
|
+
id?: number | undefined;
|
|
5
|
+
lines: number;
|
|
6
|
+
bytes: number;
|
|
7
|
+
saving?: boolean;
|
|
8
|
+
error?: string | undefined;
|
|
9
|
+
onRemove?: (() => void) | undefined;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
/** A user message as typed: an inline paste as its text, one sent as its file as a card. */
|
|
12
|
+
export declare function UserMessageText({ content, pastes, }: {
|
|
13
|
+
content: string;
|
|
14
|
+
pastes?: MessagePaste[] | null | undefined;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { FiFile } from "../../icons/fi/FiFile.js";
|
|
2
|
+
import { FiX } from "../../icons/fi/FiX.js";
|
|
3
|
+
import { ImSpinner5 } from "../../icons/im/ImSpinner5.js";
|
|
4
|
+
import { useChatConfig } from "../core/config.js";
|
|
5
|
+
import { formatFileSize } from "../utils.js";
|
|
6
|
+
import { splitPastes } from "../core/pastes.js";
|
|
7
|
+
import cx from "classnames";
|
|
8
|
+
import { Fragment } from "react";
|
|
9
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
//#region src/ai/components/PastedText.tsx
|
|
11
|
+
/** A paste sent, or about to be sent, as its saved file. */
|
|
12
|
+
function PasteFileCard({ id, lines, bytes, saving = false, error, onRemove }) {
|
|
13
|
+
const t = useChatConfig().strings.paste;
|
|
14
|
+
const detail = error ?? (saving ? t.saving : `${t.lines(lines)} · ${formatFileSize(bytes)}`);
|
|
15
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
16
|
+
"data-testid": "paste-file-card",
|
|
17
|
+
className: cx("inline-flex max-w-full items-center gap-2.5 rounded-xl border px-3 py-2 align-top whitespace-normal", "bg-(--theme-muted-panel-bg)", error ? "border-red-500" : "theme-border"),
|
|
18
|
+
children: [
|
|
19
|
+
/* @__PURE__ */ jsx(FiFile, { className: "h-5 w-5 flex-shrink-0 theme-muted-text" }),
|
|
20
|
+
/* @__PURE__ */ jsxs("div", {
|
|
21
|
+
className: "min-w-0 text-left leading-tight",
|
|
22
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
23
|
+
className: "truncate text-sm font-medium theme-text",
|
|
24
|
+
children: id ? `paste-${id}.txt` : t.title
|
|
25
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
26
|
+
className: cx("truncate text-xs", error ? "text-red-500" : "theme-muted-text"),
|
|
27
|
+
children: detail
|
|
28
|
+
})]
|
|
29
|
+
}),
|
|
30
|
+
saving && /* @__PURE__ */ jsx(ImSpinner5, { className: "h-3.5 w-3.5 flex-shrink-0 animate-spin theme-muted-text" }),
|
|
31
|
+
onRemove && /* @__PURE__ */ jsx("button", {
|
|
32
|
+
type: "button",
|
|
33
|
+
onClick: onRemove,
|
|
34
|
+
"aria-label": t.remove,
|
|
35
|
+
title: t.remove,
|
|
36
|
+
className: "flex-shrink-0 rounded p-0.5 theme-muted-text hover:theme-text",
|
|
37
|
+
children: /* @__PURE__ */ jsx(FiX, { className: "h-3.5 w-3.5" })
|
|
38
|
+
})
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** A user message as typed: an inline paste as its text, one sent as its file as a card. */
|
|
43
|
+
function UserMessageText({ content, pastes }) {
|
|
44
|
+
if (!pastes?.length) return /* @__PURE__ */ jsx(Fragment$1, { children: content });
|
|
45
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: splitPastes(content, pastes).map((segment, i) => {
|
|
46
|
+
if (typeof segment === "string") return /* @__PURE__ */ jsx(Fragment, { children: segment }, i);
|
|
47
|
+
if (segment.inline && segment.text !== void 0) return /* @__PURE__ */ jsx(Fragment, { children: segment.text }, i);
|
|
48
|
+
return /* @__PURE__ */ jsx("span", {
|
|
49
|
+
className: "my-1 block",
|
|
50
|
+
children: /* @__PURE__ */ jsx(PasteFileCard, {
|
|
51
|
+
id: segment.id,
|
|
52
|
+
lines: segment.lines,
|
|
53
|
+
bytes: segment.bytes
|
|
54
|
+
})
|
|
55
|
+
}, i);
|
|
56
|
+
}) });
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { PasteFileCard, UserMessageText };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function ProviderIssueBanner(): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { FaExclamationTriangle } from "../../icons/fa/FaExclamationTriangle.js";
|
|
2
|
+
import { useChatConfig } from "../core/config.js";
|
|
3
|
+
import { providerIssueFor } from "../core/providerIssues.js";
|
|
4
|
+
import { useChat } from "../core/ChatProvider.js";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/ai/components/ProviderIssueBanner.tsx
|
|
7
|
+
function ProviderIssueBanner() {
|
|
8
|
+
const { strings } = useChatConfig();
|
|
9
|
+
const { models, providerIssues, selectedProvider } = useChat();
|
|
10
|
+
const selectedModel = models.find((m) => m.id === selectedProvider);
|
|
11
|
+
const issue = providerIssueFor(providerIssues, selectedModel);
|
|
12
|
+
if (!issue) return null;
|
|
13
|
+
const title = issue.status === "unauthorized" ? strings.providerIssue.bannerKeyRejected(issue.provider) : strings.providerIssue.bannerUnreachable(issue.provider);
|
|
14
|
+
const detail = issue.message || (issue.status === "unauthorized" ? strings.providerIssue.keyRejectedHint : strings.providerIssue.unreachableHint);
|
|
15
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
16
|
+
className: "mx-4 mt-2 flex items-start gap-3 p-3 rounded-lg bg-amber-500/10 border border-amber-500/20",
|
|
17
|
+
children: [/* @__PURE__ */ jsx(FaExclamationTriangle, { className: "w-4 h-4 text-amber-500 shrink-0 mt-0.5" }), /* @__PURE__ */ jsxs("div", {
|
|
18
|
+
className: "flex-1 min-w-0",
|
|
19
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
20
|
+
className: "text-sm font-medium text-amber-700",
|
|
21
|
+
children: title
|
|
22
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
23
|
+
className: "text-xs text-amber-600/80 mt-0.5",
|
|
24
|
+
children: detail
|
|
25
|
+
})]
|
|
26
|
+
})]
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { ProviderIssueBanner as default };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MessagePaste } from '../types.js';
|
|
2
|
+
export type PasteUploadResult = {
|
|
3
|
+
paste: MessagePaste;
|
|
4
|
+
} | {
|
|
5
|
+
error: string;
|
|
6
|
+
} | {
|
|
7
|
+
unsupported: true;
|
|
8
|
+
};
|
|
9
|
+
/** Pastes over inlineMaxBytes are saved with the target and sent by placeholder; undefined keeps them text. */
|
|
10
|
+
export interface ComposerPastes {
|
|
11
|
+
inlineMaxBytes: number | undefined;
|
|
12
|
+
upload: (text: string) => Promise<PasteUploadResult>;
|
|
13
|
+
}
|
|
14
|
+
/** A paste waiting in the composer as a file card; paste is set once saved. */
|
|
15
|
+
export interface PasteCard {
|
|
16
|
+
key: string;
|
|
17
|
+
text: string;
|
|
18
|
+
lines: number;
|
|
19
|
+
bytes: number;
|
|
20
|
+
paste?: MessagePaste;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Keeps the composer's cards in step with the model, so text or card shows how each paste is sent. */
|
|
24
|
+
export declare function usePasteCards(pastes: ComposerPastes | undefined, input: string, setInput: (value: string) => void): {
|
|
25
|
+
cards: PasteCard[];
|
|
26
|
+
handleTextPaste: (raw: string) => boolean;
|
|
27
|
+
removeCard: (key: string) => void;
|
|
28
|
+
takeCards: () => MessagePaste[];
|
|
29
|
+
blocking: boolean;
|
|
30
|
+
};
|