@mariozechner/pi-web-ui 0.5.44

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.
Files changed (265) hide show
  1. package/README.md +252 -0
  2. package/dist/ChatPanel.d.ts +23 -0
  3. package/dist/ChatPanel.d.ts.map +1 -0
  4. package/dist/ChatPanel.js +224 -0
  5. package/dist/ChatPanel.js.map +1 -0
  6. package/dist/app.css +2 -0
  7. package/dist/components/AgentInterface.d.ts +35 -0
  8. package/dist/components/AgentInterface.d.ts.map +1 -0
  9. package/dist/components/AgentInterface.js +308 -0
  10. package/dist/components/AgentInterface.js.map +1 -0
  11. package/dist/components/AttachmentTile.d.ts +12 -0
  12. package/dist/components/AttachmentTile.d.ts.map +1 -0
  13. package/dist/components/AttachmentTile.js +114 -0
  14. package/dist/components/AttachmentTile.js.map +1 -0
  15. package/dist/components/ConsoleBlock.d.ts +11 -0
  16. package/dist/components/ConsoleBlock.d.ts.map +1 -0
  17. package/dist/components/ConsoleBlock.js +77 -0
  18. package/dist/components/ConsoleBlock.js.map +1 -0
  19. package/dist/components/Input.d.ts +26 -0
  20. package/dist/components/Input.d.ts.map +1 -0
  21. package/dist/components/Input.js +56 -0
  22. package/dist/components/Input.js.map +1 -0
  23. package/dist/components/MessageEditor.d.ts +38 -0
  24. package/dist/components/MessageEditor.d.ts.map +1 -0
  25. package/dist/components/MessageEditor.js +296 -0
  26. package/dist/components/MessageEditor.js.map +1 -0
  27. package/dist/components/MessageList.d.ts +13 -0
  28. package/dist/components/MessageList.d.ts.map +1 -0
  29. package/dist/components/MessageList.js +88 -0
  30. package/dist/components/MessageList.js.map +1 -0
  31. package/dist/components/Messages.d.ts +53 -0
  32. package/dist/components/Messages.d.ts.map +1 -0
  33. package/dist/components/Messages.js +323 -0
  34. package/dist/components/Messages.js.map +1 -0
  35. package/dist/components/ProviderKeyInput.d.ts +16 -0
  36. package/dist/components/ProviderKeyInput.d.ts.map +1 -0
  37. package/dist/components/ProviderKeyInput.js +183 -0
  38. package/dist/components/ProviderKeyInput.js.map +1 -0
  39. package/dist/components/SandboxedIframe.d.ts +63 -0
  40. package/dist/components/SandboxedIframe.d.ts.map +1 -0
  41. package/dist/components/SandboxedIframe.js +435 -0
  42. package/dist/components/SandboxedIframe.js.map +1 -0
  43. package/dist/components/StreamingMessageContainer.d.ts +17 -0
  44. package/dist/components/StreamingMessageContainer.d.ts.map +1 -0
  45. package/dist/components/StreamingMessageContainer.js +114 -0
  46. package/dist/components/StreamingMessageContainer.js.map +1 -0
  47. package/dist/dialogs/ApiKeyPromptDialog.d.ts +15 -0
  48. package/dist/dialogs/ApiKeyPromptDialog.d.ts.map +1 -0
  49. package/dist/dialogs/ApiKeyPromptDialog.js +80 -0
  50. package/dist/dialogs/ApiKeyPromptDialog.js.map +1 -0
  51. package/dist/dialogs/AttachmentOverlay.d.ts +32 -0
  52. package/dist/dialogs/AttachmentOverlay.d.ts.map +1 -0
  53. package/dist/dialogs/AttachmentOverlay.js +575 -0
  54. package/dist/dialogs/AttachmentOverlay.js.map +1 -0
  55. package/dist/dialogs/ModelSelector.d.ts +27 -0
  56. package/dist/dialogs/ModelSelector.d.ts.map +1 -0
  57. package/dist/dialogs/ModelSelector.js +334 -0
  58. package/dist/dialogs/ModelSelector.js.map +1 -0
  59. package/dist/dialogs/SettingsDialog.d.ts +31 -0
  60. package/dist/dialogs/SettingsDialog.d.ts.map +1 -0
  61. package/dist/dialogs/SettingsDialog.js +228 -0
  62. package/dist/dialogs/SettingsDialog.js.map +1 -0
  63. package/dist/index.d.ts +46 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +51 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/state/agent-session.d.ts +58 -0
  68. package/dist/state/agent-session.d.ts.map +1 -0
  69. package/dist/state/agent-session.js +252 -0
  70. package/dist/state/agent-session.js.map +1 -0
  71. package/dist/state/transports/AppTransport.d.ts +13 -0
  72. package/dist/state/transports/AppTransport.d.ts.map +1 -0
  73. package/dist/state/transports/AppTransport.js +316 -0
  74. package/dist/state/transports/AppTransport.js.map +1 -0
  75. package/dist/state/transports/ProviderTransport.d.ts +12 -0
  76. package/dist/state/transports/ProviderTransport.d.ts.map +1 -0
  77. package/dist/state/transports/ProviderTransport.js +44 -0
  78. package/dist/state/transports/ProviderTransport.js.map +1 -0
  79. package/dist/state/transports/index.d.ts +4 -0
  80. package/dist/state/transports/index.d.ts.map +1 -0
  81. package/dist/state/transports/index.js +4 -0
  82. package/dist/state/transports/index.js.map +1 -0
  83. package/dist/state/transports/proxy-types.d.ts +48 -0
  84. package/dist/state/transports/proxy-types.d.ts.map +1 -0
  85. package/dist/state/transports/proxy-types.js +2 -0
  86. package/dist/state/transports/proxy-types.js.map +1 -0
  87. package/dist/state/transports/types.d.ts +11 -0
  88. package/dist/state/transports/types.d.ts.map +1 -0
  89. package/dist/state/transports/types.js +2 -0
  90. package/dist/state/transports/types.js.map +1 -0
  91. package/dist/state/types.d.ts +15 -0
  92. package/dist/state/types.d.ts.map +1 -0
  93. package/dist/state/types.js +2 -0
  94. package/dist/state/types.js.map +1 -0
  95. package/dist/storage/app-storage.d.ts +26 -0
  96. package/dist/storage/app-storage.d.ts.map +1 -0
  97. package/dist/storage/app-storage.js +44 -0
  98. package/dist/storage/app-storage.js.map +1 -0
  99. package/dist/storage/backends/chrome-storage-backend.d.ts +18 -0
  100. package/dist/storage/backends/chrome-storage-backend.d.ts.map +1 -0
  101. package/dist/storage/backends/chrome-storage-backend.js +67 -0
  102. package/dist/storage/backends/chrome-storage-backend.js.map +1 -0
  103. package/dist/storage/backends/indexeddb-backend.d.ts +20 -0
  104. package/dist/storage/backends/indexeddb-backend.d.ts.map +1 -0
  105. package/dist/storage/backends/indexeddb-backend.js +89 -0
  106. package/dist/storage/backends/indexeddb-backend.js.map +1 -0
  107. package/dist/storage/backends/local-storage-backend.d.ts +18 -0
  108. package/dist/storage/backends/local-storage-backend.d.ts.map +1 -0
  109. package/dist/storage/backends/local-storage-backend.js +69 -0
  110. package/dist/storage/backends/local-storage-backend.js.map +1 -0
  111. package/dist/storage/repositories/provider-keys-repository.d.ts +34 -0
  112. package/dist/storage/repositories/provider-keys-repository.d.ts.map +1 -0
  113. package/dist/storage/repositories/provider-keys-repository.js +50 -0
  114. package/dist/storage/repositories/provider-keys-repository.js.map +1 -0
  115. package/dist/storage/repositories/settings-repository.d.ts +34 -0
  116. package/dist/storage/repositories/settings-repository.d.ts.map +1 -0
  117. package/dist/storage/repositories/settings-repository.js +46 -0
  118. package/dist/storage/repositories/settings-repository.js.map +1 -0
  119. package/dist/storage/types.d.ts +43 -0
  120. package/dist/storage/types.d.ts.map +1 -0
  121. package/dist/storage/types.js +2 -0
  122. package/dist/storage/types.js.map +1 -0
  123. package/dist/tools/artifacts/ArtifactElement.d.ts +10 -0
  124. package/dist/tools/artifacts/ArtifactElement.d.ts.map +1 -0
  125. package/dist/tools/artifacts/ArtifactElement.js +12 -0
  126. package/dist/tools/artifacts/ArtifactElement.js.map +1 -0
  127. package/dist/tools/artifacts/HtmlArtifact.d.ts +30 -0
  128. package/dist/tools/artifacts/HtmlArtifact.d.ts.map +1 -0
  129. package/dist/tools/artifacts/HtmlArtifact.js +217 -0
  130. package/dist/tools/artifacts/HtmlArtifact.js.map +1 -0
  131. package/dist/tools/artifacts/MarkdownArtifact.d.ts +20 -0
  132. package/dist/tools/artifacts/MarkdownArtifact.d.ts.map +1 -0
  133. package/dist/tools/artifacts/MarkdownArtifact.js +84 -0
  134. package/dist/tools/artifacts/MarkdownArtifact.js.map +1 -0
  135. package/dist/tools/artifacts/SvgArtifact.d.ts +19 -0
  136. package/dist/tools/artifacts/SvgArtifact.d.ts.map +1 -0
  137. package/dist/tools/artifacts/SvgArtifact.js +80 -0
  138. package/dist/tools/artifacts/SvgArtifact.js.map +1 -0
  139. package/dist/tools/artifacts/TextArtifact.d.ts +20 -0
  140. package/dist/tools/artifacts/TextArtifact.d.ts.map +1 -0
  141. package/dist/tools/artifacts/TextArtifact.js +147 -0
  142. package/dist/tools/artifacts/TextArtifact.js.map +1 -0
  143. package/dist/tools/artifacts/artifacts.d.ts +67 -0
  144. package/dist/tools/artifacts/artifacts.d.ts.map +1 -0
  145. package/dist/tools/artifacts/artifacts.js +836 -0
  146. package/dist/tools/artifacts/artifacts.js.map +1 -0
  147. package/dist/tools/artifacts/index.d.ts +7 -0
  148. package/dist/tools/artifacts/index.d.ts.map +1 -0
  149. package/dist/tools/artifacts/index.js +7 -0
  150. package/dist/tools/artifacts/index.js.map +1 -0
  151. package/dist/tools/index.d.ts +14 -0
  152. package/dist/tools/index.d.ts.map +1 -0
  153. package/dist/tools/index.js +29 -0
  154. package/dist/tools/index.js.map +1 -0
  155. package/dist/tools/javascript-repl.d.ts +43 -0
  156. package/dist/tools/javascript-repl.d.ts.map +1 -0
  157. package/dist/tools/javascript-repl.js +252 -0
  158. package/dist/tools/javascript-repl.js.map +1 -0
  159. package/dist/tools/renderer-registry.d.ts +11 -0
  160. package/dist/tools/renderer-registry.d.ts.map +1 -0
  161. package/dist/tools/renderer-registry.js +15 -0
  162. package/dist/tools/renderer-registry.js.map +1 -0
  163. package/dist/tools/renderers/BashRenderer.d.ts +12 -0
  164. package/dist/tools/renderers/BashRenderer.d.ts.map +1 -0
  165. package/dist/tools/renderers/BashRenderer.js +35 -0
  166. package/dist/tools/renderers/BashRenderer.js.map +1 -0
  167. package/dist/tools/renderers/CalculateRenderer.d.ts +12 -0
  168. package/dist/tools/renderers/CalculateRenderer.d.ts.map +1 -0
  169. package/dist/tools/renderers/CalculateRenderer.js +38 -0
  170. package/dist/tools/renderers/CalculateRenderer.js.map +1 -0
  171. package/dist/tools/renderers/DefaultRenderer.d.ts +8 -0
  172. package/dist/tools/renderers/DefaultRenderer.d.ts.map +1 -0
  173. package/dist/tools/renderers/DefaultRenderer.js +31 -0
  174. package/dist/tools/renderers/DefaultRenderer.js.map +1 -0
  175. package/dist/tools/renderers/GetCurrentTimeRenderer.d.ts +12 -0
  176. package/dist/tools/renderers/GetCurrentTimeRenderer.d.ts.map +1 -0
  177. package/dist/tools/renderers/GetCurrentTimeRenderer.js +30 -0
  178. package/dist/tools/renderers/GetCurrentTimeRenderer.js.map +1 -0
  179. package/dist/tools/types.d.ts +7 -0
  180. package/dist/tools/types.d.ts.map +1 -0
  181. package/dist/tools/types.js +2 -0
  182. package/dist/tools/types.js.map +1 -0
  183. package/dist/utils/attachment-utils.d.ts +19 -0
  184. package/dist/utils/attachment-utils.d.ts.map +1 -0
  185. package/dist/utils/attachment-utils.js +415 -0
  186. package/dist/utils/attachment-utils.js.map +1 -0
  187. package/dist/utils/auth-token.d.ts +3 -0
  188. package/dist/utils/auth-token.d.ts.map +1 -0
  189. package/dist/utils/auth-token.js +19 -0
  190. package/dist/utils/auth-token.js.map +1 -0
  191. package/dist/utils/format.d.ts +6 -0
  192. package/dist/utils/format.d.ts.map +1 -0
  193. package/dist/utils/format.js +47 -0
  194. package/dist/utils/format.js.map +1 -0
  195. package/dist/utils/i18n.d.ts +111 -0
  196. package/dist/utils/i18n.d.ts.map +1 -0
  197. package/dist/utils/i18n.js +224 -0
  198. package/dist/utils/i18n.js.map +1 -0
  199. package/dist/utils/test-sessions.d.ts +347 -0
  200. package/dist/utils/test-sessions.d.ts.map +1 -0
  201. package/dist/utils/test-sessions.js +2215 -0
  202. package/dist/utils/test-sessions.js.map +1 -0
  203. package/example/README.md +61 -0
  204. package/example/index.html +13 -0
  205. package/example/package-lock.json +1965 -0
  206. package/example/package.json +22 -0
  207. package/example/src/app.css +1 -0
  208. package/example/src/main.ts +57 -0
  209. package/example/src/test-sessions.ts +104 -0
  210. package/example/tsconfig.json +15 -0
  211. package/example/vite.config.ts +6 -0
  212. package/package.json +45 -0
  213. package/src/ChatPanel.ts +214 -0
  214. package/src/app.css +44 -0
  215. package/src/components/AgentInterface.ts +316 -0
  216. package/src/components/AttachmentTile.ts +112 -0
  217. package/src/components/ConsoleBlock.ts +67 -0
  218. package/src/components/Input.ts +112 -0
  219. package/src/components/MessageEditor.ts +272 -0
  220. package/src/components/MessageList.ts +82 -0
  221. package/src/components/Messages.ts +310 -0
  222. package/src/components/ProviderKeyInput.ts +170 -0
  223. package/src/components/SandboxedIframe.ts +525 -0
  224. package/src/components/StreamingMessageContainer.ts +101 -0
  225. package/src/dialogs/ApiKeyPromptDialog.ts +76 -0
  226. package/src/dialogs/AttachmentOverlay.ts +635 -0
  227. package/src/dialogs/ModelSelector.ts +324 -0
  228. package/src/dialogs/SettingsDialog.ts +223 -0
  229. package/src/index.ts +63 -0
  230. package/src/state/agent-session.ts +311 -0
  231. package/src/state/transports/AppTransport.ts +363 -0
  232. package/src/state/transports/ProviderTransport.ts +49 -0
  233. package/src/state/transports/index.ts +3 -0
  234. package/src/state/transports/proxy-types.ts +15 -0
  235. package/src/state/transports/types.ts +16 -0
  236. package/src/state/types.ts +11 -0
  237. package/src/storage/app-storage.ts +53 -0
  238. package/src/storage/backends/chrome-storage-backend.ts +82 -0
  239. package/src/storage/backends/indexeddb-backend.ts +107 -0
  240. package/src/storage/backends/local-storage-backend.ts +74 -0
  241. package/src/storage/repositories/provider-keys-repository.ts +55 -0
  242. package/src/storage/repositories/settings-repository.ts +51 -0
  243. package/src/storage/types.ts +48 -0
  244. package/src/tools/artifacts/ArtifactElement.ts +15 -0
  245. package/src/tools/artifacts/HtmlArtifact.ts +221 -0
  246. package/src/tools/artifacts/MarkdownArtifact.ts +81 -0
  247. package/src/tools/artifacts/SvgArtifact.ts +77 -0
  248. package/src/tools/artifacts/TextArtifact.ts +148 -0
  249. package/src/tools/artifacts/artifacts.ts +888 -0
  250. package/src/tools/artifacts/index.ts +6 -0
  251. package/src/tools/index.ts +35 -0
  252. package/src/tools/javascript-repl.ts +309 -0
  253. package/src/tools/renderer-registry.ts +18 -0
  254. package/src/tools/renderers/BashRenderer.ts +45 -0
  255. package/src/tools/renderers/CalculateRenderer.ts +49 -0
  256. package/src/tools/renderers/DefaultRenderer.ts +36 -0
  257. package/src/tools/renderers/GetCurrentTimeRenderer.ts +39 -0
  258. package/src/tools/types.ts +7 -0
  259. package/src/utils/attachment-utils.ts +472 -0
  260. package/src/utils/auth-token.ts +22 -0
  261. package/src/utils/format.ts +42 -0
  262. package/src/utils/i18n.ts +343 -0
  263. package/src/utils/test-sessions.ts +2247 -0
  264. package/tsconfig.build.json +20 -0
  265. package/tsconfig.json +7 -0
