| 1 | import { createStore } from "/js/AlpineStore.js"; |
| 2 | import * as api from "/js/api.js"; |
| 3 | import * as modals from "/js/modals.js"; |
| 4 | import * as notifications from "/components/notifications/notification-store.js"; |
| 5 | import { store as chatsStore } from "/components/sidebar/chats/chats-store.js"; |
| 6 | import { store as browserStore } from "/components/modals/file-browser/file-browser-store.js"; |
| 7 | import { store as skillsImportStore } from "/components/settings/skills/skills-import-store.js"; |
| 8 | import { store as modelConfigStore } from "/plugins/_model_config/webui/model-config-store.js"; |
| 9 | import * as shortcuts from "/js/shortcuts.js"; |
| 10 | import { showConfirmDialog } from "/js/confirmDialog.js"; |
| 11 | |
| 12 | const listModal = "projects/project-list.html"; |
| 13 | const createModal = "projects/project-create.html"; |
| 14 | const editModal = "projects/project-edit.html"; |
| 15 | |
| 16 | // define the model object holding data and functions |
| 17 | const model = { |
| 18 | projectList: [], |
| 19 | selectedProject: null, |
| 20 | editData: null, |
| 21 | colors: [ |
| 22 | "#7b2cbf", // Deep Purple |
| 23 | "#8338ec", // Blue Violet |
| 24 | "#9b5de5", // Amethyst |
| 25 | "#d0bfff", // Lavender |
| 26 | "#002975ff", // Prussian Blue |
| 27 | "#3a86ff", // Azure |
| 28 | "#0077b6", // Star Command Blue |
| 29 | "#4cc9f0", // Bright Blue |
| 30 | "#00bbf9", // Deep Sky Blue |
| 31 | "#a5d8ff", // Baby Blue |
| 32 | "#00f5d4", // Electric Blue |
| 33 | "#06d6a0", // Teal |
| 34 | "#1a7431", // Dartmouth Green |
| 35 | "#2a9d8f", // Jungle Green |
| 36 | "#b2f2bb", // Light Mint |
| 37 | "#9ef01a", // Lime Green |
| 38 | "#e9c46a", // Saffron |
| 39 | "#fee440", // Lemon Yellow |
| 40 | "#ffec99", // Pale Yellow |
| 41 | "#ff9f43", // Bright Orange |
| 42 | "#fb5607", // Orange Peel |
| 43 | "#ffddb5", // Peach |
| 44 | "#f95738", // Coral |
| 45 | "#e76f51", // Burnt Sienna |
| 46 | "#ff6b6b", // Vibrant Red |
| 47 | "#ffc9c9", // Light Coral |
| 48 | "#f15bb5", // Hot Pink |
| 49 | "#ff006e", // Magenta |
| 50 | "#ffafcc", // Carnation Pink |
| 51 | "#adb5bd", // Cool Gray |
| 52 | "#6c757d", // Slate Gray |
| 53 | ], |
| 54 | |
| 55 | _toFolderName(str) { |
| 56 | if (!str) return ""; |
| 57 | // a helper function to convert title to a folder safe name |
| 58 | const s = str |
| 59 | .normalize("NFD") // remove all diacritics and replace it with the latin character |
| 60 | .replace(/[\u0300-\u036f]/g, "") |
| 61 | .toLowerCase() |
| 62 | .replace(/[^a-z0-9\s-]/g, "_") // replace all special symbols with _ |
| 63 | .replace(/\s+/g, "_") // replace spaces with _ |
| 64 | .replace(/_{2,}/g, "_") // condense multiple underscores into 1 |
| 65 | .replace(/^-+|-+$/g, "") // remove any leading and trailing underscores |
| 66 | .replace(/^_+|_+$/g, ""); |
| 67 | return s; |
| 68 | }, |
| 69 | |
| 70 | getSelectedProjectSkillsPath() { |
| 71 | const projectName = this.selectedProject?.name; |
| 72 | if (!projectName) return ""; |
| 73 | return `usr/projects/${projectName}/.a0proj/skills/`; |
| 74 | }, |
| 75 | |
| 76 | async openSelectedProjectSkillsImport() { |
| 77 | const projectName = this.selectedProject?.name; |
| 78 | if (!projectName) return; |
| 79 | |
| 80 | skillsImportStore.projectKey = projectName; |
| 81 | skillsImportStore.agentProfileKey = ""; |
| 82 | await modals.openModal("settings/skills/import.html"); |
| 83 | }, |
| 84 | |
| 85 | async openSelectedProjectSkillsFolder() { |
| 86 | const path = this.getSelectedProjectSkillsPath(); |
| 87 | if (!path) return; |
| 88 | await browserStore.open(path); |
| 89 | }, |
| 90 | |
| 91 | async openProjectsModal() { |
| 92 | await this.loadProjectsList(); |
| 93 | await modals.openModal(listModal); |
| 94 | }, |
| 95 | |
| 96 | async openCreateModal() { |
| 97 | this.selectedProject = await this._createNewProjectData(); |
| 98 | await modals.openModal(createModal); |
| 99 | this.selectedProject = null; |
| 100 | }, |
| 101 | |
| 102 | async openEditModal(name) { |
| 103 | this.selectedProject = await this._createEditProjectData(name); |
| 104 | await modals.openModal(editModal); |
| 105 | this.selectedProject = null; |
| 106 | }, |
| 107 | |
| 108 | async cancelCreate() { |
| 109 | await modals.closeModal(createModal); |
| 110 | }, |
| 111 | |
| 112 | async cancelEdit() { |
| 113 | await modals.closeModal(editModal); |
| 114 | }, |
| 115 | |
| 116 | async confirmCreate() { |
| 117 | // If git_url is provided, use clone flow |
| 118 | if (this.selectedProject.git_url && this.selectedProject.git_url.trim()) { |
| 119 | await this.cloneProject(); |
| 120 | return; |
| 121 | } |
| 122 | // create folder name based on title |
| 123 | this.selectedProject.name = this._toFolderName(this.selectedProject.title); |
| 124 | const project = await this.saveSelectedProject(true); |
| 125 | await this.loadProjectsList(); |
| 126 | await modals.closeModal(createModal); |
| 127 | await this.openEditModal(project.name); |
| 128 | }, |
| 129 | |
| 130 | async cloneProject() { |
| 131 | // Security warning with custom dialog |
| 132 | const confirmed = await showConfirmDialog({ |
| 133 | title: "Security Warning", |
| 134 | message: ` |
| 135 | <p><strong>Cloning repositories from untrusted sources may pose security risks:</strong></p> |
| 136 | <ul style="margin: 0.75em 0; padding-left: 1.5em;"> |
| 137 | <li>Malicious code execution</li> |
| 138 | <li>Exposure of sensitive data</li> |
| 139 | <li>System compromise</li> |
| 140 | </ul> |
| 141 | <p style="margin-top: 0.75em;">Only clone from sources you trust.</p> |
| 142 | `, |
| 143 | type: "warning", |
| 144 | confirmText: "Clone Anyway", |
| 145 | cancelText: "Cancel" |
| 146 | }); |
| 147 | if (!confirmed) return; |
| 148 | |
| 149 | // Save reference before async operations |
| 150 | const project = this.selectedProject; |
| 151 | if (!project) return; |
| 152 | |
| 153 | // Disable button state handled by UI |
| 154 | project._cloning = true; |
| 155 | project.name = this._toFolderName(project.title); |
| 156 | |
| 157 | try { |
| 158 | const response = await api.callJsonApi("projects", { |
| 159 | action: "clone", |
| 160 | project: { |
| 161 | name: project.name, |
| 162 | title: project.title, |
| 163 | color: project.color, |
| 164 | git_url: project.git_url, |
| 165 | include_agents_md: project.include_agents_md !== false, |
| 166 | git_token: project.git_token || "", |
| 167 | llm: project.llm || null, |
| 168 | }, |
| 169 | }); |
| 170 | |
| 171 | if (response?.ok) { |
| 172 | await this.loadProjectsList(); |
| 173 | await modals.closeModal(createModal); |
| 174 | await this.openEditModal(response.data.name); |
| 175 | } else { |
| 176 | notifications.toastFrontendError( |
| 177 | response?.error || "Clone failed", |
| 178 | "Git Clone", |
| 179 | 5, |
| 180 | "git_clone", |
| 181 | notifications.NotificationPriority.NORMAL, |
| 182 | true |
| 183 | ); |
| 184 | } |
| 185 | } catch (error) { |
| 186 | console.error("Error cloning project:", error); |
| 187 | notifications.toastFrontendError( |
| 188 | "Error cloning project: " + error, |
| 189 | "Git Clone", |
| 190 | 5, |
| 191 | "git_clone", |
| 192 | notifications.NotificationPriority.NORMAL, |
| 193 | true |
| 194 | ); |
| 195 | } finally { |
| 196 | // Use the saved reference instead of this.selectedProject |
| 197 | if (project) { |
| 198 | project._cloning = false; |
| 199 | } |
| 200 | } |
| 201 | }, |
| 202 | |
| 203 | async confirmEdit() { |
| 204 | const project = await this.saveSelectedProject(false); |
| 205 | await this.loadProjectsList(); |
| 206 | await modals.closeModal(editModal); |
| 207 | }, |
| 208 | |
| 209 | async activateProject(name) { |
| 210 | try { |
| 211 | const response = await api.callJsonApi("projects", { |
| 212 | action: "activate", |
| 213 | context_id: chatsStore.getSelectedChatId(), |
| 214 | name: name, |
| 215 | }); |
| 216 | if (response?.ok) { |
| 217 | notifications.toastFrontendSuccess( |
| 218 | "Project activated successfully", |
| 219 | "Project activated", |
| 220 | 3, |
| 221 | "projects", |
| 222 | notifications.NotificationPriority.NORMAL, |
| 223 | true |
| 224 | ); |
| 225 | } else { |
| 226 | notifications.toastFrontendWarning( |
| 227 | response?.error || "Project activation reported issues", |
| 228 | "Project activation", |
| 229 | 5, |
| 230 | "projects", |
| 231 | notifications.NotificationPriority.NORMAL, |
| 232 | true |
| 233 | ); |
| 234 | } |
| 235 | } catch (error) { |
| 236 | console.error("Error activating project:", error); |
| 237 | notifications.toastFrontendError( |
| 238 | "Error activating project: " + error, |
| 239 | "Error activating project", |
| 240 | 5, |
| 241 | "projects", |
| 242 | notifications.NotificationPriority.NORMAL, |
| 243 | true |
| 244 | ); |
| 245 | } |
| 246 | await this.loadProjectsList(); |
| 247 | }, |
| 248 | |
| 249 | async deactivateProject() { |
| 250 | try { |
| 251 | const response = await api.callJsonApi("projects", { |
| 252 | action: "deactivate", |
| 253 | context_id: chatsStore.getSelectedChatId(), |
| 254 | }); |
| 255 | if (response?.ok) { |
| 256 | notifications.toastFrontendSuccess( |
| 257 | "Project deactivated successfully", |
| 258 | "Project deactivated", |
| 259 | 3, |
| 260 | "projects", |
| 261 | notifications.NotificationPriority.NORMAL, |
| 262 | true |
| 263 | ); |
| 264 | } else { |
| 265 | notifications.toastFrontendWarning( |
| 266 | response?.error || "Project deactivation reported issues", |
| 267 | "Project deactivated", |
| 268 | 5, |
| 269 | "projects", |
| 270 | notifications.NotificationPriority.NORMAL, |
| 271 | true |
| 272 | ); |
| 273 | } |
| 274 | } catch (error) { |
| 275 | console.error("Error deactivating project:", error); |
| 276 | notifications.toastFrontendError( |
| 277 | "Error deactivating project: " + error, |
| 278 | "Error deactivating project", |
| 279 | 5, |
| 280 | "projects", |
| 281 | notifications.NotificationPriority.NORMAL, |
| 282 | true |
| 283 | ); |
| 284 | } |
| 285 | await this.loadProjectsList(); |
| 286 | }, |
| 287 | |
| 288 | async deleteProjectAndCloseModal() { |
| 289 | await this.deleteProject(this.selectedProject.name); |
| 290 | await modals.closeModal(editModal); |
| 291 | }, |
| 292 | |
| 293 | async deleteProject(name) { |
| 294 | // show confirmation dialog before proceeding |
| 295 | const confirmed = window.confirm( |
| 296 | "Are you sure you want to permanently delete this project? This action is irreversible and ALL FILES will be deleted." |
| 297 | ); |
| 298 | if (!confirmed) return; |
| 299 | try { |
| 300 | const response = await api.callJsonApi("projects", { |
| 301 | action: "delete", |
| 302 | name: name, |
| 303 | }); |
| 304 | if (response.ok) { |
| 305 | notifications.toastFrontendSuccess( |
| 306 | "Project deleted successfully", |
| 307 | "Project deleted", |
| 308 | 3, |
| 309 | "projects", |
| 310 | notifications.NotificationPriority.NORMAL, |
| 311 | true |
| 312 | ); |
| 313 | await this.loadProjectsList(); |
| 314 | } else { |
| 315 | notifications.toastFrontendWarning( |
| 316 | response.error || "Project deletion blocked", |
| 317 | "Project delete", |
| 318 | 5, |
| 319 | "projects", |
| 320 | notifications.NotificationPriority.NORMAL, |
| 321 | true |
| 322 | ); |
| 323 | } |
| 324 | } catch (error) { |
| 325 | console.error("Error deleting project:", error); |
| 326 | notifications.toastFrontendError( |
| 327 | "Error deleting project: " + error, |
| 328 | "Error deleting project", |
| 329 | 5, |
| 330 | "projects", |
| 331 | notifications.NotificationPriority.NORMAL, |
| 332 | true |
| 333 | ); |
| 334 | } |
| 335 | }, |
| 336 | |
| 337 | async loadProjectsList() { |
| 338 | this.loading = true; |
| 339 | try { |
| 340 | const response = await api.callJsonApi("projects", { |
| 341 | action: "list", |
| 342 | }); |
| 343 | this.projectList = response.data || []; |
| 344 | } catch (error) { |
| 345 | console.error("Error loading projects list:", error); |
| 346 | } finally { |
| 347 | this.loading = false; |
| 348 | } |
| 349 | }, |
| 350 | |
| 351 | async saveSelectedProject(creating) { |
| 352 | try { |
| 353 | // prepare data |
| 354 | const data = { |
| 355 | ...this.selectedProject, |
| 356 | }; |
| 357 | // remove internal fields |
| 358 | for (const kvp of Object.entries(data)) |
| 359 | if (kvp[0].startsWith("_")) delete data[kvp[0]]; |
| 360 | |
| 361 | // call backend |
| 362 | const response = await api.callJsonApi("projects", { |
| 363 | action: creating ? "create" : "update", |
| 364 | project: data, |
| 365 | }); |
| 366 | // notifications |
| 367 | if (response.ok) { |
| 368 | notifications.toastFrontendSuccess( |
| 369 | "Project saved successfully", |
| 370 | "Project saved", |
| 371 | 3, |
| 372 | "projects", |
| 373 | notifications.NotificationPriority.NORMAL, |
| 374 | true |
| 375 | ); |
| 376 | const contextId = chatsStore.getSelectedChatId(); |
| 377 | if (contextId) await modelConfigStore.refreshSwitcher(contextId); |
| 378 | return response.data; |
| 379 | } else { |
| 380 | notifications.toastFrontendError( |
| 381 | response.error || "Error saving project", |
| 382 | "Error saving project", |
| 383 | 5, |
| 384 | "projects", |
| 385 | notifications.NotificationPriority.NORMAL, |
| 386 | true |
| 387 | ); |
| 388 | return null; |
| 389 | } |
| 390 | } catch (error) { |
| 391 | console.error("Error saving project:", error); |
| 392 | notifications.toastFrontendError( |
| 393 | "Error saving project: " + error, |
| 394 | "Error saving project", |
| 395 | 5, |
| 396 | "projects", |
| 397 | notifications.NotificationPriority.NORMAL, |
| 398 | true |
| 399 | ); |
| 400 | return null; |
| 401 | } |
| 402 | }, |
| 403 | |
| 404 | async _createNewProjectData() { |
| 405 | return { |
| 406 | _meta: { |
| 407 | creating: true, |
| 408 | }, |
| 409 | _cloning: false, |
| 410 | name: ``, |
| 411 | title: `Project #${this.projectList.length + 1}`, |
| 412 | description: "", |
| 413 | instructions: "", |
| 414 | include_agents_md: true, |
| 415 | color: "", |
| 416 | git_url: "", |
| 417 | git_token: "", |
| 418 | }; |
| 419 | }, |
| 420 | |
| 421 | async _createEditProjectData(name) { |
| 422 | const projectData = ( |
| 423 | await api.callJsonApi("projects", { |
| 424 | action: "load", |
| 425 | name: name, |
| 426 | }) |
| 427 | ).data; |
| 428 | return { |
| 429 | _meta: { |
| 430 | creating: false, |
| 431 | }, |
| 432 | ...projectData, |
| 433 | include_agents_md: projectData.include_agents_md !== false, |
| 434 | llm: this._normalizeProjectLlmData(projectData.llm, name), |
| 435 | }; |
| 436 | }, |
| 437 | |
| 438 | async _createProjectLlmData(projectName) { |
| 439 | await modelConfigStore.ensureLoaded(); |
| 440 | const configResult = await api.callJsonApi("/plugins/_model_config/model_config_get", { |
| 441 | project_name: projectName || "", |
| 442 | }); |
| 443 | const presetsResult = await api.callJsonApi("/plugins/_model_config/model_presets", { |
| 444 | action: "get", |
| 445 | project_name: projectName || "", |
| 446 | }); |
| 447 | return this._normalizeProjectLlmData({ |
| 448 | selected_preset: { |
| 449 | scope: "global", |
| 450 | project_name: "", |
| 451 | name: configResult.selected_preset || configResult.configured_preset || "Default", |
| 452 | }, |
| 453 | global_presets: presetsResult.global_presets || presetsResult.presets || [], |
| 454 | presets: presetsResult.presets || [], |
| 455 | }, projectName); |
| 456 | }, |
| 457 | |
| 458 | _normalizeProjectLlmData(raw, projectName) { |
| 459 | const data = raw || {}; |
| 460 | const globalPresets = this._normalizePresetsWithScope( |
| 461 | data.global_presets || [], |
| 462 | "global", |
| 463 | "" |
| 464 | ); |
| 465 | const presets = [...globalPresets]; |
| 466 | const selected = data.selected_preset || { |
| 467 | scope: "global", |
| 468 | project_name: "", |
| 469 | name: "Default", |
| 470 | }; |
| 471 | return { |
| 472 | selected_preset: selected, |
| 473 | preset_key: this.getLlmPresetKey(selected), |
| 474 | global_presets: globalPresets, |
| 475 | project_presets: [], |
| 476 | presets, |
| 477 | }; |
| 478 | }, |
| 479 | |
| 480 | _normalizePresetsWithScope(presets, defaultScope, projectName) { |
| 481 | return modelConfigStore._normalizePresets(presets || []).map((preset, index) => { |
| 482 | const raw = presets[index] || {}; |
| 483 | return { |
| 484 | ...preset, |
| 485 | scope: raw.scope || defaultScope, |
| 486 | project_name: raw.project_name || (defaultScope === "project" ? projectName : ""), |
| 487 | }; |
| 488 | }); |
| 489 | }, |
| 490 | |
| 491 | getLlmPresetKey(preset) { |
| 492 | if (!preset) return "global||Default"; |
| 493 | return `${preset.scope || "global"}|${preset.project_name || ""}|${preset.name || ""}`; |
| 494 | }, |
| 495 | |
| 496 | _findLlmPresetByKey(key) { |
| 497 | const llm = this.selectedProject?.llm; |
| 498 | if (!llm) return null; |
| 499 | return (llm.presets || []).find((preset) => this.getLlmPresetKey(preset) === key) || null; |
| 500 | }, |
| 501 | |
| 502 | applySelectedLlmPreset() { |
| 503 | const llm = this.selectedProject?.llm; |
| 504 | if (!llm) return; |
| 505 | const preset = this._findLlmPresetByKey(llm.preset_key); |
| 506 | if (!preset) return; |
| 507 | llm.selected_preset = { |
| 508 | scope: preset.scope || "global", |
| 509 | project_name: preset.project_name || "", |
| 510 | name: preset.name || "", |
| 511 | }; |
| 512 | }, |
| 513 | |
| 514 | async editSelectedProjectPresets() { |
| 515 | const llm = this.selectedProject?.llm; |
| 516 | if (!llm) return; |
| 517 | const selectedName = llm.selected_preset?.name || "Default"; |
| 518 | await modelConfigStore.openPresetEditor(selectedName); |
| 519 | await modelConfigStore.loadGlobalPresets(); |
| 520 | llm.global_presets = this._normalizePresetsWithScope( |
| 521 | modelConfigStore.globalPresets, |
| 522 | "global", |
| 523 | "" |
| 524 | ); |
| 525 | llm.presets = [...llm.global_presets]; |
| 526 | const remappedName = modelConfigStore.remapPresetName(selectedName); |
| 527 | llm.selected_preset = { |
| 528 | scope: "global", |
| 529 | project_name: "", |
| 530 | name: remappedName, |
| 531 | }; |
| 532 | llm.preset_key = this.getLlmPresetKey(llm.selected_preset); |
| 533 | }, |
| 534 | |
| 535 | async browseSelected(...relPath) { |
| 536 | const path = this.getSelectedAbsPath(...relPath); |
| 537 | return await browserStore.open(path); |
| 538 | }, |
| 539 | |
| 540 | async browseInstructionFiles() { |
| 541 | await this.browseSelected(".a0proj", "instructions"); |
| 542 | try { |
| 543 | const newData = await this._createEditProjectData( |
| 544 | this.selectedProject.name |
| 545 | ); |
| 546 | this.selectedProject.instruction_files_count = |
| 547 | newData.instruction_files_count; |
| 548 | } catch (error) { |
| 549 | //pass |
| 550 | } |
| 551 | }, |
| 552 | |
| 553 | async browseKnowledgeFiles() { |
| 554 | await this.browseSelected(".a0proj", "knowledge"); |
| 555 | // refresh and reindex project |
| 556 | try { |
| 557 | // progress notification |
| 558 | shortcuts.frontendNotification({ |
| 559 | type: shortcuts.NotificationType.PROGRESS, |
| 560 | message: "Loading knowledge...", |
| 561 | priority: shortcuts.NotificationPriority.NORMAL, |
| 562 | displayTime: 999, |
| 563 | group: "knowledge_load", |
| 564 | frontendOnly: true, |
| 565 | }); |
| 566 | |
| 567 | // call reindex knowledge |
| 568 | const reindexCall = api.callJsonApi("/plugins/_memory/knowledge_reindex", { |
| 569 | ctxid: shortcuts.getCurrentContextId(), |
| 570 | }); |
| 571 | |
| 572 | const newData = await this._createEditProjectData( |
| 573 | this.selectedProject.name |
| 574 | ); |
| 575 | this.selectedProject.knowledge_files_count = |
| 576 | newData.knowledge_files_count; |
| 577 | |
| 578 | // wait for reindex to finish |
| 579 | await reindexCall; |
| 580 | |
| 581 | // finished notification |
| 582 | shortcuts.frontendNotification({ |
| 583 | type: shortcuts.NotificationType.SUCCESS, |
| 584 | message: "Knowledge loaded successfully", |
| 585 | priority: shortcuts.NotificationPriority.NORMAL, |
| 586 | displayTime: 2, |
| 587 | group: "knowledge_load", |
| 588 | frontendOnly: true, |
| 589 | }); |
| 590 | } catch (error) { |
| 591 | // error notification |
| 592 | shortcuts.frontendNotification({ |
| 593 | type: shortcuts.NotificationType.ERROR, |
| 594 | message: "Error loading knowledge", |
| 595 | priority: shortcuts.NotificationPriority.NORMAL, |
| 596 | displayTime: 5, |
| 597 | group: "knowledge_load", |
| 598 | frontendOnly: true, |
| 599 | }); |
| 600 | } |
| 601 | }, |
| 602 | |
| 603 | getSelectedAbsPath(...relPath) { |
| 604 | return ["/a0/usr/projects", this.selectedProject.name, ...relPath] |
| 605 | .join("/") |
| 606 | .replace(/\/+/g, "/"); |
| 607 | }, |
| 608 | |
| 609 | async editActiveProject() { |
| 610 | const ctx = shortcuts.getCurrentContext(); |
| 611 | if(!ctx) return; |
| 612 | this.openEditModal(ctx.project.name); |
| 613 | }, |
| 614 | |
| 615 | async testFileStructure() { |
| 616 | try { |
| 617 | const response = await api.callJsonApi("projects", { |
| 618 | action: "file_structure", |
| 619 | name: this.selectedProject.name, |
| 620 | settings: this.selectedProject.file_structure, |
| 621 | }); |
| 622 | this.fileStructureTestOutput = response.data; |
| 623 | shortcuts.openModal("projects/project-file-structure-test.html"); |
| 624 | } catch (error) { |
| 625 | console.error("Error testing file structure:", error); |
| 626 | shortcuts.frontendNotification({ |
| 627 | type: shortcuts.NotificationType.ERROR, |
| 628 | message: "Error testing file structure", |
| 629 | priority: shortcuts.NotificationPriority.NORMAL, |
| 630 | displayTime: 3, |
| 631 | frontendOnly: true, |
| 632 | }); |
| 633 | } |
| 634 | }, |
| 635 | }; |
| 636 | |
| 637 | // convert it to alpine store |
| 638 | const store = createStore("projects", model); |
| 639 | |
| 640 | // export for use in other files |
| 641 | export { store }; |