main
js 233 lines 7.5 KB
Raw
1 import { createStore } from "/js/AlpineStore.js";
2 import { callJsonApi } from "/js/api.js";
3 import { store as modelConfigStore } from "/plugins/_model_config/webui/model-config-store.js";
4 import { store as onboardingStore } from "/plugins/_onboarding/webui/onboarding-store.js";
5 import { toastFrontendError } from "/components/notifications/notification-store.js";
6
7 const ONBOARDING_MODAL_PATH = "/plugins/_onboarding/webui/onboarding.html";
8 const STORAGE_KEY = "a0:model-gate-pending:v1";
9 const SYNTHETIC_MESSAGE_NO_BASE = Number.MAX_SAFE_INTEGER - 2;
10
11 export const store = createStore("modelGate", {
12 active: false,
13 connected: false,
14 connectedLabel: "",
15 accountConnected: false,
16 accountLabel: "",
17 pending: null,
18 gateMessageId: "",
19 choice: "",
20 dispatching: false,
21 _initialized: false,
22
23 get introText() {
24 if (this.connected) {
25 return `Model connected: ${this.connectedLabel || "ready"}`;
26 }
27 if (this.accountConnected) {
28 return `${this.accountLabel || "Your AI account"} is connected. Choose Main and Utility models and I'll answer right away.`;
29 }
30 return "I'm ready to work on this — I just need a model to think with. Pick one and I'll answer right away.";
31 },
32
33 init() {
34 if (this._initialized) return;
35 this._initialized = true;
36 this.restorePending();
37 document.addEventListener("onboarding-configured", () => {
38 void this.dispatchPendingIfConfigured();
39 });
40 document.addEventListener("model-configured", () => {
41 void this.dispatchPendingIfConfigured();
42 });
43 document.addEventListener("model-setup-changed", () => {
44 void this.dispatchPendingIfConfigured();
45 });
46 document.addEventListener("modal-closed", () => {
47 if (this.active && !this.connected) {
48 this.choice = "";
49 this.savePending();
50 void this.dispatchPendingIfConfigured();
51 }
52 });
53 },
54
55 async canSendToModel() {
56 try {
57 const data = await callJsonApi("/plugins/_model_config/model_config_get", {});
58 this.applyModelStatus(data);
59 if (!data?.model_configured) await this.refreshConnectedAccountState();
60 return !!data?.model_configured;
61 } catch (error) {
62 console.error("Could not check model configuration:", error);
63 return true;
64 }
65 },
66
67 async refreshConnectedAccountState() {
68 try {
69 const { store: oauthConfigStore } = await import("/plugins/_oauth/webui/oauth-config-store.js");
70 await oauthConfigStore.loadStatus();
71 const account = oauthConfigStore.connectedProviderCards()[0] || null;
72 this.accountConnected = Boolean(account);
73 this.accountLabel = account?.short_name || account?.display_name || account?.provider_id || "";
74 return this.accountConnected;
75 } catch (error) {
76 console.error("Could not check connected accounts:", error);
77 this.accountConnected = false;
78 this.accountLabel = "";
79 return false;
80 }
81 },
82
83 applyModelStatus(data = {}) {
84 modelConfigStore.modelConfigured = !!data.model_configured;
85 modelConfigStore.modelConfiguredLabel = data.model_configured_label || "";
86 this.connectedLabel = data.model_configured_label || this.connectedLabel || "";
87 },
88
89 start({ message, attachments, messageId, context }) {
90 this.init();
91 this.active = true;
92 this.connected = false;
93 this.pending = { message, attachments, messageId, context };
94 this.gateMessageId = this.gateMessageId || `model-gate-${messageId}`;
95 this.savePending();
96 },
97
98 syntheticMessages(currentContext) {
99 this.init();
100 if (!this.active || !this.pending || this.pending.context !== currentContext) return [];
101 return [
102 {
103 no: SYNTHETIC_MESSAGE_NO_BASE,
104 id: this.pending.messageId,
105 type: "user",
106 content: this.pending.message,
107 kvps: { attachments: this.pending.attachments },
108 },
109 {
110 no: SYNTHETIC_MESSAGE_NO_BASE + 1,
111 id: this.gateMessageId,
112 type: "model_setup_gate",
113 },
114 ];
115 },
116
117 mergeSyntheticMessages(logs, currentContext) {
118 this.init();
119 const synthetic = this.syntheticMessages(currentContext);
120 return synthetic.length ? [...(logs || []), ...synthetic] : logs;
121 },
122
123 onCardCreate() {
124 this.init();
125 void this.dispatchPendingIfConfigured();
126 },
127
128 openOnboarding(choice) {
129 this.choice = ["local", "account"].includes(choice) ? choice : "cloud";
130 this.savePending();
131 onboardingStore.presetMode = this.choice;
132 const modalPromise = window.openModal?.(ONBOARDING_MODAL_PATH);
133 void Promise.resolve(modalPromise).then(() => this.dispatchPendingIfConfigured());
134 },
135
136 async openAdvancedModelConfiguration() {
137 try {
138 const data = await callJsonApi("/plugins/_model_config/model_config_get", {});
139 await modelConfigStore.openPresetEditor(
140 data?.selected_preset || data?.configured_preset || "Default"
141 );
142 await this.dispatchPendingIfConfigured();
143 } catch (error) {
144 console.error("Could not open model preset editor:", error);
145 void toastFrontendError(
146 error?.message || "Could not open model presets.",
147 "Advanced model configuration"
148 );
149 }
150 },
151
152 async dispatchPendingIfConfigured() {
153 this.init();
154 if (this.dispatching || !this.pending) return;
155 const configured = await this.canSendToModel();
156 if (!configured) return;
157
158 const pending = this.pending;
159 this.pending = null;
160 this.connected = true;
161 this.dispatching = true;
162 this.clearSavedPending();
163 try {
164 await globalThis.sendMessage?.({
165 bypassModelGate: true,
166 skipExtensions: true,
167 preserveInput: true,
168 message: pending.message,
169 attachments: pending.attachments,
170 messageId: pending.messageId,
171 context: pending.context,
172 });
173 } finally {
174 this.dispatching = false;
175 }
176 },
177
178 savePending() {
179 const message = String(this.pending?.message || "").trim();
180 const context = String(this.pending?.context || "");
181 const messageId = String(this.pending?.messageId || "");
182 const attachments = Array.isArray(this.pending?.attachments) ? this.pending.attachments : [];
183 if (!message || !context || !messageId || attachments.length) {
184 this.clearSavedPending();
185 return;
186 }
187
188 try {
189 sessionStorage.setItem(STORAGE_KEY, JSON.stringify({
190 pending: { message, attachments: [], messageId, context },
191 gateMessageId: this.gateMessageId || `model-gate-${messageId}`,
192 choice: this.choice,
193 }));
194 } catch (_error) {
195 // Session storage can be unavailable in private or locked-down browser modes.
196 }
197 },
198
199 restorePending() {
200 if (this.pending) return;
201
202 let saved = null;
203 try {
204 saved = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "null");
205 } catch (_error) {
206 this.clearSavedPending();
207 return;
208 }
209
210 const message = String(saved?.pending?.message || "").trim();
211 const context = String(saved?.pending?.context || "");
212 const messageId = String(saved?.pending?.messageId || "");
213 if (!message || !context || !messageId) {
214 this.clearSavedPending();
215 return;
216 }
217
218 this.active = true;
219 this.connected = false;
220 this.pending = { message, attachments: [], messageId, context };
221 this.gateMessageId = String(saved?.gateMessageId || `model-gate-${messageId}`);
222 this.choice = ["local", "cloud", "account"].includes(saved?.choice) ? saved.choice : "";
223 },
224
225 clearSavedPending() {
226 try {
227 sessionStorage.removeItem(STORAGE_KEY);
228 } catch (_error) {
229 // Ignore unavailable storage; the in-memory gate state is still authoritative.
230 }
231 },
232
233 });