@oh-my-pi/pi-web-ui 1.337.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +609 -0
- package/example/README.md +61 -0
- package/example/index.html +13 -0
- package/example/package.json +24 -0
- package/example/src/app.css +1 -0
- package/example/src/custom-messages.ts +99 -0
- package/example/src/main.ts +420 -0
- package/example/tsconfig.json +23 -0
- package/example/vite.config.ts +6 -0
- package/package.json +57 -0
- package/scripts/count-prompt-tokens.ts +88 -0
- package/src/ChatPanel.ts +218 -0
- package/src/app.css +68 -0
- package/src/components/AgentInterface.ts +390 -0
- package/src/components/AttachmentTile.ts +107 -0
- package/src/components/ConsoleBlock.ts +74 -0
- package/src/components/CustomProviderCard.ts +96 -0
- package/src/components/ExpandableSection.ts +46 -0
- package/src/components/Input.ts +113 -0
- package/src/components/MessageEditor.ts +404 -0
- package/src/components/MessageList.ts +97 -0
- package/src/components/Messages.ts +384 -0
- package/src/components/ProviderKeyInput.ts +152 -0
- package/src/components/SandboxedIframe.ts +626 -0
- package/src/components/StreamingMessageContainer.ts +107 -0
- package/src/components/ThinkingBlock.ts +45 -0
- package/src/components/message-renderer-registry.ts +28 -0
- package/src/components/sandbox/ArtifactsRuntimeProvider.ts +219 -0
- package/src/components/sandbox/AttachmentsRuntimeProvider.ts +66 -0
- package/src/components/sandbox/ConsoleRuntimeProvider.ts +186 -0
- package/src/components/sandbox/FileDownloadRuntimeProvider.ts +110 -0
- package/src/components/sandbox/RuntimeMessageBridge.ts +82 -0
- package/src/components/sandbox/RuntimeMessageRouter.ts +216 -0
- package/src/components/sandbox/SandboxRuntimeProvider.ts +52 -0
- package/src/dialogs/ApiKeyPromptDialog.ts +75 -0
- package/src/dialogs/AttachmentOverlay.ts +640 -0
- package/src/dialogs/CustomProviderDialog.ts +274 -0
- package/src/dialogs/ModelSelector.ts +314 -0
- package/src/dialogs/PersistentStorageDialog.ts +146 -0
- package/src/dialogs/ProvidersModelsTab.ts +212 -0
- package/src/dialogs/SessionListDialog.ts +157 -0
- package/src/dialogs/SettingsDialog.ts +216 -0
- package/src/index.ts +115 -0
- package/src/prompts/prompts.ts +282 -0
- package/src/storage/app-storage.ts +60 -0
- package/src/storage/backends/indexeddb-storage-backend.ts +193 -0
- package/src/storage/store.ts +33 -0
- package/src/storage/stores/custom-providers-store.ts +62 -0
- package/src/storage/stores/provider-keys-store.ts +33 -0
- package/src/storage/stores/sessions-store.ts +136 -0
- package/src/storage/stores/settings-store.ts +34 -0
- package/src/storage/types.ts +206 -0
- package/src/tools/artifacts/ArtifactElement.ts +14 -0
- package/src/tools/artifacts/ArtifactPill.ts +26 -0
- package/src/tools/artifacts/Console.ts +102 -0
- package/src/tools/artifacts/DocxArtifact.ts +213 -0
- package/src/tools/artifacts/ExcelArtifact.ts +231 -0
- package/src/tools/artifacts/GenericArtifact.ts +118 -0
- package/src/tools/artifacts/HtmlArtifact.ts +203 -0
- package/src/tools/artifacts/ImageArtifact.ts +116 -0
- package/src/tools/artifacts/MarkdownArtifact.ts +83 -0
- package/src/tools/artifacts/PdfArtifact.ts +201 -0
- package/src/tools/artifacts/SvgArtifact.ts +82 -0
- package/src/tools/artifacts/TextArtifact.ts +148 -0
- package/src/tools/artifacts/artifacts-tool-renderer.ts +371 -0
- package/src/tools/artifacts/artifacts.ts +713 -0
- package/src/tools/artifacts/index.ts +7 -0
- package/src/tools/extract-document.ts +271 -0
- package/src/tools/index.ts +46 -0
- package/src/tools/javascript-repl.ts +316 -0
- package/src/tools/renderer-registry.ts +127 -0
- package/src/tools/renderers/BashRenderer.ts +52 -0
- package/src/tools/renderers/CalculateRenderer.ts +58 -0
- package/src/tools/renderers/DefaultRenderer.ts +95 -0
- package/src/tools/renderers/GetCurrentTimeRenderer.ts +92 -0
- package/src/tools/types.ts +15 -0
- package/src/utils/attachment-utils.ts +472 -0
- package/src/utils/auth-token.ts +22 -0
- package/src/utils/format.ts +42 -0
- package/src/utils/i18n.ts +653 -0
- package/src/utils/model-discovery.ts +277 -0
- package/src/utils/proxy-utils.ts +134 -0
- package/src/utils/test-sessions.ts +2357 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AssistantMessage as AssistantMessageType,
|
|
3
|
+
ImageContent,
|
|
4
|
+
TextContent,
|
|
5
|
+
ToolCall,
|
|
6
|
+
ToolResultMessage as ToolResultMessageType,
|
|
7
|
+
UserMessage as UserMessageType,
|
|
8
|
+
} from "@oh-my-pi/pi-ai";
|
|
9
|
+
import { html, LitElement, type TemplateResult } from "lit";
|
|
10
|
+
import { customElement, property } from "lit/decorators.js";
|
|
11
|
+
import { renderTool } from "../tools/index.js";
|
|
12
|
+
import type { Attachment } from "../utils/attachment-utils.js";
|
|
13
|
+
import { formatUsage } from "../utils/format.js";
|
|
14
|
+
import { i18n } from "../utils/i18n.js";
|
|
15
|
+
import "./ThinkingBlock.js";
|
|
16
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
17
|
+
|
|
18
|
+
export type UserMessageWithAttachments = {
|
|
19
|
+
role: "user-with-attachments";
|
|
20
|
+
content: string | (TextContent | ImageContent)[];
|
|
21
|
+
timestamp: number;
|
|
22
|
+
attachments?: Attachment[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Artifact message type for session persistence
|
|
26
|
+
export interface ArtifactMessage {
|
|
27
|
+
role: "artifact";
|
|
28
|
+
action: "create" | "update" | "delete";
|
|
29
|
+
filename: string;
|
|
30
|
+
content?: string;
|
|
31
|
+
title?: string;
|
|
32
|
+
timestamp: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
declare module "@oh-my-pi/pi-agent-core" {
|
|
36
|
+
interface CustomAgentMessages {
|
|
37
|
+
"user-with-attachments": UserMessageWithAttachments;
|
|
38
|
+
artifact: ArtifactMessage;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@customElement("user-message")
|
|
43
|
+
export class UserMessage extends LitElement {
|
|
44
|
+
@property({ type: Object }) message!: UserMessageWithAttachments | UserMessageType;
|
|
45
|
+
|
|
46
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override connectedCallback(): void {
|
|
51
|
+
super.connectedCallback();
|
|
52
|
+
this.style.display = "block";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override render() {
|
|
56
|
+
const content =
|
|
57
|
+
typeof this.message.content === "string"
|
|
58
|
+
? this.message.content
|
|
59
|
+
: this.message.content.find((c) => c.type === "text")?.text || "";
|
|
60
|
+
|
|
61
|
+
return html`
|
|
62
|
+
<div class="flex justify-start mx-4">
|
|
63
|
+
<div class="user-message-container py-2 px-4 rounded-xl">
|
|
64
|
+
<markdown-block .content=${content}></markdown-block>
|
|
65
|
+
${
|
|
66
|
+
this.message.role === "user-with-attachments" &&
|
|
67
|
+
this.message.attachments &&
|
|
68
|
+
this.message.attachments.length > 0
|
|
69
|
+
? html`
|
|
70
|
+
<div class="mt-3 flex flex-wrap gap-2">
|
|
71
|
+
${this.message.attachments.map(
|
|
72
|
+
(attachment) => html` <attachment-tile .attachment=${attachment}></attachment-tile> `,
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
`
|
|
76
|
+
: ""
|
|
77
|
+
}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@customElement("assistant-message")
|
|
85
|
+
export class AssistantMessage extends LitElement {
|
|
86
|
+
@property({ type: Object }) message!: AssistantMessageType;
|
|
87
|
+
@property({ type: Array }) tools?: AgentTool<any>[];
|
|
88
|
+
@property({ type: Object }) pendingToolCalls?: Set<string>;
|
|
89
|
+
@property({ type: Boolean }) hideToolCalls = false;
|
|
90
|
+
@property({ type: Object }) toolResultsById?: Map<string, ToolResultMessageType>;
|
|
91
|
+
@property({ type: Boolean }) isStreaming: boolean = false;
|
|
92
|
+
@property({ attribute: false }) onCostClick?: () => void;
|
|
93
|
+
|
|
94
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override connectedCallback(): void {
|
|
99
|
+
super.connectedCallback();
|
|
100
|
+
this.style.display = "block";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
override render() {
|
|
104
|
+
// Render content in the order it appears
|
|
105
|
+
const orderedParts: TemplateResult[] = [];
|
|
106
|
+
|
|
107
|
+
for (const chunk of this.message.content) {
|
|
108
|
+
if (chunk.type === "text" && chunk.text.trim() !== "") {
|
|
109
|
+
orderedParts.push(html`<markdown-block .content=${chunk.text}></markdown-block>`);
|
|
110
|
+
} else if (chunk.type === "thinking" && chunk.thinking.trim() !== "") {
|
|
111
|
+
orderedParts.push(
|
|
112
|
+
html`<thinking-block .content=${chunk.thinking} .isStreaming=${this.isStreaming}></thinking-block>`,
|
|
113
|
+
);
|
|
114
|
+
} else if (chunk.type === "toolCall") {
|
|
115
|
+
if (!this.hideToolCalls) {
|
|
116
|
+
const tool = this.tools?.find((t) => t.name === chunk.name);
|
|
117
|
+
const pending = this.pendingToolCalls?.has(chunk.id) ?? false;
|
|
118
|
+
const result = this.toolResultsById?.get(chunk.id);
|
|
119
|
+
// A tool call is aborted if the message was aborted and there's no result for this tool call
|
|
120
|
+
const aborted = this.message.stopReason === "aborted" && !result;
|
|
121
|
+
orderedParts.push(
|
|
122
|
+
html`<tool-message
|
|
123
|
+
.tool=${tool}
|
|
124
|
+
.toolCall=${chunk}
|
|
125
|
+
.result=${result}
|
|
126
|
+
.pending=${pending}
|
|
127
|
+
.aborted=${aborted}
|
|
128
|
+
.isStreaming=${this.isStreaming}
|
|
129
|
+
></tool-message>`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return html`
|
|
136
|
+
<div>
|
|
137
|
+
${orderedParts.length ? html` <div class="px-4 flex flex-col gap-3">${orderedParts}</div> ` : ""}
|
|
138
|
+
${
|
|
139
|
+
this.message.usage && !this.isStreaming
|
|
140
|
+
? this.onCostClick
|
|
141
|
+
? html`
|
|
142
|
+
<div
|
|
143
|
+
class="px-4 mt-2 text-xs text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
|
|
144
|
+
@click=${this.onCostClick}
|
|
145
|
+
>
|
|
146
|
+
${formatUsage(this.message.usage)}
|
|
147
|
+
</div>
|
|
148
|
+
`
|
|
149
|
+
: html` <div class="px-4 mt-2 text-xs text-muted-foreground">${formatUsage(this.message.usage)}</div> `
|
|
150
|
+
: ""
|
|
151
|
+
}
|
|
152
|
+
${
|
|
153
|
+
this.message.stopReason === "error" && this.message.errorMessage
|
|
154
|
+
? html`
|
|
155
|
+
<div class="mx-4 mt-3 p-3 bg-destructive/10 text-destructive rounded-lg text-sm overflow-hidden">
|
|
156
|
+
<strong>${i18n("Error:")}</strong> ${this.message.errorMessage}
|
|
157
|
+
</div>
|
|
158
|
+
`
|
|
159
|
+
: ""
|
|
160
|
+
}
|
|
161
|
+
${
|
|
162
|
+
this.message.stopReason === "aborted"
|
|
163
|
+
? html`<span class="text-sm text-destructive italic">${i18n("Request aborted")}</span>`
|
|
164
|
+
: ""
|
|
165
|
+
}
|
|
166
|
+
</div>
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@customElement("tool-message-debug")
|
|
172
|
+
export class ToolMessageDebugView extends LitElement {
|
|
173
|
+
@property({ type: Object }) callArgs: any;
|
|
174
|
+
@property({ type: Object }) result?: ToolResultMessageType;
|
|
175
|
+
@property({ type: Boolean }) hasResult: boolean = false;
|
|
176
|
+
|
|
177
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
178
|
+
return this; // light DOM for shared styles
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
override connectedCallback(): void {
|
|
182
|
+
super.connectedCallback();
|
|
183
|
+
this.style.display = "block";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private pretty(value: unknown): { content: string; isJson: boolean } {
|
|
187
|
+
try {
|
|
188
|
+
if (typeof value === "string") {
|
|
189
|
+
const maybeJson = JSON.parse(value);
|
|
190
|
+
return { content: JSON.stringify(maybeJson, null, 2), isJson: true };
|
|
191
|
+
}
|
|
192
|
+
return { content: JSON.stringify(value, null, 2), isJson: true };
|
|
193
|
+
} catch {
|
|
194
|
+
return { content: typeof value === "string" ? value : String(value), isJson: false };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
override render() {
|
|
199
|
+
const textOutput =
|
|
200
|
+
this.result?.content
|
|
201
|
+
?.filter((c) => c.type === "text")
|
|
202
|
+
.map((c: any) => c.text)
|
|
203
|
+
.join("\n") || "";
|
|
204
|
+
const output = this.pretty(textOutput);
|
|
205
|
+
const details = this.pretty(this.result?.details);
|
|
206
|
+
|
|
207
|
+
return html`
|
|
208
|
+
<div class="mt-3 flex flex-col gap-2">
|
|
209
|
+
<div>
|
|
210
|
+
<div class="text-xs font-medium mb-1 text-muted-foreground">${i18n("Call")}</div>
|
|
211
|
+
<code-block .code=${this.pretty(this.callArgs).content} language="json"></code-block>
|
|
212
|
+
</div>
|
|
213
|
+
<div>
|
|
214
|
+
<div class="text-xs font-medium mb-1 text-muted-foreground">${i18n("Result")}</div>
|
|
215
|
+
${
|
|
216
|
+
this.hasResult
|
|
217
|
+
? html`<code-block .code=${output.content} language="${output.isJson ? "json" : "text"}"></code-block>
|
|
218
|
+
<code-block .code=${details.content} language="${details.isJson ? "json" : "text"}"></code-block>`
|
|
219
|
+
: html`<div class="text-xs text-muted-foreground">${i18n("(no result)")}</div>`
|
|
220
|
+
}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
@customElement("tool-message")
|
|
228
|
+
export class ToolMessage extends LitElement {
|
|
229
|
+
@property({ type: Object }) toolCall!: ToolCall;
|
|
230
|
+
@property({ type: Object }) tool?: AgentTool<any>;
|
|
231
|
+
@property({ type: Object }) result?: ToolResultMessageType;
|
|
232
|
+
@property({ type: Boolean }) pending: boolean = false;
|
|
233
|
+
@property({ type: Boolean }) aborted: boolean = false;
|
|
234
|
+
@property({ type: Boolean }) isStreaming: boolean = false;
|
|
235
|
+
|
|
236
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
237
|
+
return this;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
override connectedCallback(): void {
|
|
241
|
+
super.connectedCallback();
|
|
242
|
+
this.style.display = "block";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
override render() {
|
|
246
|
+
const toolName = this.tool?.name || this.toolCall.name;
|
|
247
|
+
|
|
248
|
+
// Render tool content (renderer handles errors and styling)
|
|
249
|
+
const result: ToolResultMessageType<any> | undefined = this.aborted
|
|
250
|
+
? {
|
|
251
|
+
role: "toolResult",
|
|
252
|
+
isError: true,
|
|
253
|
+
content: [],
|
|
254
|
+
toolCallId: this.toolCall.id,
|
|
255
|
+
toolName: this.toolCall.name,
|
|
256
|
+
timestamp: Date.now(),
|
|
257
|
+
}
|
|
258
|
+
: this.result;
|
|
259
|
+
const renderResult = renderTool(
|
|
260
|
+
toolName,
|
|
261
|
+
this.toolCall.arguments,
|
|
262
|
+
result,
|
|
263
|
+
!this.aborted && (this.isStreaming || this.pending),
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// Handle custom rendering (no card wrapper)
|
|
267
|
+
if (renderResult.isCustom) {
|
|
268
|
+
return renderResult.content;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Default: wrap in card
|
|
272
|
+
return html`
|
|
273
|
+
<div class="p-2.5 border border-border rounded-md bg-card text-card-foreground shadow-xs">
|
|
274
|
+
${renderResult.content}
|
|
275
|
+
</div>
|
|
276
|
+
`;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@customElement("aborted-message")
|
|
281
|
+
export class AbortedMessage extends LitElement {
|
|
282
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
283
|
+
return this;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
override connectedCallback(): void {
|
|
287
|
+
super.connectedCallback();
|
|
288
|
+
this.style.display = "block";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
protected override render(): unknown {
|
|
292
|
+
return html`<span class="text-sm text-destructive italic">${i18n("Request aborted")}</span>`;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ============================================================================
|
|
297
|
+
// Default Message Transformer
|
|
298
|
+
// ============================================================================
|
|
299
|
+
|
|
300
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
301
|
+
import type { Message } from "@oh-my-pi/pi-ai";
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Convert attachments to content blocks for LLM.
|
|
305
|
+
* - Images become ImageContent blocks
|
|
306
|
+
* - Documents with extractedText become TextContent blocks with filename header
|
|
307
|
+
*/
|
|
308
|
+
export function convertAttachments(attachments: Attachment[]): (TextContent | ImageContent)[] {
|
|
309
|
+
const content: (TextContent | ImageContent)[] = [];
|
|
310
|
+
for (const attachment of attachments) {
|
|
311
|
+
if (attachment.type === "image") {
|
|
312
|
+
content.push({
|
|
313
|
+
type: "image",
|
|
314
|
+
data: attachment.content,
|
|
315
|
+
mimeType: attachment.mimeType,
|
|
316
|
+
} as ImageContent);
|
|
317
|
+
} else if (attachment.type === "document" && attachment.extractedText) {
|
|
318
|
+
content.push({
|
|
319
|
+
type: "text",
|
|
320
|
+
text: `\n\n[Document: ${attachment.fileName}]\n${attachment.extractedText}`,
|
|
321
|
+
} as TextContent);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return content;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Check if a message is a UserMessageWithAttachments.
|
|
329
|
+
*/
|
|
330
|
+
export function isUserMessageWithAttachments(msg: AgentMessage): msg is UserMessageWithAttachments {
|
|
331
|
+
return (msg as UserMessageWithAttachments).role === "user-with-attachments";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Check if a message is an ArtifactMessage.
|
|
336
|
+
*/
|
|
337
|
+
export function isArtifactMessage(msg: AgentMessage): msg is ArtifactMessage {
|
|
338
|
+
return (msg as ArtifactMessage).role === "artifact";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Default convertToLlm for web-ui apps.
|
|
343
|
+
*
|
|
344
|
+
* Handles:
|
|
345
|
+
* - UserMessageWithAttachments: converts to user message with content blocks
|
|
346
|
+
* - ArtifactMessage: filtered out (UI-only, for session reconstruction)
|
|
347
|
+
* - Standard LLM messages (user, assistant, toolResult): passed through
|
|
348
|
+
*/
|
|
349
|
+
export function defaultConvertToLlm(messages: AgentMessage[]): Message[] {
|
|
350
|
+
return messages
|
|
351
|
+
.filter((m) => {
|
|
352
|
+
// Filter out artifact messages - they're for session reconstruction only
|
|
353
|
+
if (isArtifactMessage(m)) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
return true;
|
|
357
|
+
})
|
|
358
|
+
.map((m): Message | null => {
|
|
359
|
+
// Convert user-with-attachments to user message with content blocks
|
|
360
|
+
if (isUserMessageWithAttachments(m)) {
|
|
361
|
+
const textContent: (TextContent | ImageContent)[] =
|
|
362
|
+
typeof m.content === "string" ? [{ type: "text", text: m.content }] : [...m.content];
|
|
363
|
+
|
|
364
|
+
if (m.attachments) {
|
|
365
|
+
textContent.push(...convertAttachments(m.attachments));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
role: "user",
|
|
370
|
+
content: textContent,
|
|
371
|
+
timestamp: m.timestamp,
|
|
372
|
+
} as Message;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Pass through standard LLM roles
|
|
376
|
+
if (m.role === "user" || m.role === "assistant" || m.role === "toolResult") {
|
|
377
|
+
return m as Message;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Filter out unknown message types
|
|
381
|
+
return null;
|
|
382
|
+
})
|
|
383
|
+
.filter((m): m is Message => m !== null);
|
|
384
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { i18n } from "@mariozechner/mini-lit";
|
|
2
|
+
import { Badge } from "@mariozechner/mini-lit/dist/Badge.js";
|
|
3
|
+
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
|
|
4
|
+
import { type Context, complete, getModel } from "@oh-my-pi/pi-ai";
|
|
5
|
+
import { html, LitElement } from "lit";
|
|
6
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
7
|
+
import { getAppStorage } from "../storage/app-storage.js";
|
|
8
|
+
import { applyProxyIfNeeded } from "../utils/proxy-utils.js";
|
|
9
|
+
import { Input } from "./Input.js";
|
|
10
|
+
|
|
11
|
+
// Test models for each provider
|
|
12
|
+
const TEST_MODELS: Record<string, string> = {
|
|
13
|
+
anthropic: "claude-3-5-haiku-20241022",
|
|
14
|
+
openai: "gpt-4o-mini",
|
|
15
|
+
google: "gemini-2.5-flash",
|
|
16
|
+
groq: "openai/gpt-oss-20b",
|
|
17
|
+
openrouter: "z-ai/glm-4.6",
|
|
18
|
+
cerebras: "gpt-oss-120b",
|
|
19
|
+
xai: "grok-4-fast-non-reasoning",
|
|
20
|
+
zai: "glm-4.5-air",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
@customElement("provider-key-input")
|
|
24
|
+
export class ProviderKeyInput extends LitElement {
|
|
25
|
+
@property() provider = "";
|
|
26
|
+
@state() private keyInput = "";
|
|
27
|
+
@state() private testing = false;
|
|
28
|
+
@state() private failed = false;
|
|
29
|
+
@state() private hasKey = false;
|
|
30
|
+
@state() private inputChanged = false;
|
|
31
|
+
|
|
32
|
+
protected createRenderRoot() {
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override async connectedCallback() {
|
|
37
|
+
super.connectedCallback();
|
|
38
|
+
await this.checkKeyStatus();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private async checkKeyStatus() {
|
|
42
|
+
try {
|
|
43
|
+
const key = await getAppStorage().providerKeys.get(this.provider);
|
|
44
|
+
this.hasKey = !!key;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error("Failed to check key status:", error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async testApiKey(provider: string, apiKey: string): Promise<boolean> {
|
|
51
|
+
try {
|
|
52
|
+
const modelId = TEST_MODELS[provider];
|
|
53
|
+
// Returning true here for Ollama and friends. Can' know which model to use for testing
|
|
54
|
+
if (!modelId) return true;
|
|
55
|
+
|
|
56
|
+
let model = getModel(provider as any, modelId);
|
|
57
|
+
if (!model) return false;
|
|
58
|
+
|
|
59
|
+
// Get proxy URL from settings (if available)
|
|
60
|
+
const proxyEnabled = await getAppStorage().settings.get<boolean>("proxy.enabled");
|
|
61
|
+
const proxyUrl = await getAppStorage().settings.get<string>("proxy.url");
|
|
62
|
+
|
|
63
|
+
// Apply proxy only if this provider/key combination requires it
|
|
64
|
+
model = applyProxyIfNeeded(model, apiKey, proxyEnabled ? proxyUrl || undefined : undefined);
|
|
65
|
+
|
|
66
|
+
const context: Context = {
|
|
67
|
+
messages: [{ role: "user", content: "Reply with: ok", timestamp: Date.now() }],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const result = await complete(model, context, {
|
|
71
|
+
apiKey,
|
|
72
|
+
maxTokens: 200,
|
|
73
|
+
} as any);
|
|
74
|
+
|
|
75
|
+
return result.stopReason === "stop";
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(`API key test failed for ${provider}:`, error);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async saveKey() {
|
|
83
|
+
if (!this.keyInput) return;
|
|
84
|
+
|
|
85
|
+
this.testing = true;
|
|
86
|
+
this.failed = false;
|
|
87
|
+
|
|
88
|
+
const success = await this.testApiKey(this.provider, this.keyInput);
|
|
89
|
+
|
|
90
|
+
this.testing = false;
|
|
91
|
+
|
|
92
|
+
if (success) {
|
|
93
|
+
try {
|
|
94
|
+
await getAppStorage().providerKeys.set(this.provider, this.keyInput);
|
|
95
|
+
this.hasKey = true;
|
|
96
|
+
this.inputChanged = false;
|
|
97
|
+
this.requestUpdate();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error("Failed to save API key:", error);
|
|
100
|
+
this.failed = true;
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
this.failed = false;
|
|
103
|
+
this.requestUpdate();
|
|
104
|
+
}, 5000);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
this.failed = true;
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
this.failed = false;
|
|
110
|
+
this.requestUpdate();
|
|
111
|
+
}, 5000);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
render() {
|
|
116
|
+
return html`
|
|
117
|
+
<div class="space-y-3">
|
|
118
|
+
<div class="flex items-center gap-2">
|
|
119
|
+
<span class="text-sm font-medium capitalize text-foreground">${this.provider}</span>
|
|
120
|
+
${
|
|
121
|
+
this.testing
|
|
122
|
+
? Badge({ children: i18n("Testing..."), variant: "secondary" })
|
|
123
|
+
: this.hasKey
|
|
124
|
+
? html`<span class="text-green-600 dark:text-green-400">✓</span>`
|
|
125
|
+
: ""
|
|
126
|
+
}
|
|
127
|
+
${this.failed ? Badge({ children: i18n("✗ Invalid"), variant: "destructive" }) : ""}
|
|
128
|
+
</div>
|
|
129
|
+
<div class="flex items-center gap-2">
|
|
130
|
+
${Input({
|
|
131
|
+
type: "password",
|
|
132
|
+
placeholder: this.hasKey ? "••••••••••••" : i18n("Enter API key"),
|
|
133
|
+
value: this.keyInput,
|
|
134
|
+
onInput: (e: Event) => {
|
|
135
|
+
this.keyInput = (e.target as HTMLInputElement).value;
|
|
136
|
+
this.inputChanged = true;
|
|
137
|
+
this.requestUpdate();
|
|
138
|
+
},
|
|
139
|
+
className: "flex-1",
|
|
140
|
+
})}
|
|
141
|
+
${Button({
|
|
142
|
+
onClick: () => this.saveKey(),
|
|
143
|
+
variant: "default",
|
|
144
|
+
size: "sm",
|
|
145
|
+
disabled: !this.keyInput || this.testing || (this.hasKey && !this.inputChanged),
|
|
146
|
+
children: i18n("Save"),
|
|
147
|
+
})}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
}
|