@@ -0,0 +1,324 @@
1
+ import { Badge, Button, DialogBase, DialogHeader, html, icon, type TemplateResult } from "@mariozechner/mini-lit";
2
+ import type { Model } from "@mariozechner/pi-ai";
3
+ import { MODELS } from "@mariozechner/pi-ai/dist/models.generated.js";
4
+ import type { PropertyValues } from "lit";
5
+ import { customElement, state } from "lit/decorators.js";
6
+ import { createRef, ref } from "lit/directives/ref.js";
7
+ import { Brain, Image as ImageIcon } from "lucide";
8
+ import { Ollama } from "ollama/dist/browser.mjs";
9
+ import { Input } from "../components/Input.js";
10
+ import { formatModelCost } from "../utils/format.js";
11
+ import { i18n } from "../utils/i18n.js";
12
+
13
+ @customElement("agent-model-selector")
14
+ export class ModelSelector extends DialogBase {
15
+ @state() currentModel: Model<any> | null = null;
16
+ @state() searchQuery = "";
17
+ @state() filterThinking = false;
18
+ @state() filterVision = false;
19
+ @state() ollamaModels: Model<any>[] = [];
20
+ @state() ollamaError: string | null = null;
21
+ @state() selectedIndex = 0;
22
+ @state() private navigationMode: "mouse" | "keyboard" = "mouse";
23
+
24
+ private onSelectCallback?: (model: Model<any>) => void;
25
+ private scrollContainerRef = createRef<HTMLDivElement>();
26
+ private searchInputRef = createRef<HTMLInputElement>();
27
+ private lastMousePosition = { x: 0, y: 0 };
28
+
29
+ protected override modalWidth = "min(400px, 90vw)";
30
+
31
+ static async open(currentModel: Model<any> | null, onSelect: (model: Model<any>) => void) {
32
+ const selector = new ModelSelector();
33
+ selector.currentModel = currentModel;
34
+ selector.onSelectCallback = onSelect;
35
+ selector.open();
36
+ selector.fetchOllamaModels();
37
+ }
38
+
39
+ override async firstUpdated(changedProperties: PropertyValues): Promise<void> {
40
+ super.firstUpdated(changedProperties);
41
+ // Wait for dialog to be fully rendered
42
+ await this.updateComplete;
43
+ // Focus the search input when dialog opens
44
+ this.searchInputRef.value?.focus();
45
+
46
+ // Track actual mouse movement
47
+ this.addEventListener("mousemove", (e: MouseEvent) => {
48
+ // Check if mouse actually moved
49
+ if (e.clientX !== this.lastMousePosition.x || e.clientY !== this.lastMousePosition.y) {
50
+ this.lastMousePosition = { x: e.clientX, y: e.clientY };
51
+ // Only switch to mouse mode on actual mouse movement
52
+ if (this.navigationMode === "keyboard") {
53
+ this.navigationMode = "mouse";
54
+ // Update selection to the item under the mouse
55
+ const target = e.target as HTMLElement;
56
+ const modelItem = target.closest("[data-model-item]");
57
+ if (modelItem) {
58
+ const allItems = this.scrollContainerRef.value?.querySelectorAll("[data-model-item]");
59
+ if (allItems) {
60
+ const index = Array.from(allItems).indexOf(modelItem);
61
+ if (index !== -1) {
62
+ this.selectedIndex = index;
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ });
69
+
70
+ // Add global keyboard handler for the dialog
71
+ this.addEventListener("keydown", (e: KeyboardEvent) => {
72
+ // Get filtered models to know the bounds
73
+ const filteredModels = this.getFilteredModels();
74
+
75
+ if (e.key === "ArrowDown") {
76
+ e.preventDefault();
77
+ this.navigationMode = "keyboard";
78
+ this.selectedIndex = Math.min(this.selectedIndex + 1, filteredModels.length - 1);
79
+ this.scrollToSelected();
80
+ } else if (e.key === "ArrowUp") {
81
+ e.preventDefault();
82
+ this.navigationMode = "keyboard";
83
+ this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
84
+ this.scrollToSelected();
85
+ } else if (e.key === "Enter") {
86
+ e.preventDefault();
87
+ if (filteredModels[this.selectedIndex]) {
88
+ this.handleSelect(filteredModels[this.selectedIndex].model);
89
+ }
90
+ }
91
+ });
92
+ }
93
+
94
+ private async fetchOllamaModels() {
95
+ try {
96
+ // Create Ollama client
97
+ const ollama = new Ollama({ host: "http://localhost:11434" });
98
+
99
+ // Get list of available models
100
+ const { models } = await ollama.list();
101
+
102
+ // Fetch details for each model and convert to Model format
103
+ const ollamaModelPromises: Promise<Model<any> | null>[] = models
104
+ .map(async (model) => {
105
+ try {
106
+ // Get model details
107
+ const details = await ollama.show({
108
+ model: model.name,
109
+ });
110
+
111
+ // Some Ollama servers don't report capabilities; don't filter on them
112
+
113
+ // Extract model info
114
+ const modelInfo: any = details.model_info || {};
115
+
116
+ // Get context window size - look for architecture-specific keys
117
+ const architecture = modelInfo["general.architecture"] || "";
118
+ const contextKey = `${architecture}.context_length`;
119
+ const contextWindow = parseInt(modelInfo[contextKey] || "8192", 10);
120
+ const maxTokens = 4096; // Default max output tokens
121
+
122
+ // Create Model object manually since ollama models aren't in MODELS constant
123
+ const ollamaModel: Model<any> = {
124
+ id: model.name,
125
+ name: model.name,
126
+ api: "openai-completions" as any,
127
+ provider: "ollama",
128
+ baseUrl: "http://localhost:11434/v1",
129
+ reasoning: false,
130
+ input: ["text"],
131
+ cost: {
132
+ input: 0,
133
+ output: 0,
134
+ cacheRead: 0,
135
+ cacheWrite: 0,
136
+ },
137
+ contextWindow: contextWindow,
138
+ maxTokens: maxTokens,
139
+ };
140
+
141
+ return ollamaModel;
142
+ } catch (err) {
143
+ console.error(`Failed to fetch details for model ${model.name}:`, err);
144
+ return null;
145
+ }
146
+ })
147
+ .filter((m) => m !== null);
148
+
149
+ const results = await Promise.all(ollamaModelPromises);
150
+ this.ollamaModels = results.filter((m): m is Model<any> => m !== null);
151
+ } catch (err) {
152
+ // Ollama not available or other error - silently ignore
153
+ console.debug("Ollama not available:", err);
154
+ this.ollamaError = err instanceof Error ? err.message : String(err);
155
+ }
156
+ }
157
+
158
+ private formatTokens(tokens: number): string {
159
+ if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(0)}M`;
160
+ if (tokens >= 1000) return `${(tokens / 1000).toFixed(0)}`;
161
+ return String(tokens);
162
+ }
163
+
164
+ private handleSelect(model: Model<any>) {
165
+ if (model) {
166
+ this.onSelectCallback?.(model);
167
+ this.close();
168
+ }
169
+ }
170
+
171
+ private getFilteredModels(): Array<{ provider: string; id: string; model: any }> {
172
+ // Collect all models from all providers
173
+ const allModels: Array<{ provider: string; id: string; model: any }> = [];
174
+ for (const [provider, providerData] of Object.entries(MODELS)) {
175
+ for (const [modelId, model] of Object.entries(providerData)) {
176
+ allModels.push({ provider, id: modelId, model });
177
+ }
178
+ }
179
+
180
+ // Add Ollama models
181
+ for (const ollamaModel of this.ollamaModels) {
182
+ allModels.push({
183
+ id: ollamaModel.id,
184
+ provider: "ollama",
185
+ model: ollamaModel,
186
+ });
187
+ }
188
+
189
+ // Filter models based on search and capability filters
190
+ let filteredModels = allModels;
191
+
192
+ // Apply search filter
193
+ if (this.searchQuery) {
194
+ filteredModels = filteredModels.filter(({ provider, id, model }) => {
195
+ const searchTokens = this.searchQuery.split(/\s+/).filter((t) => t);
196
+ const searchText = `${provider} ${id} ${model.name}`.toLowerCase();
197
+ return searchTokens.every((token) => searchText.includes(token));
198
+ });
199
+ }
200
+
201
+ // Apply capability filters
202
+ if (this.filterThinking) {
203
+ filteredModels = filteredModels.filter(({ model }) => model.reasoning);
204
+ }
205
+ if (this.filterVision) {
206
+ filteredModels = filteredModels.filter(({ model }) => model.input.includes("image"));
207
+ }
208
+
209
+ // Sort: current model first, then by provider
210
+ filteredModels.sort((a, b) => {
211
+ const aIsCurrent = this.currentModel?.id === a.model.id;
212
+ const bIsCurrent = this.currentModel?.id === b.model.id;
213
+ if (aIsCurrent && !bIsCurrent) return -1;
214
+ if (!aIsCurrent && bIsCurrent) return 1;
215
+ return a.provider.localeCompare(b.provider);
216
+ });
217
+
218
+ return filteredModels;
219
+ }
220
+
221
+ private scrollToSelected() {
222
+ requestAnimationFrame(() => {
223
+ const scrollContainer = this.scrollContainerRef.value;
224
+ const selectedElement = scrollContainer?.querySelectorAll("[data-model-item]")[
225
+ this.selectedIndex
226
+ ] as HTMLElement;
227
+ if (selectedElement) {
228
+ selectedElement.scrollIntoView({ block: "nearest", behavior: "smooth" });
229
+ }
230
+ });
231
+ }
232
+
233
+ protected override renderContent(): TemplateResult {
234
+ const filteredModels = this.getFilteredModels();
235
+
236
+ return html`
237
+ <!-- Header and Search -->
238
+ <div class="p-6 pb-4 flex flex-col gap-4 border-b border-border flex-shrink-0">
239
+ ${DialogHeader({ title: i18n("Select Model") })}
240
+ ${Input({
241
+ placeholder: i18n("Search models..."),
242
+ value: this.searchQuery,
243
+ inputRef: this.searchInputRef,
244
+ onInput: (e: Event) => {
245
+ this.searchQuery = (e.target as HTMLInputElement).value;
246
+ this.selectedIndex = 0;
247
+ // Reset scroll position when search changes
248
+ if (this.scrollContainerRef.value) {
249
+ this.scrollContainerRef.value.scrollTop = 0;
250
+ }
251
+ },
252
+ })}
253
+ <div class="flex gap-2">
254
+ ${Button({
255
+ variant: this.filterThinking ? "default" : "secondary",
256
+ size: "sm",
257
+ onClick: () => {
258
+ this.filterThinking = !this.filterThinking;
259
+ this.selectedIndex = 0;
260
+ if (this.scrollContainerRef.value) {
261
+ this.scrollContainerRef.value.scrollTop = 0;
262
+ }
263
+ },
264
+ className: "rounded-full",
265
+ children: html`<span class="inline-flex items-center gap-1">${icon(Brain, "sm")} ${i18n("Thinking")}</span>`,
266
+ })}
267
+ ${Button({
268
+ variant: this.filterVision ? "default" : "secondary",
269
+ size: "sm",
270
+ onClick: () => {
271
+ this.filterVision = !this.filterVision;
272
+ this.selectedIndex = 0;
273
+ if (this.scrollContainerRef.value) {
274
+ this.scrollContainerRef.value.scrollTop = 0;
275
+ }
276
+ },
277
+ className: "rounded-full",
278
+ children: html`<span class="inline-flex items-center gap-1">${icon(ImageIcon, "sm")} ${i18n("Vision")}</span>`,
279
+ })}
280
+ </div>
281
+ </div>
282
+
283
+ <!-- Scrollable model list -->
284
+ <div class="flex-1 overflow-y-auto" ${ref(this.scrollContainerRef)}>
285
+ ${filteredModels.map(({ provider, id, model }, index) => {
286
+ // Check if this is the current model by comparing IDs
287
+ const isCurrent = this.currentModel?.id === model.id;
288
+ const isSelected = index === this.selectedIndex;
289
+ return html`
290
+ <div
291
+ data-model-item
292
+ class="px-4 py-3 ${
293
+ this.navigationMode === "mouse" ? "hover:bg-muted" : ""
294
+ } cursor-pointer border-b border-border ${isSelected ? "bg-accent" : ""}"
295
+ @click=${() => this.handleSelect(model)}
296
+ @mouseenter=${() => {
297
+ // Only update selection in mouse mode
298
+ if (this.navigationMode === "mouse") {
299
+ this.selectedIndex = index;
300
+ }
301
+ }}
302
+ >
303
+ <div class="flex items-center justify-between gap-2 mb-1">
304
+ <div class="flex items-center gap-2 flex-1 min-w-0">
305
+ <span class="text-sm font-medium text-foreground truncate">${id}</span>
306
+ ${isCurrent ? html`<span class="text-green-500">✓</span>` : ""}
307
+ </div>
308
+ ${Badge(provider, "outline")}
309
+ </div>
310
+ <div class="flex items-center justify-between text-xs text-muted-foreground">
311
+ <div class="flex items-center gap-2">
312
+ <span class="${model.reasoning ? "" : "opacity-30"}">${icon(Brain, "sm")}</span>
313
+ <span class="${model.input.includes("image") ? "" : "opacity-30"}">${icon(ImageIcon, "sm")}</span>
314
+ <span>${this.formatTokens(model.contextWindow)}K/${this.formatTokens(model.maxTokens)}K</span>
315
+ </div>
316
+ <span>${formatModelCost(model.cost)}</span>
317
+ </div>
318
+ </div>
319
+ `;
320
+ })}
321
+ </div>
322
+ `;
323
+ }
324
+ }
@@ -0,0 +1,223 @@
1
+ import {
2
+ Dialog,
3
+ DialogContent,
4
+ DialogHeader,
5
+ html,
6
+ Input,
7
+ i18n,
8
+ Label,
9
+ Switch,
10
+ type TemplateResult,
11
+ } from "@mariozechner/mini-lit";
12
+ import { getProviders } from "@mariozechner/pi-ai";
13
+ import { LitElement } from "lit";
14
+ import { customElement, property, state } from "lit/decorators.js";
15
+ import "../components/ProviderKeyInput.js";
16
+ import { getAppStorage } from "../storage/app-storage.js";
17
+
18
+ // Base class for settings tabs
19
+ export abstract class SettingsTab extends LitElement {
20
+ abstract getTabName(): string;
21
+
22
+ protected createRenderRoot() {
23
+ return this;
24
+ }
25
+ }
26
+
27
+ // API Keys Tab
28
+ @customElement("api-keys-tab")
29
+ export class ApiKeysTab extends SettingsTab {
30
+ getTabName(): string {
31
+ return i18n("API Keys");
32
+ }
33
+
34
+ render(): TemplateResult {
35
+ const providers = getProviders();
36
+
37
+ return html`
38
+ <div class="flex flex-col gap-6">
39
+ <p class="text-sm text-muted-foreground">
40
+ ${i18n("Configure API keys for LLM providers. Keys are stored locally in your browser.")}
41
+ </p>
42
+ ${providers.map((provider) => html`<provider-key-input .provider=${provider}></provider-key-input>`)}
43
+ </div>
44
+ `;
45
+ }
46
+ }
47
+
48
+ // Proxy Tab
49
+ @customElement("proxy-tab")
50
+ export class ProxyTab extends SettingsTab {
51
+ @state() private proxyEnabled = false;
52
+ @state() private proxyUrl = "http://localhost:3001";
53
+
54
+ override async connectedCallback() {
55
+ super.connectedCallback();
56
+ // Load proxy settings when tab is connected
57
+ try {
58
+ const storage = getAppStorage();
59
+ const enabled = await storage.settings.get<boolean>("proxy.enabled");
60
+ const url = await storage.settings.get<string>("proxy.url");
61
+
62
+ if (enabled !== null) this.proxyEnabled = enabled;
63
+ if (url !== null) this.proxyUrl = url;
64
+ } catch (error) {
65
+ console.error("Failed to load proxy settings:", error);
66
+ }
67
+ }
68
+
69
+ private async saveProxySettings() {
70
+ try {
71
+ const storage = getAppStorage();
72
+ await storage.settings.set("proxy.enabled", this.proxyEnabled);
73
+ await storage.settings.set("proxy.url", this.proxyUrl);
74
+ } catch (error) {
75
+ console.error("Failed to save proxy settings:", error);
76
+ }
77
+ }
78
+
79
+ getTabName(): string {
80
+ return i18n("Proxy");
81
+ }
82
+
83
+ render(): TemplateResult {
84
+ return html`
85
+ <div class="flex flex-col gap-4">
86
+ <p class="text-sm text-muted-foreground">
87
+ ${i18n("The CORS proxy strips CORS headers from API responses, allowing browser-based apps to make direct calls to LLM providers without CORS restrictions. It forwards requests to providers while removing headers that would otherwise block cross-origin requests.")}
88
+ </p>
89
+
90
+ <div class="flex items-center justify-between">
91
+ <span class="text-sm font-medium text-foreground">${i18n("Use CORS Proxy")}</span>
92
+ ${Switch({
93
+ checked: this.proxyEnabled,
94
+ onChange: (checked: boolean) => {
95
+ this.proxyEnabled = checked;
96
+ this.saveProxySettings();
97
+ },
98
+ })}
99
+ </div>
100
+
101
+ <div class="space-y-2">
102
+ ${Label({ children: i18n("Proxy URL") })}
103
+ ${Input({
104
+ type: "text",
105
+ value: this.proxyUrl,
106
+ disabled: !this.proxyEnabled,
107
+ onInput: (e) => {
108
+ this.proxyUrl = (e.target as HTMLInputElement).value;
109
+ },
110
+ onChange: () => this.saveProxySettings(),
111
+ })}
112
+ </div>
113
+ </div>
114
+ `;
115
+ }
116
+ }
117
+
118
+ @customElement("settings-dialog")
119
+ export class SettingsDialog extends LitElement {
120
+ @property({ type: Array, attribute: false }) tabs: SettingsTab[] = [];
121
+ @state() private isOpen = false;
122
+ @state() private activeTabIndex = 0;
123
+
124
+ protected createRenderRoot() {
125
+ return this;
126
+ }
127
+
128
+ static async open(tabs: SettingsTab[]) {
129
+ const dialog = new SettingsDialog();
130
+ dialog.tabs = tabs;
131
+ dialog.isOpen = true;
132
+ document.body.appendChild(dialog);
133
+ }
134
+
135
+ private setActiveTab(index: number) {
136
+ this.activeTabIndex = index;
137
+ }
138
+
139
+ private renderSidebarItem(tab: SettingsTab, index: number): TemplateResult {
140
+ const isActive = this.activeTabIndex === index;
141
+ return html`
142
+ <button
143
+ class="w-full text-left px-4 py-3 rounded-md transition-colors ${
144
+ isActive
145
+ ? "bg-secondary text-foreground font-medium"
146
+ : "text-muted-foreground hover:bg-secondary/50 hover:text-foreground"
147
+ }"
148
+ @click=${() => this.setActiveTab(index)}
149
+ >
150
+ ${tab.getTabName()}
151
+ </button>
152
+ `;
153
+ }
154
+
155
+ private renderMobileTab(tab: SettingsTab, index: number): TemplateResult {
156
+ const isActive = this.activeTabIndex === index;
157
+ return html`
158
+ <button
159
+ class="px-3 py-2 text-sm font-medium transition-colors ${
160
+ isActive ? "border-b-2 border-primary text-foreground" : "text-muted-foreground hover:text-foreground"
161
+ }"
162
+ @click=${() => this.setActiveTab(index)}
163
+ >
164
+ ${tab.getTabName()}
165
+ </button>
166
+ `;
167
+ }
168
+
169
+ render() {
170
+ if (this.tabs.length === 0) {
171
+ return html``;
172
+ }
173
+
174
+ return Dialog({
175
+ isOpen: this.isOpen,
176
+ onClose: () => {
177
+ this.isOpen = false;
178
+ this.remove();
179
+ },
180
+ width: "min(1000px, 90vw)",
181
+ height: "min(800px, 90vh)",
182
+ children: html`
183
+ ${DialogContent({
184
+ className: "h-full p-6",
185
+ children: html`
186
+ <div class="flex flex-col h-full overflow-hidden">
187
+ <!-- Header -->
188
+ <div class="pb-4 flex-shrink-0">${DialogHeader({ title: i18n("Settings") })}</div>
189
+
190
+ <!-- Mobile Tabs -->
191
+ <div class="md:hidden flex flex-shrink-0 pb-4">
192
+ ${this.tabs.map((tab, index) => this.renderMobileTab(tab, index))}
193
+ </div>
194
+
195
+ <!-- Layout -->
196
+ <div class="flex flex-1 overflow-hidden">
197
+ <!-- Sidebar (desktop only) -->
198
+ <div class="hidden md:block w-64 flex-shrink-0 space-y-1">
199
+ ${this.tabs.map((tab, index) => this.renderSidebarItem(tab, index))}
200
+ </div>
201
+
202
+ <!-- Content -->
203
+ <div class="flex-1 overflow-y-auto md:pl-6">
204
+ ${this.tabs.map(
205
+ (tab, index) =>
206
+ html`<div style="display: ${this.activeTabIndex === index ? "block" : "none"}">${tab}</div>`,
207
+ )}
208
+ </div>
209
+ </div>
210
+
211
+ <!-- Footer -->
212
+ <div class="pt-4 flex-shrink-0">
213
+ <p class="text-xs text-muted-foreground text-center">
214
+ ${i18n("Settings are stored locally in your browser")}
215
+ </p>
216
+ </div>
217
+ </div>
218
+ `,
219
+ })}
220
+ `,
221
+ });
222
+ }
223
+ }
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ // Main chat interface
2
+ export { ChatPanel } from "./ChatPanel.js";
3
+
4
+ // Components
5
+ export { AgentInterface } from "./components/AgentInterface.js";
6
+ export { AttachmentTile } from "./components/AttachmentTile.js";
7
+ export { ConsoleBlock } from "./components/ConsoleBlock.js";
8
+ export { Input } from "./components/Input.js";
9
+ export { MessageEditor } from "./components/MessageEditor.js";
10
+ export { MessageList } from "./components/MessageList.js";
11
+ // Message components
12
+ export { AssistantMessage, ToolMessage, UserMessage } from "./components/Messages.js";
13
+ export {
14
+ type SandboxFile,
15
+ SandboxIframe,
16
+ type SandboxResult,
17
+ type SandboxUrlProvider,
18
+ } from "./components/SandboxedIframe.js";
19
+ export { StreamingMessageContainer } from "./components/StreamingMessageContainer.js";
20
+ export { ApiKeyPromptDialog } from "./dialogs/ApiKeyPromptDialog.js";
21
+ export { AttachmentOverlay } from "./dialogs/AttachmentOverlay.js";
22
+ // Dialogs
23
+ export { ModelSelector } from "./dialogs/ModelSelector.js";
24
+ export { ApiKeysTab, ProxyTab, SettingsDialog, SettingsTab } from "./dialogs/SettingsDialog.js";
25
+ export type { AgentSessionState, ThinkingLevel } from "./state/agent-session.js";
26
+ // State management
27
+ export { AgentSession } from "./state/agent-session.js";
28
+
29
+ // Transports
30
+ export { AppTransport } from "./state/transports/AppTransport.js";
31
+ export { ProviderTransport } from "./state/transports/ProviderTransport.js";
32
+ export type { ProxyAssistantMessageEvent } from "./state/transports/proxy-types.js";
33
+ export type { AgentRunConfig, AgentTransport } from "./state/transports/types.js";
34
+ // Storage
35
+ export { AppStorage, getAppStorage, initAppStorage, setAppStorage } from "./storage/app-storage.js";
36
+ export { ChromeStorageBackend } from "./storage/backends/chrome-storage-backend.js";
37
+ export { IndexedDBBackend } from "./storage/backends/indexeddb-backend.js";
38
+ export { LocalStorageBackend } from "./storage/backends/local-storage-backend.js";
39
+ export { ProviderKeysRepository } from "./storage/repositories/provider-keys-repository.js";
40
+ export { SettingsRepository } from "./storage/repositories/settings-repository.js";
41
+ export type { AppStorageConfig, StorageBackend } from "./storage/types.js";
42
+ // Artifacts
43
+ export { ArtifactElement } from "./tools/artifacts/ArtifactElement.js";
44
+ export { type Artifact, ArtifactsPanel, type ArtifactsParams } from "./tools/artifacts/artifacts.js";
45
+ export { HtmlArtifact } from "./tools/artifacts/HtmlArtifact.js";
46
+ export { MarkdownArtifact } from "./tools/artifacts/MarkdownArtifact.js";
47
+ export { SvgArtifact } from "./tools/artifacts/SvgArtifact.js";
48
+ export { TextArtifact } from "./tools/artifacts/TextArtifact.js";
49
+ // Tools
50
+ export { getToolRenderer, registerToolRenderer, renderToolParams, renderToolResult } from "./tools/index.js";
51
+ export { createJavaScriptReplTool, javascriptReplTool } from "./tools/javascript-repl.js";
52
+ export { BashRenderer } from "./tools/renderers/BashRenderer.js";
53
+ export { CalculateRenderer } from "./tools/renderers/CalculateRenderer.js";
54
+ // Tool renderers
55
+ export { DefaultRenderer } from "./tools/renderers/DefaultRenderer.js";
56
+ export { GetCurrentTimeRenderer } from "./tools/renderers/GetCurrentTimeRenderer.js";
57
+ export type { ToolRenderer } from "./tools/types.js";
58
+ export type { Attachment } from "./utils/attachment-utils.js";
59
+ // Utils
60
+ export { loadAttachment } from "./utils/attachment-utils.js";
61
+ export { clearAuthToken, getAuthToken } from "./utils/auth-token.js";
62
+ export { formatCost, formatModelCost, formatTokenCount, formatUsage } from "./utils/format.js";
63
+ export { i18n, setLanguage } from "./utils/i18n.js";