├── .dockerignore ├── .eslintignore ├── .eslintrc-auto-import.json ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── build.yml │ ├── develop.yml │ ├── playwright.yml │ └── vitest.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── extensions.json ├── Dockerfile ├── LICENSE ├── README.md ├── auto-imports.d.ts ├── components.d.ts ├── docker-compose.yml ├── e2e └── Example.spec.ts ├── index.html ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── postcss.config.js ├── public └── favicon.ico ├── src ├── App.vue ├── assets │ ├── fonts │ │ ├── OFL.txt │ │ ├── Rubik-Bold.ttf │ │ ├── Rubik-BoldItalic.ttf │ │ ├── Rubik-Italic.ttf │ │ ├── Rubik-Medium.ttf │ │ ├── Rubik-MediumItalic.ttf │ │ ├── Rubik-Regular.ttf │ │ ├── Rubik-SemiBold.ttf │ │ └── Rubik-SemiBoldItalic.ttf │ ├── logo.svg │ └── main.css ├── components │ ├── CheckBox.vue │ ├── DynamicForm.vue │ ├── ErrorBox.vue │ ├── Header.vue │ ├── InputBox.vue │ ├── MemorySelect.vue │ ├── MessageBox.vue │ ├── ModalBox.vue │ ├── NotificationStack.vue │ ├── Pagination.vue │ ├── SelectBox.vue │ ├── SidePanel.vue │ └── UserDropdown.vue ├── composables │ ├── download.ts │ ├── perms.ts │ └── upload.ts ├── directives │ └── vLock.ts ├── globals.d.ts ├── main.ts ├── models │ ├── JSONSchema.ts │ ├── Message.ts │ ├── Notification.ts │ └── Plot.ts ├── router.ts ├── services │ ├── ApiService.ts │ ├── AuthConfigService.ts │ ├── EmbedderConfigService.ts │ ├── LLMConfigService.ts │ ├── LogService.ts │ ├── MemoryService.ts │ ├── PluginService.ts │ ├── RabbitHoleService.ts │ └── UserService.ts ├── stores │ ├── types.ts │ ├── useAuthConfig.ts │ ├── useEmbedderConfig.ts │ ├── useLLMConfig.ts │ ├── useMainStore.ts │ ├── useMemory.ts │ ├── useMessages.ts │ ├── useNotifications.ts │ ├── usePlugins.ts │ ├── useRabbitHole.ts │ └── useUsers.ts ├── utils │ ├── errors.ts │ ├── markdown.ts │ ├── schema.ts │ └── typeGuards.ts └── views │ ├── AuthView.vue │ ├── EmbeddersView.vue │ ├── ErrorView.vue │ ├── HomeView.vue │ ├── MemoryView.vue │ ├── PluginsView.vue │ ├── ProvidersView.vue │ └── SettingsView.vue ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── unit ├── MessageBox.test.ts ├── ModalBox.test.ts ├── NotificationStack.test.ts └── SelectBox.test.ts └── vite.config.mts /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | vite.config.ts 2 | public/ 3 | tailwind.config.js 4 | postcss.config.js 5 | -------------------------------------------------------------------------------- /.eslintrc-auto-import.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "Component": true, 4 | "ComponentPublicInstance": true, 5 | "ComputedRef": true, 6 | "EffectScope": true, 7 | "InjectionKey": true, 8 | "PropType": true, 9 | "Ref": true, 10 | "VNode": true, 11 | "acceptHMRUpdate": true, 12 | "afterAll": true, 13 | "afterEach": true, 14 | "assert": true, 15 | "asyncComputed": true, 16 | "autoResetRef": true, 17 | "beforeAll": true, 18 | "beforeEach": true, 19 | "chai": true, 20 | "computed": true, 21 | "computedAsync": true, 22 | "computedEager": true, 23 | "computedInject": true, 24 | "computedWithControl": true, 25 | "controlledComputed": true, 26 | "controlledRef": true, 27 | "createApp": true, 28 | "createEventHook": true, 29 | "createGlobalState": true, 30 | "createInjectionState": true, 31 | "createPinia": true, 32 | "createReactiveFn": true, 33 | "createReusableTemplate": true, 34 | "createSharedComposable": true, 35 | "createTemplatePromise": true, 36 | "createUnrefFn": true, 37 | "customRef": true, 38 | "debouncedRef": true, 39 | "debouncedWatch": true, 40 | "defineAsyncComponent": true, 41 | "defineComponent": true, 42 | "defineStore": true, 43 | "describe": true, 44 | "downloadContent": true, 45 | "eagerComputed": true, 46 | "effectScope": true, 47 | "expect": true, 48 | "extendRef": true, 49 | "generateVeeObject": true, 50 | "getActivePinia": true, 51 | "getCurrentInstance": true, 52 | "getCurrentScope": true, 53 | "getErrorMessage": true, 54 | "h": true, 55 | "ignorableWatch": true, 56 | "inject": true, 57 | "isApiError": true, 58 | "isDefined": true, 59 | "isError": true, 60 | "isErrorLikeObject": true, 61 | "isProxy": true, 62 | "isReactive": true, 63 | "isReadonly": true, 64 | "isRef": true, 65 | "isString": true, 66 | "it": true, 67 | "makeDestructurable": true, 68 | "mapActions": true, 69 | "mapGetters": true, 70 | "mapState": true, 71 | "mapStores": true, 72 | "mapWritableState": true, 73 | "markRaw": true, 74 | "nextTick": true, 75 | "onActivated": true, 76 | "onBeforeMount": true, 77 | "onBeforeRouteLeave": true, 78 | "onBeforeRouteUpdate": true, 79 | "onBeforeUnmount": true, 80 | "onBeforeUpdate": true, 81 | "onClickOutside": true, 82 | "onDeactivated": true, 83 | "onErrorCaptured": true, 84 | "onKeyStroke": true, 85 | "onLongPress": true, 86 | "onMounted": true, 87 | "onRenderTracked": true, 88 | "onRenderTriggered": true, 89 | "onScopeDispose": true, 90 | "onServerPrefetch": true, 91 | "onStartTyping": true, 92 | "onUnmounted": true, 93 | "onUpdated": true, 94 | "pausableWatch": true, 95 | "provide": true, 96 | "reactify": true, 97 | "reactifyObject": true, 98 | "reactive": true, 99 | "reactiveComputed": true, 100 | "reactiveOmit": true, 101 | "reactivePick": true, 102 | "readonly": true, 103 | "ref": true, 104 | "refAutoReset": true, 105 | "refDebounced": true, 106 | "refDefault": true, 107 | "refThrottled": true, 108 | "refWithControl": true, 109 | "resolveComponent": true, 110 | "resolveRef": true, 111 | "resolveUnref": true, 112 | "setActivePinia": true, 113 | "setMapStoreSuffix": true, 114 | "shallowReactive": true, 115 | "shallowReadonly": true, 116 | "shallowRef": true, 117 | "storeToRefs": true, 118 | "suite": true, 119 | "syncRef": true, 120 | "syncRefs": true, 121 | "templateRef": true, 122 | "test": true, 123 | "throttledRef": true, 124 | "throttledWatch": true, 125 | "toRaw": true, 126 | "toReactive": true, 127 | "toRef": true, 128 | "toRefs": true, 129 | "toValue": true, 130 | "triggerRef": true, 131 | "tryOnBeforeMount": true, 132 | "tryOnBeforeUnmount": true, 133 | "tryOnMounted": true, 134 | "tryOnScopeDispose": true, 135 | "tryOnUnmounted": true, 136 | "unref": true, 137 | "unrefElement": true, 138 | "until": true, 139 | "uploadContent": true, 140 | "useActiveElement": true, 141 | "useAnimate": true, 142 | "useArrayDifference": true, 143 | "useArrayEvery": true, 144 | "useArrayFilter": true, 145 | "useArrayFind": true, 146 | "useArrayFindIndex": true, 147 | "useArrayFindLast": true, 148 | "useArrayIncludes": true, 149 | "useArrayJoin": true, 150 | "useArrayMap": true, 151 | "useArrayReduce": true, 152 | "useArraySome": true, 153 | "useArrayUnique": true, 154 | "useAsyncQueue": true, 155 | "useAsyncState": true, 156 | "useAttrs": true, 157 | "useBase64": true, 158 | "useBattery": true, 159 | "useBluetooth": true, 160 | "useBreakpoints": true, 161 | "useBroadcastChannel": true, 162 | "useBrowserLocation": true, 163 | "useCached": true, 164 | "useClipboard": true, 165 | "useCloned": true, 166 | "useColorMode": true, 167 | "useConfirmDialog": true, 168 | "useCounter": true, 169 | "useCssModule": true, 170 | "useCssVar": true, 171 | "useCssVars": true, 172 | "useCurrentElement": true, 173 | "useCycleList": true, 174 | "useDark": true, 175 | "useDateFormat": true, 176 | "useDebounce": true, 177 | "useDebounceFn": true, 178 | "useDebouncedRefHistory": true, 179 | "useDeviceMotion": true, 180 | "useDeviceOrientation": true, 181 | "useDevicePixelRatio": true, 182 | "useDevicesList": true, 183 | "useDisplayMedia": true, 184 | "useDocumentVisibility": true, 185 | "useDraggable": true, 186 | "useDropZone": true, 187 | "useElementBounding": true, 188 | "useElementByPoint": true, 189 | "useElementHover": true, 190 | "useElementSize": true, 191 | "useElementVisibility": true, 192 | "useEventBus": true, 193 | "useEventListener": true, 194 | "useEventSource": true, 195 | "useEyeDropper": true, 196 | "useFavicon": true, 197 | "useFetch": true, 198 | "useFileDialog": true, 199 | "useFileSystemAccess": true, 200 | "useFocus": true, 201 | "useFocusWithin": true, 202 | "useFps": true, 203 | "useFullscreen": true, 204 | "useGamepad": true, 205 | "useGeolocation": true, 206 | "useIdle": true, 207 | "useImage": true, 208 | "useInfiniteScroll": true, 209 | "useIntersectionObserver": true, 210 | "useInterval": true, 211 | "useIntervalFn": true, 212 | "useKeyModifier": true, 213 | "useLastChanged": true, 214 | "useLink": true, 215 | "useLocalStorage": true, 216 | "useMagicKeys": true, 217 | "useManualRefHistory": true, 218 | "useMediaControls": true, 219 | "useMediaQuery": true, 220 | "useMemoize": true, 221 | "useMemory": true, 222 | "useMounted": true, 223 | "useMouse": true, 224 | "useMouseInElement": true, 225 | "useMousePressed": true, 226 | "useMutationObserver": true, 227 | "useNavigatorLanguage": true, 228 | "useNetwork": true, 229 | "useNow": true, 230 | "useObjectUrl": true, 231 | "useOffsetPagination": true, 232 | "useOnline": true, 233 | "usePageLeave": true, 234 | "useParallax": true, 235 | "useParentElement": true, 236 | "usePerformanceObserver": true, 237 | "usePermission": true, 238 | "usePointer": true, 239 | "usePointerLock": true, 240 | "usePointerSwipe": true, 241 | "usePreferredColorScheme": true, 242 | "usePreferredContrast": true, 243 | "usePreferredDark": true, 244 | "usePreferredLanguages": true, 245 | "usePreferredReducedMotion": true, 246 | "usePrevious": true, 247 | "useRafFn": true, 248 | "useRefHistory": true, 249 | "useResizeObserver": true, 250 | "useRoute": true, 251 | "useRouter": true, 252 | "useScreenOrientation": true, 253 | "useScreenSafeArea": true, 254 | "useScriptTag": true, 255 | "useScroll": true, 256 | "useScrollLock": true, 257 | "useSessionStorage": true, 258 | "useShare": true, 259 | "useSlots": true, 260 | "useSorted": true, 261 | "useSpeechRecognition": true, 262 | "useSpeechSynthesis": true, 263 | "useStepper": true, 264 | "useStorage": true, 265 | "useStorageAsync": true, 266 | "useStyleTag": true, 267 | "useSupported": true, 268 | "useSwipe": true, 269 | "useTemplateRefsList": true, 270 | "useTextDirection": true, 271 | "useTextSelection": true, 272 | "useTextareaAutosize": true, 273 | "useThrottle": true, 274 | "useThrottleFn": true, 275 | "useThrottledRefHistory": true, 276 | "useTimeAgo": true, 277 | "useTimeout": true, 278 | "useTimeoutFn": true, 279 | "useTimeoutPoll": true, 280 | "useTimestamp": true, 281 | "useTitle": true, 282 | "useToNumber": true, 283 | "useToString": true, 284 | "useToggle": true, 285 | "useTransition": true, 286 | "useUrlSearchParams": true, 287 | "useUserMedia": true, 288 | "useVModel": true, 289 | "useVModels": true, 290 | "useVibrate": true, 291 | "useVirtualList": true, 292 | "useWakeLock": true, 293 | "useWebNotification": true, 294 | "useWebSocket": true, 295 | "useWebWorker": true, 296 | "useWebWorkerFn": true, 297 | "useWindowFocus": true, 298 | "useWindowScroll": true, 299 | "useWindowSize": true, 300 | "vi": true, 301 | "vitest": true, 302 | "watch": true, 303 | "watchArray": true, 304 | "watchAtMost": true, 305 | "watchDebounced": true, 306 | "watchDeep": true, 307 | "watchEffect": true, 308 | "watchIgnorable": true, 309 | "watchImmediate": true, 310 | "watchOnce": true, 311 | "watchPausable": true, 312 | "watchPostEffect": true, 313 | "watchSyncEffect": true, 314 | "watchThrottled": true, 315 | "watchTriggerable": true, 316 | "watchWithFilter": true, 317 | "whenever": true, 318 | "ExtractDefaultPropTypes": true, 319 | "ExtractPropTypes": true, 320 | "ExtractPublicPropTypes": true, 321 | "WritableComputedRef": true, 322 | "injectLocal": true, 323 | "provideLocal": true, 324 | "storeRouteMapping": true, 325 | "useClipboardItems": true, 326 | "markdown": true, 327 | "usePerms": true, 328 | "DirectiveBinding": true, 329 | "MaybeRef": true, 330 | "MaybeRefOrGetter": true, 331 | "onWatcherCleanup": true, 332 | "useId": true, 333 | "useModel": true, 334 | "useTemplateRef": true, 335 | "onElementRemoval": true, 336 | "usePreferredReducedTransparency": true, 337 | "useSSRWidth": true 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "./.eslintrc-auto-import.json", 5 | "eslint:recommended", 6 | "plugin:vue/vue3-recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:tailwindcss/recommended", 9 | "@vue/typescript/recommended", 10 | "plugin:vue/vue3-essential", 11 | "@vue/eslint-config-typescript", 12 | "plugin:prettier/recommended" 13 | ], 14 | "ignorePatterns": ["**/*.config.js"], 15 | "plugins": ["@typescript-eslint", "prettier"], 16 | "parser": "vue-eslint-parser", 17 | "parserOptions": { 18 | "ecmaVersion": "latest", 19 | "parser": "@typescript-eslint/parser" 20 | }, 21 | "rules": { 22 | "prettier/prettier": "error", 23 | "@typescript-eslint/consistent-type-imports": "warn", 24 | "arrow-spacing": ["warn", { "before": true, "after": true }], 25 | "no-var": "error", 26 | "@typescript-eslint/no-explicit-any": "warn", 27 | "keyword-spacing": "error", 28 | "space-infix-ops": "error", 29 | "space-unary-ops": "error", 30 | "prefer-const": "error", 31 | "object-curly-spacing": ["error", "always"], 32 | "no-empty-function": "off", 33 | "@typescript-eslint/no-empty-function": "warn", 34 | "vue/multi-word-component-names": "off", 35 | "vue/max-attributes-per-line": "off", 36 | "vue/first-attribute-linebreak": "off", 37 | "vue/html-indent": [ 38 | "error", 39 | "tab", 40 | { 41 | "attribute": 1, 42 | "baseIndent": 1, 43 | "closeBracket": 0, 44 | "alignAttributesVertically": true, 45 | "ignores": [] 46 | } 47 | ], 48 | "vue/html-closing-bracket-newline": [ 49 | "warn", 50 | { 51 | "singleline": "never", 52 | "multiline": "never" 53 | } 54 | ], 55 | "vue/v-on-event-hyphenation": "off", 56 | "vue/attribute-hyphenation": "off", 57 | "tailwindcss/no-custom-classname": "off" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[Bug]' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Ask or suggest any type of things related to the project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[Feature]' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | # Checklist: 17 | 18 | - [ ] My code follows the style guidelines of this project 19 | - [ ] I have performed a self-review of my own code 20 | - [ ] I have commented my code, particularly in hard-to-understand areas 21 | - [ ] My changes generate no new warnings 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Release Main Zip 2 | 3 | on: 4 | push: 5 | branches: [main, master] 6 | paths-ignore: 7 | - 'README.md' 8 | - 'LICENSE' 9 | - 'Dockerfile' 10 | - 'docker-compose.yml' 11 | - '.gitignore' 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | permissions: 18 | contents: write 19 | issues: write 20 | 21 | jobs: 22 | build: 23 | timeout-minutes: 120 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: pnpm/action-setup@v4 28 | with: 29 | version: 9 30 | - name: Install dependencies 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: 18.x 34 | cache: 'pnpm' 35 | - run: pnpm install --frozen-lockfile 36 | - name: 'TODO to Issue' 37 | uses: alstr/todo-to-issue-action@v4 38 | with: 39 | IDENTIFIERS: '[{"name": "FEATURE", "labels": ["enhancement"]}, {"name": "BUG", "labels": ["bug"]}]' 40 | ISSUE_TEMPLATE: '**Describe the reason of this issue**\n{{ body }}\n\nThe issue is present here:\n\n{{ snippet }}' 41 | - name: Build static files 42 | run: pnpm run build 43 | - name: Zip Release 44 | uses: TheDoctor0/zip-release@0.7.6 45 | with: 46 | type: 'zip' 47 | filename: 'release.zip' 48 | directory: './dist' 49 | path: '.' 50 | - name: Upload Release 51 | uses: ncipollo/release-action@v1.14.0 52 | with: 53 | tag: 'Admin' 54 | artifacts: './dist/release.zip' 55 | allowUpdates: true 56 | replacesArtifacts: true 57 | body: | 58 | ${{ github.event.head_commit.message }} 59 | token: ${{ secrets.GITHUB_TOKEN }} 60 | -------------------------------------------------------------------------------- /.github/workflows/develop.yml: -------------------------------------------------------------------------------- 1 | name: Release Develop Zip 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | paths-ignore: 7 | - 'README.md' 8 | - 'LICENSE' 9 | - 'Dockerfile' 10 | - 'docker-compose.yml' 11 | - '.gitignore' 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | permissions: 18 | contents: write 19 | 20 | jobs: 21 | build: 22 | timeout-minutes: 120 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: pnpm/action-setup@v4 27 | with: 28 | version: 9 29 | - name: Install dependencies 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: 18.x 33 | cache: 'pnpm' 34 | - run: pnpm install --frozen-lockfile 35 | - name: Build static files 36 | run: pnpm run build 37 | - name: Zip Release 38 | uses: TheDoctor0/zip-release@0.7.6 39 | with: 40 | type: 'zip' 41 | filename: 'develop.zip' 42 | directory: './dist' 43 | path: '.' 44 | - name: Upload Release 45 | uses: ncipollo/release-action@v1.14.0 46 | with: 47 | tag: 'Admin' 48 | artifacts: './dist/develop.zip' 49 | allowUpdates: true 50 | replacesArtifacts: true 51 | body: | 52 | ${{ github.event.head_commit.message }} 53 | token: ${{ secrets.GITHUB_TOKEN }} 54 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright E2E Tests 2 | 3 | on: 4 | push: 5 | branches: [main, master, develop] 6 | paths: 7 | - '**.ts' 8 | - '**.vue' 9 | pull_request: 10 | branches: [main, master, develop] 11 | paths: 12 | - '**.ts' 13 | - '**.vue' 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | test: 21 | timeout-minutes: 60 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: pnpm/action-setup@v4 26 | with: 27 | version: 9 28 | - name: Install dependencies 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: 18.x 32 | cache: 'pnpm' 33 | - run: pnpm install --frozen-lockfile 34 | - name: Install Playwright Browsers 35 | run: pnpx playwright install --with-deps 36 | - name: Run Playwright tests 37 | run: pnpx playwright test 38 | - uses: actions/upload-artifact@v4 39 | if: always() 40 | with: 41 | name: playwright-report 42 | path: playwright-report/ 43 | retention-days: 30 44 | -------------------------------------------------------------------------------- /.github/workflows/vitest.yml: -------------------------------------------------------------------------------- 1 | name: Vitest Unit Tests 2 | 3 | on: 4 | push: 5 | branches: [main, master, develop] 6 | paths: 7 | - '**.ts' 8 | - '**.vue' 9 | pull_request: 10 | branches: [main, master, develop] 11 | paths: 12 | - '**.ts' 13 | - '**.vue' 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | test: 21 | timeout-minutes: 60 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: pnpm/action-setup@v4 26 | with: 27 | version: 9 28 | - name: Install dependencies 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: 18.x 32 | cache: 'pnpm' 33 | - run: pnpm install --frozen-lockfile 34 | - name: Run Vitest tests 35 | run: pnpm run unit 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .env.local 27 | 28 | dist/ 29 | build/ 30 | static/ 31 | tsconfig.tsbuildinfo 32 | /test-results/ 33 | /playwright-report/ 34 | /playwright/.cache/ 35 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | auto-imports.d.ts 2 | components.d.ts 3 | .eslintrc-auto-import.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "bracketSameLine": true, 5 | "printWidth": 140, 6 | "useTabs": true, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18 2 | 3 | # Create app directory 4 | WORKDIR /app 5 | 6 | RUN rm -fr node_modules 7 | 8 | # Expose port 3000 and build + start application 9 | EXPOSE 3000 10 | CMD ["/bin/bash", "-c", "npm install; npm run dev"] 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Discord Server 3 | 4 | 5 | GitHub Stars 6 | 7 | 8 | GitHub Forks 9 | 10 | 11 | GitHub License 12 | 13 | 14 | GitHub Issues 15 | 16 | 17 | GitHub Pull Requests 18 | 19 | 20 | # Cheshire Cat Admin UI 🐱 21 | 22 | This is the source code to build the admin client for the Cheshire Cat AI. 23 | 24 | ## About the project 25 | 26 | The Cheshire Cat is a framework to build long-tail AIs: 27 | 28 | - Language model agnostic, allows compatibility with OpenAI, Cohere, HuggingFace, and custom models 29 | - Long-term memory storage capabilities 30 | - Seamless integration with external tools, such as APIs and other models 31 | - Ability to ingest various document formats, including PDFs and text files 32 | - 100% dockerized for simple and efficient deployment 33 | - Extensibility via plugins, offering unparalleled flexibility to users. 34 | 35 | ### Pre-requisites 36 | 37 | Make sure you have the following installed on your machine: 38 | 39 | ```bash 40 | node v18.15+ 41 | ``` 42 | 43 | ### Installation 44 | 45 | This project uses `pnpm` as the package manager. You can install it by running: 46 | 47 | ```bash 48 | npm install -g pnpm 49 | ``` 50 | 51 | Then, install the dependencies: 52 | 53 | ```bash 54 | pnpm install 55 | ``` 56 | 57 | ### Scripts 58 | 59 | Here's a list of scripts that you can run to get the app up and running 60 | 61 | #### Dev mode 62 | 63 | Run the app in dev mode with hot-reloading enabled and the browser automatically opening on port `3000` (default) 64 | 65 | ```bash 66 | pnpm run dev 67 | ``` 68 | 69 | #### Build 70 | 71 | Build the app for production 72 | 73 | ```bash 74 | pnpm run build 75 | ``` 76 | 77 | #### Start the app 78 | 79 | Runs the build and serves the built app on port `3000` (default) 80 | 81 | ```bash 82 | pnpm run preview 83 | ``` 84 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | CheckBox: typeof import('./src/components/CheckBox.vue')['default'] 11 | Dialog: typeof import('@headlessui/vue')['Dialog'] 12 | DialogPanel: typeof import('@headlessui/vue')['DialogPanel'] 13 | DialogTitle: typeof import('@headlessui/vue')['DialogTitle'] 14 | DynamicForm: typeof import('./src/components/DynamicForm.vue')['default'] 15 | ErrorBox: typeof import('./src/components/ErrorBox.vue')['default'] 16 | Header: typeof import('./src/components/Header.vue')['default'] 17 | HeroiconsAdjustmentsVertical: typeof import('~icons/heroicons/adjustments-vertical')['default'] 18 | HeroiconsArrowDown20Solid: typeof import('~icons/heroicons/arrow-down20-solid')['default'] 19 | HeroiconsArrowPath: typeof import('~icons/heroicons/arrow-path')['default'] 20 | HeroiconsArrowRightOnRectangle: typeof import('~icons/heroicons/arrow-right-on-rectangle')['default'] 21 | HeroiconsBars3Solid: typeof import('~icons/heroicons/bars3-solid')['default'] 22 | HeroiconsBoltSolid: typeof import('~icons/heroicons/bolt-solid')['default'] 23 | HeroiconsChevronUpDown20Solid: typeof import('~icons/heroicons/chevron-up-down20-solid')['default'] 24 | HeroiconsClipboard: typeof import('~icons/heroicons/clipboard')['default'] 25 | HeroiconsCloudArrowDownSolid: typeof import('~icons/heroicons/cloud-arrow-down-solid')['default'] 26 | HeroiconsCog6Tooth20Solid: typeof import('~icons/heroicons/cog6-tooth20-solid')['default'] 27 | HeroiconsDocumentTextSolid: typeof import('~icons/heroicons/document-text-solid')['default'] 28 | HeroiconsGlobeAlt: typeof import('~icons/heroicons/globe-alt')['default'] 29 | HeroiconsHome20Solid: typeof import('~icons/heroicons/home20-solid')['default'] 30 | HeroiconsInformationCircleSolid: typeof import('~icons/heroicons/information-circle-solid')['default'] 31 | HeroiconsLink20Solid: typeof import('~icons/heroicons/link20-solid')['default'] 32 | HeroiconsMagnifyingGlass20Solid: typeof import('~icons/heroicons/magnifying-glass20-solid')['default'] 33 | HeroiconsMicrophoneSolid: typeof import('~icons/heroicons/microphone-solid')['default'] 34 | HeroiconsMoonSolid: typeof import('~icons/heroicons/moon-solid')['default'] 35 | HeroiconsPaperAirplaneSolid: typeof import('~icons/heroicons/paper-airplane-solid')['default'] 36 | HeroiconsSunSolid: typeof import('~icons/heroicons/sun-solid')['default'] 37 | HeroiconsTrashSolid: typeof import('~icons/heroicons/trash-solid')['default'] 38 | HeroiconsUserSolid: typeof import('~icons/heroicons/user-solid')['default'] 39 | HeroiconsXMark20Solid: typeof import('~icons/heroicons/x-mark20-solid')['default'] 40 | InputBox: typeof import('./src/components/InputBox.vue')['default'] 41 | Listbox: typeof import('@headlessui/vue')['Listbox'] 42 | ListboxButton: typeof import('@headlessui/vue')['ListboxButton'] 43 | ListboxOption: typeof import('@headlessui/vue')['ListboxOption'] 44 | ListboxOptions: typeof import('@headlessui/vue')['ListboxOptions'] 45 | MemorySelect: typeof import('./src/components/MemorySelect.vue')['default'] 46 | Menu: typeof import('@headlessui/vue')['Menu'] 47 | MenuButton: typeof import('@headlessui/vue')['MenuButton'] 48 | MenuItem: typeof import('@headlessui/vue')['MenuItem'] 49 | MenuItems: typeof import('@headlessui/vue')['MenuItems'] 50 | MessageBox: typeof import('./src/components/MessageBox.vue')['default'] 51 | ModalBox: typeof import('./src/components/ModalBox.vue')['default'] 52 | NotificationStack: typeof import('./src/components/NotificationStack.vue')['default'] 53 | Pagination: typeof import('./src/components/Pagination.vue')['default'] 54 | PhArrowCounterClockwiseBold: typeof import('~icons/ph/arrow-counter-clockwise-bold')['default'] 55 | PhBrainFill: typeof import('~icons/ph/brain-fill')['default'] 56 | PhCaretLeftFill: typeof import('~icons/ph/caret-left-fill')['default'] 57 | PhCaretRightFill: typeof import('~icons/ph/caret-right-fill')['default'] 58 | PhChatCenteredDots: typeof import('~icons/ph/chat-centered-dots')['default'] 59 | PhChats: typeof import('~icons/ph/chats')['default'] 60 | PhExportBold: typeof import('~icons/ph/export-bold')['default'] 61 | PhFileFill: typeof import('~icons/ph/file-fill')['default'] 62 | PhFiles: typeof import('~icons/ph/files')['default'] 63 | PhFloppyDiskBold: typeof import('~icons/ph/floppy-disk-bold')['default'] 64 | PhInfo: typeof import('~icons/ph/info')['default'] 65 | PhLightbulbFilamentFill: typeof import('~icons/ph/lightbulb-filament-fill')['default'] 66 | PhListMagnifyingGlass: typeof import('~icons/ph/list-magnifying-glass')['default'] 67 | PhNut: typeof import('~icons/ph/nut')['default'] 68 | PhPencilFill: typeof import('~icons/ph/pencil-fill')['default'] 69 | PhPlugFill: typeof import('~icons/ph/plug-fill')['default'] 70 | PhPlus: typeof import('~icons/ph/plus')['default'] 71 | PhQuestionMark: typeof import('~icons/ph/question-mark')['default'] 72 | PhTextbox: typeof import('~icons/ph/textbox')['default'] 73 | PhToolbox: typeof import('~icons/ph/toolbox')['default'] 74 | PhTrashFill: typeof import('~icons/ph/trash-fill')['default'] 75 | RouterLink: typeof import('vue-router')['RouterLink'] 76 | RouterView: typeof import('vue-router')['RouterView'] 77 | SelectBox: typeof import('./src/components/SelectBox.vue')['default'] 78 | SidePanel: typeof import('./src/components/SidePanel.vue')['default'] 79 | TransitionChild: typeof import('@headlessui/vue')['TransitionChild'] 80 | TransitionRoot: typeof import('@headlessui/vue')['TransitionRoot'] 81 | UseImage: typeof import('@vueuse/components')['UseImage'] 82 | UserDropdown: typeof import('./src/components/UserDropdown.vue')['default'] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | cheshire-cat-admin: 5 | build: 6 | context: . 7 | container_name: cheshire_cat_admin 8 | ports: 9 | - 3000:3000 10 | volumes: 11 | - ./:/app 12 | restart: unless-stopped 13 | -------------------------------------------------------------------------------- /e2e/Example.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { test, expect } from '@playwright/test' 3 | 4 | test('has title', async ({ page }) => { 5 | await page.goto('https://playwright.dev/') 6 | await expect(page).toHaveTitle(/Playwright/) 7 | }) 8 | 9 | test('get started link', async ({ page }) => { 10 | await page.goto('https://playwright.dev/') 11 | await page.getByRole('link', { name: 'Get started' }).click() 12 | await expect(page).toHaveURL(/.*intro/) 13 | }) 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cheshire Cat 8 | 9 | 10 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ccat-admin", 3 | "version": "1.0.0", 4 | "private": true, 5 | "author": "Project contributors", 6 | "scripts": { 7 | "preinstall": "npx only-allow pnpm", 8 | "dev": "vite", 9 | "build": "run-p typecheck build-only", 10 | "preview": "vite preview", 11 | "build-only": "vite build --base=/admin/", 12 | "typecheck": "vue-tsc --noEmit", 13 | "e2e": "playwright test", 14 | "e2e:ui": "playwright test show-report", 15 | "unit": "vitest --run", 16 | "unit:ui": "vitest --ui", 17 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 18 | "format": "prettier . --write", 19 | "fix": "run-p lint format" 20 | }, 21 | "dependencies": { 22 | "@casl/ability": "^6.7.1", 23 | "@casl/vue": "^2.2.2", 24 | "@headlessui/vue": "^1.7.23", 25 | "@saehrimnir/druidjs": "^0.7.3", 26 | "@tailwindcss/forms": "^0.5.9", 27 | "@types/lodash": "^4.17.10", 28 | "@vee-validate/rules": "^4.11.6", 29 | "@vueuse/components": "^11.1.0", 30 | "@vueuse/core": "^11.1.0", 31 | "@vueuse/integrations": "^11.1.0", 32 | "animate.css": "^4.1.1", 33 | "apexcharts": "^3.54.1", 34 | "axios": "^1.7.7", 35 | "ccat-api": "^0.12.1", 36 | "daisyui": "^4.12.13", 37 | "highlight.js": "^11.10.0", 38 | "jwt-decode": "^4.0.0", 39 | "lodash": "^4.17.21", 40 | "pinia": "^2.2.4", 41 | "remarkable": "^2.0.1", 42 | "universal-cookie": "^7.2.1", 43 | "vee-validate": "^4.12.6", 44 | "vite-tsconfig-paths": "^5.0.1", 45 | "vue": "^3.5.12", 46 | "vue-component-type-helpers": "^2.1.6", 47 | "vue-router": "^4.4.5", 48 | "vue3-apexcharts": "^1.7.0" 49 | }, 50 | "devDependencies": { 51 | "@iconify-json/heroicons": "^1.2.1", 52 | "@iconify-json/ph": "^1.2.1", 53 | "@pinia/testing": "^0.1.6", 54 | "@playwright/test": "^1.48.0", 55 | "@rushstack/eslint-patch": "^1.10.4", 56 | "@tsconfig/node18": "^18.2.4", 57 | "@types/jsdom": "^21.1.7", 58 | "@types/node": "^22.7.5", 59 | "@types/remarkable": "^2.0.8", 60 | "@vitejs/plugin-vue": "^5.1.4", 61 | "@vitest/ui": "^2.1.3", 62 | "@vue/eslint-config-typescript": "^14.1.0", 63 | "@vue/test-utils": "^2.4.6", 64 | "@vue/tsconfig": "^0.5.1", 65 | "autoprefixer": "^10.4.20", 66 | "eslint": "8.57.0", 67 | "eslint-config-prettier": "^9.1.0", 68 | "eslint-plugin-prettier": "^5.2.1", 69 | "eslint-plugin-tailwindcss": "^3.17.5", 70 | "eslint-plugin-vue": "^9.29.0", 71 | "jsdom": "^24.1.0", 72 | "npm-run-all": "^4.1.5", 73 | "postcss": "^8.4.47", 74 | "prettier": "3.3.3", 75 | "tailwindcss": "^3.4.13", 76 | "typescript": "~5.6.3", 77 | "unplugin-auto-import": "^0.18.3", 78 | "unplugin-fonts": "^1.1.1", 79 | "unplugin-icons": "^0.19.3", 80 | "unplugin-vue-components": "^0.27.4", 81 | "vite": "^5.4.9", 82 | "vitest": "^2.1.3", 83 | "vue-tsc": "^2.1.6" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test' 2 | 3 | /** 4 | * See https://playwright.dev/docs/test-configuration. 5 | */ 6 | export default defineConfig({ 7 | testDir: './e2e', 8 | timeout: 30 * 1000, 9 | fullyParallel: true, 10 | expect: { 11 | /** 12 | * Maximum time expect() should wait for the condition to be met. 13 | * For example in `await expect(locator).toHaveText();` 14 | */ 15 | timeout: 5000, 16 | }, 17 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 18 | forbidOnly: !!process.env.CI, 19 | /* Retry on CI only */ 20 | retries: process.env.CI ? 2 : 0, 21 | /* Opt out of parallel tests on CI. */ 22 | workers: process.env.CI ? 1 : undefined, 23 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 24 | reporter: process.env.CI ? 'github' : 'html', 25 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 26 | use: { 27 | /* Base URL to use in actions like `await page.goto('/')`. */ 28 | // baseURL: 'http://127.0.0.1:3000', 29 | 30 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 31 | trace: 'on-first-retry', 32 | }, 33 | 34 | /* Configure projects for major browsers */ 35 | projects: [ 36 | { 37 | name: 'chromium', 38 | use: { ...devices['Desktop Chrome'] }, 39 | }, 40 | 41 | { 42 | name: 'firefox', 43 | use: { ...devices['Desktop Firefox'] }, 44 | }, 45 | 46 | { 47 | name: 'webkit', 48 | use: { ...devices['Desktop Safari'] }, 49 | }, 50 | 51 | /* Test against mobile viewports. */ 52 | // { 53 | // name: 'Mobile Chrome', 54 | // use: { ...devices['Pixel 5'] }, 55 | // }, 56 | // { 57 | // name: 'Mobile Safari', 58 | // use: { ...devices['iPhone 12'] }, 59 | // }, 60 | ], 61 | webServer: { 62 | command: process.env.CI ? 'vite preview --port 3000' : 'vite dev', 63 | port: 3000, 64 | reuseExistingServer: !process.env.CI, 65 | }, 66 | }) 67 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheshire-cat-ai/admin-vue/deac88da519505a045c29444f06e9375b0f6423f/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | -------------------------------------------------------------------------------- /src/assets/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /src/assets/fonts/Rubik-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheshire-cat-ai/admin-vue/deac88da519505a045c29444f06e9375b0f6423f/src/assets/fonts/Rubik-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Rubik-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheshire-cat-ai/admin-vue/deac88da519505a045c29444f06e9375b0f6423f/src/assets/fonts/Rubik-BoldItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Rubik-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheshire-cat-ai/admin-vue/deac88da519505a045c29444f06e9375b0f6423f/src/assets/fonts/Rubik-Italic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Rubik-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheshire-cat-ai/admin-vue/deac88da519505a045c29444f06e9375b0f6423f/src/assets/fonts/Rubik-Medium.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Rubik-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheshire-cat-ai/admin-vue/deac88da519505a045c29444f06e9375b0f6423f/src/assets/fonts/Rubik-MediumItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Rubik-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheshire-cat-ai/admin-vue/deac88da519505a045c29444f06e9375b0f6423f/src/assets/fonts/Rubik-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Rubik-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheshire-cat-ai/admin-vue/deac88da519505a045c29444f06e9375b0f6423f/src/assets/fonts/Rubik-SemiBold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Rubik-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheshire-cat-ai/admin-vue/deac88da519505a045c29444f06e9375b0f6423f/src/assets/fonts/Rubik-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .animate__animated.animate__fastest { 6 | -webkit-animation-duration: calc(1s / 4); 7 | animation-duration: calc(1s / 4); 8 | -webkit-animation-duration: calc(var(--animate-duration) / 4); 9 | animation-duration: calc(var(--animate-duration) / 4); 10 | } 11 | 12 | @layer base { 13 | html { 14 | font-family: 'Rubik', system-ui, sans-serif; 15 | } 16 | body { 17 | scrollbar-width: 10px; 18 | scrollbar-color: theme(backgroundColor.base-100) theme(backgroundColor.neutral); 19 | } 20 | ::-webkit-scrollbar { 21 | width: 10px; 22 | height: 10px; 23 | } 24 | ::-webkit-scrollbar-thumb { 25 | background: theme(backgroundColor.neutral); 26 | border-radius: 10px; 27 | } 28 | input[type='number'] { 29 | @apply !pr-0; 30 | } 31 | audio { 32 | @apply h-10; 33 | } 34 | audio::-webkit-media-controls-enclosure { 35 | @apply bg-base-100 max-h-10 rounded-lg; 36 | } 37 | audio::-webkit-media-controls-mute-button, 38 | video::-webkit-media-controls-mute-button { 39 | background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJXaW5kb3dUZXh0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCiAgICA8cGF0aCBkPSJNMyA5djZoNGw1IDVWNEw3IDlIM3ptMTMuNSAzYzAtMS43Ny0xLjAyLTMuMjktMi41LTQuMDN2OC4wNWMxLjQ4LS43MyAyLjUtMi4yNSAyLjUtNC4wMnpNMTQgMy4yM3YyLjA2YzIuODkuODYgNSAzLjU0IDUgNi43MXMtMi4xMSA1Ljg1LTUgNi43MXYyLjA2YzQuMDEtLjkxIDctNC40OSA3LTguNzdzLTIuOTktNy44Ni03LTguNzd6Ii8+DQogICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPg0KPC9zdmc+'); 40 | } 41 | [data-theme='dark'] audio::-webkit-media-controls-mute-button, 42 | [data-theme='dark'] video::-webkit-media-controls-mute-button { 43 | background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJXaW5kb3ciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIGQ9Ik0zIDl2Nmg0bDUgNVY0TDcgOUgzem0xMy41IDNjMC0xLjc3LTEuMDItMy4yOS0yLjUtNC4wM3Y4LjA1YzEuNDgtLjczIDIuNS0yLjI1IDIuNS00LjAyek0xNCAzLjIzdjIuMDZjMi44OS44NiA1IDMuNTQgNSA2Ljcxcy0yLjExIDUuODUtNSA2LjcxdjIuMDZjNC4wMS0uOTEgNy00LjQ5IDctOC43N3MtMi45OS03Ljg2LTctOC43N3oiLz4NCiAgICA8cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+DQo8L3N2Zz4NCg=='); 44 | } 45 | audio::-webkit-media-controls-play-button, 46 | video::-webkit-media-controls-play-button { 47 | background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJXaW5kb3dUZXh0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCiAgICA8cGF0aCBkPSJNOCA1djE0bDExLTd6Ii8+DQogICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPg0KPC9zdmc+'); 48 | } 49 | [data-theme='dark'] audio::-webkit-media-controls-play-button, 50 | [data-theme='dark'] video::-webkit-media-controls-play-button { 51 | background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJXaW5kb3ciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIGQ9Ik04IDV2MTRsMTEtN3oiLz4NCiAgICA8cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+DQo8L3N2Zz4NCg=='); 52 | } 53 | video { 54 | @apply rounded-lg shadow-xl; 55 | } 56 | video::-webkit-media-controls-panel { 57 | background: linear-gradient(to bottom, transparent, theme(backgroundColor.base-300)) repeat-x bottom left; 58 | } 59 | video::-webkit-media-controls-fullscreen-button { 60 | background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJXaW5kb3dUZXh0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCiAgICA8cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+DQogICAgPHBhdGggZD0iTTcgMTRINXY1aDV2LTJIN3YtM3ptLTItNGgyVjdoM1Y1SDV2NXptMTIgN2gtM3YyaDV2LTVoLTJ2M3pNMTQgNXYyaDN2M2gyVjVoLTV6Ii8+DQo8L3N2Zz4='); 61 | } 62 | [data-theme='dark'] video::-webkit-media-controls-fullscreen-button { 63 | background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJXaW5kb3ciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4NCiAgICA8cGF0aCBkPSJNNyAxNEg1djVoNXYtMkg3di0zem0tMi00aDJWN2gzVjVINXY1em0xMiA3aC0zdjJoNXYtNWgtMnYzek0xNCA1djJoM3YzaDJWNWgtNXoiLz4NCjwvc3ZnPg0K'); 64 | } 65 | } 66 | 67 | @layer components { 68 | .btn-outline { 69 | @apply border-2; 70 | } 71 | .btn-primary:not(.btn-outline, .btn-ghost) { 72 | @apply !text-base-100; 73 | } 74 | .btn-primary.btn-outline, 75 | .btn-primary.btn-ghost { 76 | @apply hover:!text-base-100; 77 | } 78 | .menu .active { 79 | @apply !bg-base-200 hover:!bg-base-200 !text-primary; 80 | } 81 | .menu li > *:active { 82 | @apply !bg-base-200 !text-primary; 83 | } 84 | .btn-ghost { 85 | @apply disabled:bg-transparent; 86 | } 87 | .checkbox { 88 | @apply border-2; 89 | } 90 | .select, 91 | .input, 92 | .textarea { 93 | @apply overflow-hidden outline bg-transparent outline-1 focus:outline-offset-0 border-0 !ring-0 !transition-all !duration-75 focus:outline-primary; 94 | } 95 | .toggle { 96 | @apply !bg-none !border-2; 97 | } 98 | .chat-bubble a { 99 | @apply !link !link-info; 100 | } 101 | .chat-bubble table { 102 | @apply !table !table-xs; 103 | } 104 | .chat-bubble pre { 105 | @apply !my-4 !whitespace-pre-wrap; 106 | } 107 | .chat-bubble ul { 108 | @apply !list-disc !list-inside; 109 | } 110 | .chat-bubble ol { 111 | @apply !list-decimal !list-inside; 112 | } 113 | } 114 | 115 | .apexcharts-tooltip { 116 | @apply !rounded-lg !text-xs; 117 | } 118 | 119 | .apexcharts-toolbar { 120 | @apply !max-w-none !right-1/2 translate-x-1/2 gap-2; 121 | } 122 | 123 | .apexcharts-text, 124 | .apexcharts-legend-text { 125 | @apply !font-medium; 126 | } 127 | 128 | .apexcharts-toolbar > * { 129 | @apply !m-0; 130 | } 131 | 132 | .apexcharts-toolbar-custom-icon { 133 | @apply !w-fit !h-fit; 134 | } 135 | 136 | .apexcharts-menu { 137 | @apply !rounded-lg !min-w-0 !bg-base-200 !border-neutral !border-2 !p-0 !mt-1 !right-1 overflow-hidden; 138 | } 139 | 140 | .apexcharts-menu > * { 141 | @apply !py-1 !px-2 hover:bg-base-300 font-medium transition-colors; 142 | } 143 | 144 | .apexcharts-series-scatter { 145 | cursor: pointer; 146 | } 147 | -------------------------------------------------------------------------------- /src/components/CheckBox.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 31 | -------------------------------------------------------------------------------- /src/components/DynamicForm.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 105 | -------------------------------------------------------------------------------- /src/components/ErrorBox.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 88 | -------------------------------------------------------------------------------- /src/components/InputBox.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 59 | -------------------------------------------------------------------------------- /src/components/MemorySelect.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 73 | -------------------------------------------------------------------------------- /src/components/MessageBox.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 137 | -------------------------------------------------------------------------------- /src/components/ModalBox.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 64 | -------------------------------------------------------------------------------- /src/components/NotificationStack.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 25 | 26 | 39 | -------------------------------------------------------------------------------- /src/components/Pagination.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 74 | -------------------------------------------------------------------------------- /src/components/SelectBox.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 84 | -------------------------------------------------------------------------------- /src/components/SidePanel.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 76 | -------------------------------------------------------------------------------- /src/components/UserDropdown.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 53 | -------------------------------------------------------------------------------- /src/composables/download.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param fileName the file name to set when downloaded 4 | * @returns The download method 5 | */ 6 | export function downloadContent(fileName = 'output') { 7 | const download = (content: string | object) => { 8 | const isObject = typeof content == 'object' 9 | 10 | const output = isObject ? JSON.stringify(content, undefined, 2) : content 11 | 12 | const element = document.createElement('a') 13 | element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(output)) 14 | element.setAttribute('download', `${fileName}.${isObject ? 'json' : 'txt'}`) 15 | element.style.display = 'none' 16 | document.body.appendChild(element) 17 | element.click() 18 | document.body.removeChild(element) 19 | } 20 | 21 | return { 22 | download, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/composables/perms.ts: -------------------------------------------------------------------------------- 1 | import { useAbility } from '@casl/vue' 2 | import type { AppAbility } from '@services/ApiService' 3 | 4 | export const usePerms = () => useAbility() 5 | -------------------------------------------------------------------------------- /src/composables/upload.ts: -------------------------------------------------------------------------------- 1 | import { usePlugins } from '@stores/usePlugins' 2 | import { useRabbitHole } from '@stores/useRabbitHole' 3 | import { AcceptedMemoryTypes, AcceptedPluginTypes } from 'ccat-api' 4 | 5 | const { installPlugin } = usePlugins() 6 | const { sendFile, sendMemory, sendWebsite, getAllowedMimetypes } = useRabbitHole() 7 | 8 | /** 9 | * A composable method to upload file to the Rabbit Hole based on file type 10 | * @param category The type of file who is going to ask for in the file dialog box 11 | */ 12 | export function uploadContent() { 13 | const upload = async (category: 'memory' | 'content' | 'web' | 'plugin', data?: File | string) => { 14 | const { open: openDialog, onChange: onFileUpload } = useFileDialog() 15 | 16 | const allowedMimetypes: string[] = [] 17 | 18 | const sendContent = category == 'plugin' ? installPlugin : category == 'memory' ? sendMemory : sendFile 19 | 20 | onFileUpload(files => { 21 | if (files == null) return 22 | for (const file of files) sendContent(file) 23 | }) 24 | 25 | if (category == 'memory') allowedMimetypes.push(...AcceptedMemoryTypes) 26 | else if (category == 'plugin') allowedMimetypes.push(...AcceptedPluginTypes) 27 | else if (category == 'content') { 28 | const mimetypes = (await getAllowedMimetypes()) ?? [] 29 | allowedMimetypes.push(...mimetypes) 30 | } 31 | 32 | if (category == 'web' && typeof data == 'string') sendWebsite(data) 33 | else if (data instanceof File && allowedMimetypes.includes(data.type)) sendContent(data) 34 | else if (category != 'web' && typeof data == 'string') sendContent(new File([new Blob([data])], category, { type: 'text/plain' })) 35 | else openDialog({ accept: allowedMimetypes.join(',') }) 36 | } 37 | 38 | return { 39 | upload, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/directives/vLock.ts: -------------------------------------------------------------------------------- 1 | import type { Directive } from 'vue' 2 | 3 | export const vLock: Directive = { 4 | mounted(el, binding) { 5 | if (binding.value) { 6 | el.style.pointerEvents = 'none' 7 | } else { 8 | el.style.pointerEvents = 'initial' 9 | } 10 | }, 11 | updated(el, binding) { 12 | if (binding.value) { 13 | el.style.pointerEvents = 'none' 14 | } else { 15 | el.style.pointerEvents = 'initial' 16 | } 17 | }, 18 | } 19 | export default vLock 20 | -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@saehrimnir/druidjs' 2 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import { abilitiesPlugin } from '@casl/vue' 4 | import { defineAbility } from '@casl/ability' 5 | import { defineRule } from 'vee-validate' 6 | import { all as AllRules } from '@vee-validate/rules' 7 | import vLock from '@/directives/vLock' 8 | import App from '@/App.vue' 9 | import router from '@/router' 10 | import 'unfonts.css' 11 | import 'animate.css' 12 | import '@assets/main.css' 13 | import { cloneDeep } from 'lodash' 14 | 15 | Object.keys(AllRules).forEach(rule => { 16 | defineRule(rule, AllRules[rule]) 17 | }) 18 | 19 | const app = createApp(App) 20 | 21 | const pinia = createPinia() 22 | 23 | // eslint-disable-next-line @typescript-eslint/no-empty-function 24 | const ability = defineAbility(() => {}) 25 | 26 | pinia.use(({ store }) => { 27 | const state = cloneDeep(store.$state) 28 | store.$reset = () => store.$patch(state) 29 | }) 30 | app.use(pinia) 31 | app.use(router) 32 | app.use(abilitiesPlugin, ability, { 33 | useGlobalProperties: true, 34 | }) 35 | 36 | app.directive('lock', vLock) 37 | 38 | app.mount('#app') 39 | -------------------------------------------------------------------------------- /src/models/JSONSchema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Map the individual settings record for each provider or embedder 3 | */ 4 | export type JSONSettings = Record 5 | 6 | /** 7 | * The structure of the generic JSON that arrives from the endpoints 8 | */ 9 | export interface JSONResponse { 10 | readonly status: 'error' | 'success' 11 | readonly message: string 12 | readonly data?: T 13 | } 14 | 15 | export interface SchemaField { 16 | label: string 17 | description: string 18 | as: string 19 | name: string 20 | type?: string 21 | rules?: string 22 | children?: { 23 | value: string 24 | text: string 25 | }[] 26 | default?: string | number | boolean 27 | } 28 | 29 | export const InputType = { 30 | number: 'number', 31 | integer: 'number', 32 | string: 'text', 33 | boolean: 'checkbox', 34 | select: 'select-one', 35 | } as const 36 | -------------------------------------------------------------------------------- /src/models/Message.ts: -------------------------------------------------------------------------------- 1 | import type { SocketResponse } from 'ccat-api' 2 | 3 | /** 4 | * The base interface for all message types. 5 | * It defines the structure of a basic message. 6 | * The purpose of this type is to be extended by other message types. 7 | */ 8 | export interface MessageBase { 9 | readonly id: string 10 | readonly when: Date 11 | text: string 12 | } 13 | 14 | /** 15 | * An interface for messages sent by the bot. 16 | */ 17 | export interface BotMessage extends MessageBase { 18 | readonly sender: 'bot' 19 | why: Required 20 | } 21 | 22 | /** 23 | * An interface for messages sent by the user. 24 | */ 25 | export interface UserMessage extends MessageBase { 26 | readonly sender: 'user' 27 | readonly file?: File 28 | } 29 | 30 | /** 31 | * The union type for all message types. 32 | */ 33 | export type Message = BotMessage | UserMessage 34 | -------------------------------------------------------------------------------- /src/models/Notification.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines the structure of a notification object. 3 | * A notification is a message that is displayed to the user. 4 | * It can be an error message, a success message, or a warning message. 5 | */ 6 | export interface Notification { 7 | readonly id: string | number 8 | readonly text: string 9 | readonly type?: 'info' | 'success' | 'error' 10 | hidden?: boolean 11 | } 12 | -------------------------------------------------------------------------------- /src/models/Plot.ts: -------------------------------------------------------------------------------- 1 | export interface MarkerData { 2 | id: string 3 | collection: string 4 | text: string 5 | when: string 6 | source: string 7 | score: number 8 | } 9 | 10 | export interface PlotData { 11 | name: string 12 | data: { 13 | x: number 14 | y: number 15 | }[] 16 | meta?: MarkerData[] 17 | } 18 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | 3 | const router = createRouter({ 4 | linkActiveClass: 'active', 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | path: '/', 9 | name: 'home', 10 | component: () => import('@views/HomeView.vue'), 11 | }, 12 | { 13 | path: '/settings', 14 | name: 'settings', 15 | component: () => import('@views/SettingsView.vue'), 16 | children: [ 17 | { 18 | path: '', 19 | name: 'providers', 20 | component: () => import('@views/ProvidersView.vue'), 21 | }, 22 | { 23 | path: '', 24 | name: 'embedders', 25 | component: () => import('@views/EmbeddersView.vue'), 26 | }, 27 | { 28 | path: '', 29 | name: 'auth', 30 | component: () => import('@views/AuthView.vue'), 31 | }, 32 | ], 33 | }, 34 | { 35 | path: '/memory', 36 | name: 'memory', 37 | component: () => import('@views/MemoryView.vue'), 38 | }, 39 | { 40 | path: '/plugins', 41 | name: 'plugins', 42 | component: () => import('@views/PluginsView.vue'), 43 | }, 44 | { 45 | path: '/:pathMatch(.*)*', 46 | name: 'error', 47 | component: () => import('@views/ErrorView.vue'), 48 | }, 49 | ], 50 | }) 51 | 52 | export default router 53 | -------------------------------------------------------------------------------- /src/services/ApiService.ts: -------------------------------------------------------------------------------- 1 | import type { JSONResponse } from '@models/JSONSchema' 2 | import LogService from '@services/LogService' 3 | import { CatClient, type CancelablePromise, type AuthPermission, type AuthResource } from 'ccat-api' 4 | import type { PureAbility as Ability } from '@casl/ability' 5 | 6 | const { DEV } = import.meta.env 7 | 8 | const getPort = () => { 9 | if (DEV) return 1865 10 | if (window.location.port == '443' || window.location.port == '80') return undefined 11 | return parseInt(window.location.port) 12 | } 13 | 14 | /** 15 | * API client to make requests to the endpoints and passing the JWT for authentication. 16 | * Start as null and is initialized by App.vue 17 | */ 18 | export let apiClient: CatClient | undefined = undefined 19 | 20 | /** 21 | * Function to instantiate the API client with the JWT token 22 | * @param credential The JWT token to pass to the API client 23 | */ 24 | export const instantiateApiClient = (credential: string | undefined) => { 25 | apiClient = new CatClient({ 26 | host: window.location.hostname, 27 | port: getPort(), 28 | secure: window.location.protocol === 'https:', 29 | credential: credential, 30 | timeout: 15000, 31 | instant: true, 32 | ws: { 33 | retries: 5, 34 | delay: 2000, 35 | onFailed: () => { 36 | console.error('Failed to connect WebSocket after several retries.') 37 | }, 38 | }, 39 | }) 40 | } 41 | 42 | /** 43 | * A function that wraps the promise request into a try/catch block 44 | * and check for errors to throw to the UI 45 | * @param request The axios promise function to await 46 | * @param success The message to return in case of success 47 | * @param error The message to return in case of error 48 | * @param log The log message/array of stuff to show 49 | * @returns A JSONResponse object containing status, message and optionally a data property 50 | */ 51 | export const tryRequest = async ( 52 | request: CancelablePromise | undefined, 53 | success: string, 54 | error: string, 55 | log: unknown[] | string = success, 56 | ) => { 57 | try { 58 | if (request == undefined) throw new Error('Failed to reach the endpoint') 59 | 60 | const result = (await request) as T 61 | 62 | if (typeof log === 'string') LogService.success(log) 63 | else LogService.success(...log) 64 | 65 | return { 66 | status: 'success', 67 | message: success, 68 | data: result, 69 | } as JSONResponse 70 | } catch (err) { 71 | const msg = getErrorMessage(err, error) 72 | 73 | LogService.error(msg) 74 | 75 | return { 76 | status: 'error', 77 | message: msg, 78 | } as JSONResponse 79 | } 80 | } 81 | 82 | export type AppAbility = Ability<[AuthPermission, AuthResource]> 83 | 84 | // TODO: Fix why this is not working 85 | declare module 'vue' { 86 | export interface ComponentCustomProperties { 87 | $ability: AppAbility 88 | $can(this: this, ...args: Parameters): boolean 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/services/AuthConfigService.ts: -------------------------------------------------------------------------------- 1 | import { apiClient, tryRequest } from '@services/ApiService' 2 | import type { JSONSettings } from '@models/JSONSchema' 3 | 4 | /* 5 | * This is a service that is used to get/set the auth handlers. 6 | */ 7 | const AuthService = Object.freeze({ 8 | getHandlers: async () => { 9 | return await tryRequest( 10 | apiClient?.api?.authHandler.getAuthHandlerSettings(), 11 | 'Getting all the available auth handlers', 12 | 'Unable to get the list of available auth handlers', 13 | ) 14 | }, 15 | setHandlerSettings: async (handlerName: string, settings: JSONSettings) => { 16 | return await tryRequest( 17 | apiClient?.api?.authHandler.upsertAuthenticatorSetting(handlerName, settings), 18 | 'Auth handler updated successfully', 19 | "Auth handler couldn't be updated", 20 | 'Sending the auth handler settings to the cat', 21 | ) 22 | }, 23 | }) 24 | 25 | export default AuthService 26 | -------------------------------------------------------------------------------- /src/services/EmbedderConfigService.ts: -------------------------------------------------------------------------------- 1 | import { apiClient, tryRequest } from '@services/ApiService' 2 | import type { JSONSettings } from '@models/JSONSchema' 3 | 4 | /* 5 | * This is a service that is used to get/set the language model embedders settings. 6 | */ 7 | const EmbedderService = Object.freeze({ 8 | getEmbedders: async () => { 9 | return await tryRequest( 10 | apiClient?.api?.embedder.getEmbeddersSettings(), 11 | 'Getting all the available embedders', 12 | 'Unable to get the list of available embedders', 13 | ) 14 | }, 15 | setEmbedderSettings: async (languageEmbedderName: string, settings: JSONSettings) => { 16 | return await tryRequest( 17 | apiClient?.api?.embedder.upsertEmbedderSetting(languageEmbedderName, settings), 18 | 'Language model embedder updated successfully', 19 | "Language model embedder couldn't be updated", 20 | 'Sending the embedder settings to the cat', 21 | ) 22 | }, 23 | }) 24 | 25 | export default EmbedderService 26 | -------------------------------------------------------------------------------- /src/services/LLMConfigService.ts: -------------------------------------------------------------------------------- 1 | import { apiClient, tryRequest } from '@services/ApiService' 2 | import type { JSONSettings } from '@models/JSONSchema' 3 | 4 | /* 5 | * This is a service that is used to get/set the language models providers settings. 6 | */ 7 | const LLMService = Object.freeze({ 8 | getProviders: async () => { 9 | return await tryRequest( 10 | apiClient?.api?.llm.getLlmsSettings(), 11 | 'Getting all the available providers', 12 | 'Unable to get the list of available providers', 13 | ) 14 | }, 15 | setProviderSettings: async (languageModelName: string, settings: JSONSettings) => { 16 | return await tryRequest( 17 | apiClient?.api?.llm.upsertLlmSetting(languageModelName, settings), 18 | 'Language model provider updated successfully', 19 | "Language model provider couldn't be updated", 20 | 'Sending the language model settings to the cat', 21 | ) 22 | }, 23 | }) 24 | 25 | export default LLMService 26 | -------------------------------------------------------------------------------- /src/services/LogService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a service that is used to log messages for debugging purposes. 3 | * It doesn't do anything in production mode. 4 | */ 5 | const LogService = Object.freeze({ 6 | error: (...args: unknown[]) => { 7 | if (import.meta.env.MODE === 'development') { 8 | console.error('🐱 Log:', ...args) 9 | } 10 | }, 11 | success: (...args: unknown[]) => { 12 | if (import.meta.env.MODE === 'development') { 13 | console.log('🐱 Log:', ...args) 14 | } 15 | }, 16 | }) 17 | 18 | export default LogService 19 | -------------------------------------------------------------------------------- /src/services/MemoryService.ts: -------------------------------------------------------------------------------- 1 | import { apiClient, tryRequest } from '@services/ApiService' 2 | 3 | /* 4 | * This is a service that is used to manage the memory of the Cheshire Cat. 5 | */ 6 | const MemoryService = Object.freeze({ 7 | getCollections: async () => { 8 | return await tryRequest( 9 | apiClient?.api?.memory.getCollections(), 10 | 'Getting all the available collections', 11 | 'Unable to fetch available collections', 12 | ) 13 | }, 14 | deleteMemoryPoint: async (collection: string, memory: string) => { 15 | return await tryRequest( 16 | apiClient?.api?.memory.deletePointInMemory(collection, memory), 17 | 'The selected memory point was wiped successfully', 18 | 'Unable to wipe the memory point', 19 | ) 20 | }, 21 | wipeAllCollections: async () => { 22 | return await tryRequest( 23 | apiClient?.api?.memory.wipeCollections(), 24 | 'All in-memory collections were wiped', 25 | 'Unable to wipe the in-memory collections', 26 | ) 27 | }, 28 | wipeCollection: async (collectionId: string) => { 29 | return await tryRequest( 30 | apiClient?.api?.memory.wipeSingleCollection(collectionId), 31 | `The ${collectionId} collection was wiped`, 32 | `Unable to wipe the ${collectionId} collection`, 33 | ) 34 | }, 35 | wipeConversation: async () => { 36 | return await tryRequest( 37 | apiClient?.api?.memory.wipeConversationHistory(), 38 | 'The current conversation was wiped', 39 | 'Unable to wipe the in-memory current conversation', 40 | ) 41 | }, 42 | getConversation: async () => { 43 | const result = await tryRequest( 44 | apiClient?.api?.memory.getConversationHistory(), 45 | 'Retrieved the conversation history', 46 | 'Unable to retrieve the in-memory conversation history', 47 | ) 48 | return result.data?.history ?? [] 49 | }, 50 | callMemory: async (query: string, memories = 10) => { 51 | const result = await tryRequest( 52 | apiClient?.api?.memory.recallMemoriesFromText(query, memories), 53 | `Recalling ${memories} memories with ${query} as query`, 54 | 'Unable to recall memory', 55 | `Recalling ${memories} memories from the cat with "${query}"`, 56 | ) 57 | return result.data 58 | }, 59 | }) 60 | 61 | export default MemoryService 62 | -------------------------------------------------------------------------------- /src/services/PluginService.ts: -------------------------------------------------------------------------------- 1 | import { apiClient, tryRequest } from '@services/ApiService' 2 | import type { JSONSettings } from '@models/JSONSchema' 3 | 4 | /* 5 | * This is a service that is used to get the list of plugins active on the Cheshire Cat. 6 | * It can also toggle them according to the user's choice. 7 | */ 8 | const PluginService = Object.freeze({ 9 | installFromRegistry: async (url: string) => { 10 | return await tryRequest( 11 | apiClient?.api?.plugins.installPluginFromRegistry({ url }), 12 | 'Installing plugin from registry', 13 | 'Unable to install the plugin from this url', 14 | ) 15 | }, 16 | getPlugins: async (query?: string) => { 17 | return await tryRequest( 18 | apiClient?.api?.plugins.listAvailablePlugins(query), 19 | query ? `Searching plugins with query: ${query}` : 'Getting all the available plugins', 20 | query ? `Unable to search plugins with query: ${query}` : 'Unable to fetch the plugins', 21 | ) 22 | }, 23 | getPluginsSettings: async () => { 24 | return await tryRequest(apiClient?.api?.plugins.getPluginsSettings(), `Getting plugins settings`, `Unable to get plugins settings`) 25 | }, 26 | getSinglePluginSettings: async (id: string) => { 27 | const result = await tryRequest( 28 | apiClient?.api?.plugins.getPluginSettings(id), 29 | `Getting plugin ${id} settings`, 30 | `Unable to get plugin ${id} settings`, 31 | ) 32 | return result.data 33 | }, 34 | togglePlugin: async (id: string) => { 35 | return await tryRequest(apiClient?.api?.plugins.togglePlugin(id), `Toggle plugin ${id}`, `Unable to toggle plugin ${id}`) 36 | }, 37 | updateSettings: async (id: string, settings: JSONSettings) => { 38 | return await tryRequest( 39 | apiClient?.api?.plugins.upsertPluginSettings(id, settings), 40 | `Updated plugin ${id} settings`, 41 | `Unable to update plugin ${id} settings`, 42 | ) 43 | }, 44 | deletePlugin: async (id: string) => { 45 | return await tryRequest(apiClient?.api?.plugins.deletePlugin(id), `Deleted plugin ${id}`, `Unable to delete plugin ${id}`) 46 | }, 47 | sendFile: async (file: File) => { 48 | return await tryRequest( 49 | apiClient?.api?.plugins.installPlugin({ file }), 50 | `Plugin ${file.name} installed successfully!`, 51 | `Unable to install the plugin ${file.name}`, 52 | ) 53 | }, 54 | }) 55 | 56 | export default PluginService 57 | -------------------------------------------------------------------------------- /src/services/RabbitHoleService.ts: -------------------------------------------------------------------------------- 1 | import { apiClient, tryRequest } from '@services/ApiService' 2 | 3 | /* 4 | * This service is used to send files down to the rabbit hole. 5 | * Meaning this service sends files to the backend. 6 | */ 7 | const RabbitHoleService = Object.freeze({ 8 | sendFile: async (file: File) => { 9 | return await tryRequest( 10 | apiClient?.api?.rabbitHole.uploadFile({ file }), 11 | `File ${file.name} successfully sent down the rabbit hole!`, 12 | 'Unable to send the file to the rabbit hole!', 13 | 'Sending a file to the rabbit hole', 14 | ) 15 | }, 16 | sendWeb: async (url: string) => { 17 | return await tryRequest( 18 | apiClient?.api?.rabbitHole.uploadUrl({ url }), 19 | 'Website successfully sent down the rabbit hole!', 20 | 'Unable to send the website to the rabbit hole!', 21 | 'Sending a website content to the rabbit hole', 22 | ) 23 | }, 24 | sendMemory: async (file: File) => { 25 | return await tryRequest( 26 | apiClient?.api?.rabbitHole.uploadMemory({ file }), 27 | 'Memories file successfully sent down the rabbit hole!', 28 | 'Unable to send the memories to the rabbit hole!', 29 | 'Sending a bunch of memories to the rabbit hole', 30 | ) 31 | }, 32 | getAllowedMimetypes: async () => { 33 | const result = await tryRequest( 34 | apiClient?.api?.rabbitHole.getAllowedMimetypes(), 35 | 'Memories file successfully sent down the rabbit hole!', 36 | 'Unable to send the memories to the rabbit hole!', 37 | 'Sending a bunch of memories to the rabbit hole', 38 | ) 39 | return result.data?.allowed 40 | }, 41 | }) 42 | 43 | export default RabbitHoleService 44 | -------------------------------------------------------------------------------- /src/services/UserService.ts: -------------------------------------------------------------------------------- 1 | import { apiClient, tryRequest } from '@services/ApiService' 2 | import type { UserCreate, UserCredentials, UserUpdate } from 'ccat-api' 3 | 4 | /* 5 | * This is a service that is used to manage the users of the Cheshire Cat. 6 | */ 7 | const UserService = Object.freeze({ 8 | getUsers: async () => { 9 | return await tryRequest(apiClient?.api?.users.readUsers(), 'Getting all the users', 'Unable to fetch users') 10 | }, 11 | getUser: async (id: string) => { 12 | return await tryRequest(apiClient?.api?.users.readUser(id), 'The selected user id does not exist', 'Unable to get the user') 13 | }, 14 | impersonateUser: async (credentials: UserCredentials) => { 15 | return await tryRequest( 16 | apiClient?.api?.userAuth.authToken(credentials), 17 | `Impersonating the user ${credentials.username}`, 18 | `Unable to impersonate the user ${credentials.username}`, 19 | ) 20 | }, 21 | getAvailablePermissions: async () => { 22 | return await tryRequest( 23 | apiClient?.api?.userAuth.getAvailablePermissions(), 24 | 'Getting all the available permissions', 25 | 'Unable to fetch the available permissions', 26 | ) 27 | }, 28 | createUser: async (user: UserCreate) => { 29 | return await tryRequest( 30 | apiClient?.api?.users.createUser(user), 31 | `User ${user.username} was created successfully`, 32 | `Unable to create user ${user.username}`, 33 | ) 34 | }, 35 | updateUser: async (id: string, body: UserUpdate) => { 36 | return await tryRequest( 37 | apiClient?.api?.users.updateUser(id, body), 38 | `The user ${id} was updated successfully`, 39 | `Unable to update user ${id}`, 40 | ) 41 | }, 42 | deleteUser: async (id: string) => { 43 | return await tryRequest(apiClient?.api?.users.deleteUser(id), `The user ${id} was deleted successfully`, `Unable to delete user ${id}`) 44 | }, 45 | }) 46 | 47 | export default UserService 48 | -------------------------------------------------------------------------------- /src/stores/types.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from '@models/Message' 2 | import type { Notification } from '@models/Notification' 3 | import type { JSONSettings } from '@models/JSONSchema' 4 | import type { CollectionsList, SettingsResponse, PluginsList, UserResponse } from 'ccat-api' 5 | import type { FileResponse, WebResponse } from 'ccat-api' 6 | 7 | /** 8 | * Defines a generic interface for defining the state of an asynchronous operation. 9 | */ 10 | export interface AsyncStateBase { 11 | loading: boolean 12 | error?: string 13 | } 14 | 15 | /** 16 | * Defines a generic interface for defining the state of an asynchronous operation that returns data. 17 | */ 18 | export interface AsyncState extends AsyncStateBase { 19 | data?: TData 20 | } 21 | 22 | export type RabbitHoleResponse = FileResponse | WebResponse | Record 23 | 24 | /** 25 | * Defines the structure of the 'fileUploader' state. 26 | * This state contains information about the last file that the user has sent to the bot as well as the response form the server. 27 | * It extends the AsyncState interface, which defines the structure of the state of an asynchronous operation. 28 | */ 29 | export type FileUploaderState = AsyncState 30 | 31 | /** 32 | * Defines the structure of the settings config state. 33 | */ 34 | export interface SettingsConfigState extends AsyncState { 35 | selected?: string 36 | settings: Record 37 | } 38 | 39 | /** 40 | * Defines the structure of the 'messages' state. 41 | * This state contains information about the messages sent by the user and the bot, 42 | * as well as a list of default messages that can be sent by the user. 43 | * It extends the AsyncStateBase interface, which defines the structure of the state of an asynchronous operation. 44 | */ 45 | export interface MessagesState extends AsyncStateBase { 46 | ready: boolean 47 | messages: Message[] 48 | generating?: string 49 | defaultMessages: string[] 50 | } 51 | 52 | /** 53 | * Defines the structure of the 'notifications' state. 54 | * This state contains information about the notifications sent to the user. 55 | */ 56 | export interface NotificationsState { 57 | history: Notification[] 58 | } 59 | 60 | /** 61 | * Defines the structure of the 'plugins' state. 62 | * This state contains information about the installed plugins. 63 | */ 64 | export type PluginsState = AsyncState> 65 | 66 | /** 67 | * Defines the structure of the 'collections' state. 68 | * This state contains information about the available collections. 69 | */ 70 | export type CollectionsState = AsyncState 71 | 72 | /** 73 | * Defines the structure of the 'users' state. 74 | * This state contains information about the available users. 75 | */ 76 | export type UsersListState = AsyncState> 77 | -------------------------------------------------------------------------------- /src/stores/useAuthConfig.ts: -------------------------------------------------------------------------------- 1 | import type { SettingsConfigState } from '@stores/types' 2 | import AuthConfigService from '@services/AuthConfigService' 3 | import { useNotifications } from '@stores/useNotifications' 4 | import type { JSONSettings } from '@models/JSONSchema' 5 | 6 | export const useAuthConfig = defineStore('auth', () => { 7 | const currentState = reactive({ 8 | loading: false, 9 | settings: {}, 10 | }) 11 | 12 | const { sendNotificationFromJSON } = useNotifications() 13 | 14 | const { 15 | state: handlers, 16 | isLoading, 17 | execute, 18 | } = useAsyncState(AuthConfigService.getHandlers, undefined, { 19 | resetOnExecute: false, 20 | }) 21 | 22 | const getAvailableHandlers = computed(() => { 23 | const settings = handlers.value?.data?.settings 24 | const schemas = settings ? settings.map(s => s.schema) : [] 25 | if (schemas.length === 0) currentState.error = 'No auth handlers found' 26 | return schemas as Record[] 27 | }) 28 | 29 | watchEffect(() => { 30 | currentState.loading = isLoading.value 31 | currentState.data = handlers.value?.data 32 | currentState.error = handlers.value?.status === 'error' ? handlers.value.message : undefined 33 | 34 | if (currentState.data) { 35 | currentState.selected = currentState.data.selected_configuration ?? currentState.data.settings[0].schema?.title 36 | currentState.settings = currentState.data.settings.reduce((acc, { name, value }) => ({ ...acc, [name]: value }), {}) 37 | } 38 | }) 39 | 40 | const getHandlerSchema = (selected = currentState.selected) => { 41 | if (!selected) return undefined 42 | return getAvailableHandlers.value.find(schema => schema?.title === selected) 43 | } 44 | 45 | const getHandlerSettings = (selected = currentState.selected) => { 46 | if (!selected) return {} satisfies JSONSettings 47 | return currentState.settings[selected] ?? ({} satisfies JSONSettings) 48 | } 49 | 50 | const setHandlerSettings = async (name: string, settings: JSONSettings) => { 51 | currentState.loading = true 52 | const result = await AuthConfigService.setHandlerSettings(name, settings) 53 | currentState.loading = false 54 | sendNotificationFromJSON(result) 55 | if (result.status != 'error') { 56 | currentState.selected = name 57 | currentState.settings[name] = settings 58 | } 59 | return result.status != 'error' 60 | } 61 | 62 | return { 63 | currentState, 64 | getAvailableHandlers, 65 | setHandlerSettings, 66 | getHandlerSchema, 67 | getHandlerSettings, 68 | refreshSettings: execute, 69 | } 70 | }) 71 | 72 | if (import.meta.hot) { 73 | import.meta.hot.accept(acceptHMRUpdate(useAuthConfig, import.meta.hot)) 74 | } 75 | -------------------------------------------------------------------------------- /src/stores/useEmbedderConfig.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSettings } from '@models/JSONSchema' 2 | import EmbedderConfigService from '@services/EmbedderConfigService' 3 | import { useNotifications } from '@stores/useNotifications' 4 | import type { SettingsConfigState } from '@stores/types' 5 | 6 | export const useEmbedderConfig = defineStore('embedder', () => { 7 | const currentState = reactive({ 8 | loading: false, 9 | settings: {}, 10 | }) 11 | 12 | const { sendNotificationFromJSON } = useNotifications() 13 | 14 | const { 15 | state: embedders, 16 | isLoading, 17 | execute, 18 | } = useAsyncState(EmbedderConfigService.getEmbedders, undefined, { 19 | resetOnExecute: false, 20 | }) 21 | 22 | const getAvailableEmbedders = computed(() => { 23 | const settings = embedders.value?.data?.settings 24 | const schemas = settings ? settings.map(s => s.schema) : [] 25 | if (schemas.length === 0) currentState.error = 'No embedders found' 26 | return schemas as Record[] 27 | }) 28 | 29 | watchEffect(() => { 30 | currentState.loading = isLoading.value 31 | currentState.data = embedders.value?.data 32 | currentState.error = embedders.value?.status === 'error' ? embedders.value.message : undefined 33 | 34 | if (currentState.data) { 35 | currentState.selected = currentState.data.selected_configuration ?? currentState.data.settings[0].schema?.title 36 | currentState.settings = currentState.data.settings.reduce((acc, { name, value }) => ({ ...acc, [name]: value }), {}) 37 | } 38 | }) 39 | 40 | const getEmbedderSchema = (selected = currentState.selected) => { 41 | if (!selected) return undefined 42 | return getAvailableEmbedders.value.find(schema => schema?.title === selected) 43 | } 44 | 45 | const getEmbedderSettings = (selected = currentState.selected) => { 46 | if (!selected) return {} satisfies JSONSettings 47 | return currentState.settings[selected] ?? ({} satisfies JSONSettings) 48 | } 49 | 50 | const setEmbedderSettings = async (name: string, settings: JSONSettings) => { 51 | currentState.loading = true 52 | const result = await EmbedderConfigService.setEmbedderSettings(name, settings) 53 | currentState.loading = false 54 | sendNotificationFromJSON(result) 55 | if (result.status != 'error') { 56 | currentState.selected = name 57 | currentState.settings[name] = settings 58 | } 59 | return result.status != 'error' 60 | } 61 | 62 | return { 63 | currentState, 64 | getAvailableEmbedders, 65 | getEmbedderSchema, 66 | getEmbedderSettings, 67 | setEmbedderSettings, 68 | refreshSettings: execute, 69 | } 70 | }) 71 | 72 | if (import.meta.hot) { 73 | import.meta.hot.accept(acceptHMRUpdate(useEmbedderConfig, import.meta.hot)) 74 | } 75 | -------------------------------------------------------------------------------- /src/stores/useLLMConfig.ts: -------------------------------------------------------------------------------- 1 | import type { SettingsConfigState } from '@stores/types' 2 | import LLMConfigService from '@services/LLMConfigService' 3 | import { useNotifications } from '@stores/useNotifications' 4 | import type { JSONSettings } from '@models/JSONSchema' 5 | 6 | export const useLLMConfig = defineStore('llm', () => { 7 | const currentState = reactive({ 8 | loading: false, 9 | settings: {}, 10 | }) 11 | 12 | const { sendNotificationFromJSON } = useNotifications() 13 | 14 | const { 15 | state: providers, 16 | isLoading, 17 | execute, 18 | } = useAsyncState(LLMConfigService.getProviders, undefined, { 19 | resetOnExecute: false, 20 | }) 21 | 22 | const getAvailableProviders = computed(() => { 23 | const settings = providers.value?.data?.settings 24 | const schemas = settings ? settings.map(s => s.schema) : [] 25 | if (schemas.length === 0) currentState.error = 'No large language models found' 26 | return schemas as Record[] 27 | }) 28 | 29 | watchEffect(() => { 30 | currentState.loading = isLoading.value 31 | currentState.data = providers.value?.data 32 | currentState.error = providers.value?.status === 'error' ? providers.value.message : undefined 33 | 34 | if (currentState.data) { 35 | currentState.selected = currentState.data.selected_configuration ?? currentState.data.settings[0].schema?.title 36 | currentState.settings = currentState.data.settings.reduce((acc, { name, value }) => ({ ...acc, [name]: value }), {}) 37 | } 38 | }) 39 | 40 | const getProviderSchema = (selected = currentState.selected) => { 41 | if (!selected) return undefined 42 | return getAvailableProviders.value.find(schema => schema?.title === selected) 43 | } 44 | 45 | const getProviderSettings = (selected = currentState.selected) => { 46 | if (!selected) return {} satisfies JSONSettings 47 | return currentState.settings[selected] ?? ({} satisfies JSONSettings) 48 | } 49 | 50 | const setProviderSettings = async (name: string, settings: JSONSettings) => { 51 | currentState.loading = true 52 | const result = await LLMConfigService.setProviderSettings(name, settings) 53 | currentState.loading = false 54 | sendNotificationFromJSON(result) 55 | if (result.status != 'error') { 56 | currentState.selected = name 57 | currentState.settings[name] = settings 58 | } 59 | return result.status != 'error' 60 | } 61 | 62 | return { 63 | currentState, 64 | getAvailableProviders, 65 | setProviderSettings, 66 | getProviderSchema, 67 | getProviderSettings, 68 | refreshSettings: execute, 69 | } 70 | }) 71 | 72 | if (import.meta.hot) { 73 | import.meta.hot.accept(acceptHMRUpdate(useLLMConfig, import.meta.hot)) 74 | } 75 | -------------------------------------------------------------------------------- /src/stores/useMainStore.ts: -------------------------------------------------------------------------------- 1 | import type { AuthPermission, AuthResource } from 'ccat-api' 2 | import type { JwtPayload } from 'jwt-decode' 3 | import { useJwt } from '@vueuse/integrations/useJwt' 4 | import { useCookies } from '@vueuse/integrations/useCookies' 5 | import { useAbility } from '@casl/vue' 6 | import { createMongoAbility } from '@casl/ability' 7 | 8 | import { instantiateApiClient } from '@services/ApiService' 9 | import LogService from '@services/LogService' 10 | 11 | interface Filter { 12 | [k: string]: { 13 | values: string[] 14 | current: string 15 | } 16 | } 17 | 18 | type AuthToken = JwtPayload & { 19 | username: string 20 | permissions: Record 21 | } 22 | 23 | /** 24 | * App wide store, containing info used in multiple views and components 25 | */ 26 | export const useMainStore = defineStore('main', () => { 27 | /** 28 | * Extract cookie from headers and JWT payload from it 29 | */ 30 | const cookies = useCookies(['ccat_user_token'], { doNotParse: true, autoUpdateDependencies: true }) 31 | const cookie = computed({ 32 | get: () => cookies.get('ccat_user_token'), 33 | set: value => cookies.set('ccat_user_token', value), 34 | }) 35 | const jwtPayload = computed(() => { 36 | if (!cookie.value) return null 37 | const { payload } = useJwt(cookie.value) 38 | return payload.value 39 | }) 40 | const perms = useAbility() 41 | 42 | tryOnBeforeMount(() => { 43 | if (jwtPayload.value) { 44 | instantiateApiClient(cookie.value) 45 | perms.update( 46 | createMongoAbility( 47 | jwtPayload.value === null 48 | ? [] 49 | : Object.entries(jwtPayload.value.permissions).map(([subject, action]) => ({ 50 | subject, 51 | action, 52 | })), 53 | ).rules, 54 | ) 55 | LogService.success(`Authenticated as ${jwtPayload.value.username}`) 56 | } 57 | }) 58 | 59 | /** 60 | * Dark theme 61 | */ 62 | const isDark = useDark({ 63 | storageKey: 'currentTheme', 64 | selector: 'html', 65 | disableTransition: false, 66 | attribute: 'data-theme', 67 | valueDark: 'dark', 68 | valueLight: 'light', 69 | }) 70 | const toggleDark = useToggle(isDark) 71 | 72 | /** 73 | * plugins filters 74 | */ 75 | const pluginsFilters = useLocalStorage('pluginsFilters', { 76 | presence: { 77 | current: 'both', 78 | values: ['both', 'installed', 'registry'], 79 | }, 80 | visibility: { 81 | current: 'both', 82 | values: ['both', 'enabled', 'disabled'], 83 | }, 84 | }) 85 | 86 | const logoutCurrentUser = () => { 87 | cookies.remove('ccat_user_token') 88 | // TODO: find different solution for this redirect, maybe having a LoginView and moving login page to frontend 89 | window.location.href = window.location.origin + '/auth/login' 90 | } 91 | 92 | return { 93 | isDark, 94 | pluginsFilters, 95 | toggleDark, 96 | cookie, 97 | jwtPayload, 98 | logoutCurrentUser, 99 | } 100 | }) 101 | 102 | if (import.meta.hot) { 103 | import.meta.hot.accept(acceptHMRUpdate(useMainStore, import.meta.hot)) 104 | } 105 | -------------------------------------------------------------------------------- /src/stores/useMemory.ts: -------------------------------------------------------------------------------- 1 | import MemoryService from '@services/MemoryService' 2 | import { useNotifications } from '@stores/useNotifications' 3 | import type { CollectionsState } from '@stores/types' 4 | import { remove } from 'lodash' 5 | 6 | export const useMemory = defineStore('memory', () => { 7 | const currentState = reactive({ 8 | loading: false, 9 | data: [], 10 | }) 11 | 12 | const { 13 | state: collections, 14 | isLoading, 15 | execute: fetchCollections, 16 | } = useAsyncState(MemoryService.getCollections, undefined, { resetOnExecute: false }) 17 | 18 | watchEffect(() => { 19 | currentState.loading = isLoading.value 20 | currentState.data = collections.value?.data?.collections 21 | currentState.error = collections.value?.status === 'error' ? collections.value.message : undefined 22 | }) 23 | 24 | const { sendNotificationFromJSON } = useNotifications() 25 | 26 | const wipeAllCollections = async () => { 27 | currentState.loading = true 28 | const result = await MemoryService.wipeAllCollections() 29 | currentState.loading = false 30 | if (result.status == 'success' && currentState.data) { 31 | remove(currentState.data, v => v.name != 'procedural') 32 | fetchCollections() 33 | } 34 | return sendNotificationFromJSON(result) 35 | } 36 | 37 | const wipeConversation = async () => { 38 | const result = await MemoryService.wipeConversation() 39 | return sendNotificationFromJSON(result) 40 | } 41 | 42 | const wipeCollection = async (collection: string) => { 43 | currentState.loading = true 44 | const result = await MemoryService.wipeCollection(collection) 45 | currentState.loading = false 46 | if (result.status == 'success' && currentState.data) { 47 | remove(currentState.data, v => v.name == collection) 48 | fetchCollections() 49 | } 50 | return sendNotificationFromJSON(result) 51 | } 52 | 53 | const callMemory = async (text: string, memories: number) => { 54 | const result = await MemoryService.callMemory(text, memories) 55 | return result 56 | } 57 | 58 | const deleteMemoryPoint = async (collection: string, memory: string) => { 59 | const result = await MemoryService.deleteMemoryPoint(collection, memory) 60 | return sendNotificationFromJSON(result) 61 | } 62 | 63 | return { 64 | currentState, 65 | fetchCollections, 66 | wipeAllCollections, 67 | wipeConversation, 68 | wipeCollection, 69 | callMemory, 70 | deleteMemoryPoint, 71 | } 72 | }) 73 | 74 | if (import.meta.hot) { 75 | import.meta.hot.accept(acceptHMRUpdate(useMemory, import.meta.hot)) 76 | } 77 | -------------------------------------------------------------------------------- /src/stores/useMessages.ts: -------------------------------------------------------------------------------- 1 | import type { MessagesState } from '@stores/types' 2 | import type { BotMessage, UserMessage } from '@models/Message' 3 | import { uniqueId } from 'lodash' 4 | import { useNotifications } from '@stores/useNotifications' 5 | import { apiClient } from '@services/ApiService' 6 | import MemoryService from '@services/MemoryService' 7 | import type { SocketResponse } from 'ccat-api' 8 | 9 | export const useMessages = defineStore('messages', () => { 10 | const currentState = reactive({ 11 | error: undefined, 12 | ready: false, 13 | loading: false, 14 | messages: [], 15 | defaultMessages: [ 16 | "What's up?", 17 | "Who's the Queen of Hearts?", 18 | 'Where is the white rabbit?', 19 | 'What is Python?', 20 | 'How do I write my own AI app?', 21 | 'Does pineapple belong on pizza?', 22 | 'What is the meaning of life?', 23 | 'What is the best programming language?', 24 | 'What is the best pizza topping?', 25 | 'What is a language model?', 26 | 'What is a neural network?', 27 | 'What is a chatbot?', 28 | 'What time is it?', 29 | 'Is AI capable of creating art?', 30 | 'What is the best way to learn AI?', 31 | 'Is it worth learning AI?', 32 | 'Who is the Cheshire Cat?', 33 | 'Is Alice in Wonderland a true story?', 34 | 'Who is the Mad Hatter?', 35 | 'How do I find my way to Wonderland?', 36 | 'Is Wonderland a real place?', 37 | ], 38 | }) 39 | 40 | const { state: history } = useAsyncState(MemoryService.getConversation, [], { resetOnExecute: false }) 41 | 42 | watchEffect(() => { 43 | history.value.forEach(({ who, message, why, when }) => { 44 | addMessage({ 45 | text: message, 46 | sender: who == 'AI' ? 'bot' : 'user', 47 | when: when ? new Date(when * 1000) : new Date(), 48 | why: why as SocketResponse['why'], 49 | }) 50 | }) 51 | currentState.loading = false 52 | }) 53 | 54 | const { showNotification } = useNotifications() 55 | 56 | watchEffect(() => { 57 | /** 58 | * Check if the websocket is open and set the ready state to true 59 | * (this is needed because apiClient initializes before the callbacks are added) 60 | */ 61 | if (apiClient?.socketState === WebSocket.OPEN) { 62 | currentState.ready = true 63 | } 64 | 65 | /** 66 | * Subscribes to the messages service on component mount 67 | * and dispatches the received messages to the store. 68 | * It also dispatches the error to the store if an error occurs. 69 | */ 70 | if (apiClient == undefined) { 71 | return 72 | } 73 | apiClient 74 | .onConnected(() => { 75 | currentState.ready = true 76 | currentState.error = undefined 77 | }) 78 | .onMessage(({ content, type, why }) => { 79 | switch (type) { 80 | case 'chat_token': { 81 | if (currentState.generating == undefined) { 82 | const id = addMessage({ 83 | text: content, 84 | sender: 'bot', 85 | when: new Date(), 86 | why, 87 | }) 88 | currentState.generating = id 89 | } else { 90 | const index = currentState.messages.findIndex(m => m.id === currentState.generating) 91 | if (index !== -1) currentState.messages[index].text += content 92 | } 93 | break 94 | } 95 | case 'chat': { 96 | if (currentState.generating) { 97 | const index = currentState.messages.findIndex(m => m.id === currentState.generating) 98 | currentState.messages[index].text = content 99 | ;(currentState.messages[index] as BotMessage).why = why 100 | currentState.generating = undefined 101 | } else { 102 | addMessage({ 103 | text: content, 104 | sender: 'bot', 105 | when: new Date(), 106 | why, 107 | }) 108 | } 109 | break 110 | } 111 | case 'notification': 112 | showNotification({ 113 | type: 'info', 114 | text: content, 115 | }) 116 | break 117 | default: 118 | break 119 | } 120 | }) 121 | .onError(error => { 122 | currentState.loading = currentState.ready = false 123 | currentState.error = error.description 124 | }) 125 | .onDisconnected(() => { 126 | currentState.ready = false 127 | }) 128 | }) 129 | 130 | tryOnUnmounted(() => { 131 | /** 132 | * Unsubscribes to the messages service on component unmount 133 | */ 134 | apiClient?.close() 135 | }) 136 | 137 | /** 138 | * Adds a message to the list of messages 139 | */ 140 | const addMessage = (message: Omit | Omit) => { 141 | currentState.error = undefined 142 | const id = uniqueId('m_') 143 | const msg = { 144 | id, 145 | ...message, 146 | } 147 | currentState.messages.push(msg) 148 | if (!(message as UserMessage)?.file) currentState.loading = msg.sender === 'user' 149 | return id 150 | } 151 | 152 | /** 153 | * Selects 5 random default messages from the messages slice. 154 | */ 155 | const selectRandomDefaultMessages = () => { 156 | const messages = [...currentState.defaultMessages] 157 | const shuffled = messages.sort(() => 0.5 - Math.random()) 158 | return shuffled.slice(0, 5) 159 | } 160 | 161 | /** 162 | * Sends a message to the messages service and dispatches it to the store 163 | */ 164 | const dispatchMessage = (message: string | File, store = true) => { 165 | if (typeof message === 'string') { 166 | apiClient?.send({ text: message }) 167 | if (store) 168 | addMessage({ 169 | text: message.trim(), 170 | when: new Date(), 171 | sender: 'user', 172 | }) 173 | } else { 174 | if (store) 175 | addMessage({ 176 | text: '', 177 | when: new Date(), 178 | sender: 'user', 179 | file: message, 180 | }) 181 | } 182 | } 183 | 184 | return { 185 | currentState, 186 | history, 187 | addMessage, 188 | selectRandomDefaultMessages, 189 | dispatchMessage, 190 | } 191 | }) 192 | 193 | if (import.meta.hot) { 194 | import.meta.hot.accept(acceptHMRUpdate(useMessages, import.meta.hot)) 195 | } 196 | -------------------------------------------------------------------------------- /src/stores/useNotifications.ts: -------------------------------------------------------------------------------- 1 | import type { JSONResponse } from '@models/JSONSchema' 2 | import type { Notification } from '@models/Notification' 3 | import type { NotificationsState } from '@stores/types' 4 | import { uniqueId } from 'lodash' 5 | 6 | export const useNotifications = defineStore('notifications', () => { 7 | const currentState = reactive({ 8 | history: [], 9 | }) 10 | 11 | const getNotifications = () => { 12 | return currentState.history.filter(notification => !notification.hidden) 13 | } 14 | 15 | const hideNotification = (id: Notification['id']) => { 16 | const notificationIndex = currentState.history.findIndex(notification => notification.id === id) 17 | if (notificationIndex >= 0 && notificationIndex < currentState.history.length) { 18 | currentState.history[notificationIndex].hidden = true 19 | } 20 | } 21 | 22 | const sendNotificationFromJSON = (result: JSONResponse) => { 23 | showNotification({ 24 | type: result.status, 25 | text: result.message, 26 | }) 27 | return result.status != 'error' 28 | } 29 | 30 | const showNotification = (notification: Omit, timeout = 3000) => { 31 | const newNotification = { 32 | id: uniqueId('n_'), 33 | ...notification, 34 | } 35 | currentState.history.push(newNotification) 36 | const to = setTimeout(() => { 37 | hideNotification(newNotification.id) 38 | clearTimeout(to) 39 | }, timeout) 40 | } 41 | 42 | return { 43 | currentState, 44 | hideNotification, 45 | getNotifications, 46 | showNotification, 47 | sendNotificationFromJSON, 48 | } 49 | }) 50 | 51 | if (import.meta.hot) { 52 | import.meta.hot.accept(acceptHMRUpdate(useNotifications, import.meta.hot)) 53 | } 54 | -------------------------------------------------------------------------------- /src/stores/usePlugins.ts: -------------------------------------------------------------------------------- 1 | import type { PluginsState } from '@stores/types' 2 | import type { Plugin } from 'ccat-api' 3 | import { useNotifications } from '@stores/useNotifications' 4 | import PluginService from '@services/PluginService' 5 | import type { JSONSettings } from '@models/JSONSchema' 6 | 7 | export const usePlugins = defineStore('plugins', () => { 8 | const currentState = reactive({ 9 | loading: false, 10 | data: { 11 | installed: [], 12 | registry: [], 13 | }, 14 | }) 15 | 16 | const { state: plugins, isLoading, execute: fetchPlugins } = useAsyncState(PluginService.getPlugins, undefined, { resetOnExecute: false }) 17 | const { state: settings, execute: fetchSettings } = useAsyncState(PluginService.getPluginsSettings, undefined, { resetOnExecute: false }) 18 | 19 | const { showNotification, sendNotificationFromJSON } = useNotifications() 20 | 21 | watchEffect(() => { 22 | currentState.loading = isLoading.value 23 | currentState.data = plugins.value?.data 24 | currentState.error = plugins.value?.status === 'error' ? plugins.value.message : undefined 25 | }) 26 | 27 | const getSchema = (id: Plugin['id']) => settings.value?.data?.settings.find(p => p.name === id)?.schema 28 | 29 | const getSettings = async (id: Plugin['id']) => (await PluginService.getSinglePluginSettings(id))?.value 30 | 31 | const togglePlugin = async (id: Plugin['id'], name: Plugin['name'], active: boolean) => { 32 | currentState.loading = true 33 | const res = await PluginService.togglePlugin(id) 34 | if (res.status == 'success') { 35 | showNotification({ 36 | text: `Plugin "${name}" is being switched ${active ? 'OFF' : 'ON'}!`, 37 | type: 'info', 38 | }) 39 | } else sendNotificationFromJSON(res) 40 | fetchSettings() 41 | fetchPlugins() 42 | currentState.loading = false 43 | return res.status != 'error' 44 | } 45 | 46 | const updateSettings = async (id: Plugin['id'], settings: JSONSettings) => { 47 | const res = await PluginService.updateSettings(id, settings) 48 | sendNotificationFromJSON(res) 49 | return res.status != 'error' 50 | } 51 | 52 | const removePlugin = async (id: Plugin['id']) => { 53 | if (currentState.data?.installed.find(p => p.id === id)) { 54 | const res = await PluginService.deletePlugin(id) 55 | sendNotificationFromJSON(res) 56 | fetchPlugins() 57 | return res.status != 'error' 58 | } 59 | return false 60 | } 61 | 62 | const installPlugin = async (file: File) => { 63 | currentState.loading = true 64 | const res = await PluginService.sendFile(file) 65 | await fetchSettings() 66 | await fetchPlugins() 67 | currentState.loading = false 68 | sendNotificationFromJSON(res) 69 | } 70 | 71 | const searchPlugin = async (query: string) => { 72 | currentState.loading = true 73 | const res = await PluginService.getPlugins(query) 74 | if (res.status == 'error') sendNotificationFromJSON(res) 75 | currentState.loading = false 76 | return res.data 77 | } 78 | 79 | const installRegistryPlugin = async (url: string) => { 80 | currentState.loading = true 81 | const res = await PluginService.installFromRegistry(url) 82 | if (res.status == 'error') sendNotificationFromJSON(res) 83 | fetchSettings() 84 | fetchPlugins() 85 | currentState.loading = false 86 | return res.data 87 | } 88 | 89 | return { 90 | currentState, 91 | togglePlugin, 92 | fetchPlugins, 93 | removePlugin, 94 | installPlugin, 95 | updateSettings, 96 | getSchema, 97 | getSettings, 98 | fetchSettings, 99 | searchPlugin, 100 | installRegistryPlugin, 101 | } 102 | }) 103 | 104 | if (import.meta.hot) { 105 | import.meta.hot.accept(acceptHMRUpdate(usePlugins, import.meta.hot)) 106 | } 107 | -------------------------------------------------------------------------------- /src/stores/useRabbitHole.ts: -------------------------------------------------------------------------------- 1 | import type { FileUploaderState } from '@stores/types' 2 | import { useNotifications } from '@stores/useNotifications' 3 | import { useMessages } from '@stores/useMessages' 4 | import RabbitHoleService from '@services/RabbitHoleService' 5 | 6 | export const useRabbitHole = defineStore('rabbitHole', () => { 7 | const currentState = reactive({ 8 | loading: false, 9 | }) 10 | 11 | const { sendNotificationFromJSON } = useNotifications() 12 | const { dispatchMessage } = useMessages() 13 | 14 | const sendFile = async (file: File) => { 15 | currentState.loading = true 16 | const res = await RabbitHoleService.sendFile(file) 17 | currentState.loading = false 18 | currentState.data = res.data 19 | if (res.data && res.status == 'success') dispatchMessage(file) 20 | sendNotificationFromJSON(res) 21 | } 22 | 23 | const sendWebsite = async (url: string) => { 24 | currentState.loading = true 25 | const res = await RabbitHoleService.sendWeb(url) 26 | currentState.loading = false 27 | currentState.data = res.data 28 | sendNotificationFromJSON(res) 29 | } 30 | 31 | const sendMemory = async (file: File) => { 32 | currentState.loading = true 33 | const res = await RabbitHoleService.sendMemory(file) 34 | currentState.loading = false 35 | currentState.data = res.data 36 | sendNotificationFromJSON(res) 37 | } 38 | 39 | const getAllowedMimetypes = async () => await RabbitHoleService.getAllowedMimetypes() 40 | 41 | return { 42 | currentState, 43 | sendFile, 44 | sendWebsite, 45 | sendMemory, 46 | getAllowedMimetypes, 47 | } 48 | }) 49 | 50 | if (import.meta.hot) { 51 | import.meta.hot.accept(acceptHMRUpdate(useRabbitHole, import.meta.hot)) 52 | } 53 | -------------------------------------------------------------------------------- /src/stores/useUsers.ts: -------------------------------------------------------------------------------- 1 | import UserService from '@services/UserService' 2 | import { useNotifications } from '@stores/useNotifications' 3 | import type { UsersListState } from '@stores/types' 4 | import { remove } from 'lodash' 5 | import type { UserCreate, UserUpdate } from 'ccat-api' 6 | import { useMainStore } from './useMainStore' 7 | 8 | export const useUsers = defineStore('users', () => { 9 | const currentState = reactive({ 10 | loading: false, 11 | data: [], 12 | }) 13 | 14 | const { state: users, isLoading, execute: fetchUsers } = useAsyncState(UserService.getUsers, undefined, { resetOnExecute: false }) 15 | const { state: perms } = useAsyncState(UserService.getAvailablePermissions, undefined, { resetOnExecute: false }) 16 | 17 | const availablePerms = computed(() => perms.value?.data ?? {}) 18 | 19 | watchEffect(() => { 20 | currentState.loading = isLoading.value 21 | currentState.data = users.value?.data 22 | currentState.error = users.value?.status === 'error' ? users.value.message : undefined 23 | }) 24 | 25 | const { sendNotificationFromJSON } = useNotifications() 26 | const { cookie } = storeToRefs(useMainStore()) 27 | 28 | const deleteUser = async (id: string) => { 29 | currentState.loading = true 30 | const result = await UserService.deleteUser(id) 31 | currentState.loading = false 32 | if (result.status == 'success' && currentState.data) { 33 | remove(currentState.data, v => v.id === id) 34 | fetchUsers() 35 | } 36 | return sendNotificationFromJSON(result) 37 | } 38 | 39 | const impersonateUser = async (username: string, password: string) => { 40 | currentState.loading = true 41 | const result = await UserService.impersonateUser({ username, password }) 42 | if (result.status == 'success' && result.data) cookie.value = result.data.access_token 43 | currentState.loading = false 44 | return sendNotificationFromJSON(result) 45 | } 46 | 47 | const createUser = async (body: UserCreate) => { 48 | const result = await UserService.createUser(body) 49 | currentState.data?.push(result.data!) 50 | return sendNotificationFromJSON(result) 51 | } 52 | 53 | const updateUser = async (id: string, body: UserUpdate) => { 54 | const result = await UserService.updateUser(id, body) 55 | const index = currentState.data!.findIndex(v => v.id === id) 56 | if (index !== -1) currentState.data![index] = result.data! 57 | return sendNotificationFromJSON(result) 58 | } 59 | 60 | return { 61 | availablePerms, 62 | currentState, 63 | fetchUsers, 64 | deleteUser, 65 | createUser, 66 | updateUser, 67 | impersonateUser, 68 | } 69 | }) 70 | 71 | if (import.meta.hot) { 72 | import.meta.hot.accept(acceptHMRUpdate(useUsers, import.meta.hot)) 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This module defines and export a collection of functions that are related to error management or error manipulation 3 | * commonly used throughout the application. 4 | */ 5 | 6 | /** 7 | * Returns the error message from an error or error-like object. 8 | * If the value is not an error or error-like object, the unknownError argument is returned. 9 | */ 10 | export const getErrorMessage = (error: unknown, unknownError = 'Unknown error') => { 11 | if (isApiError(error)) return error.body.detail.error 12 | if (isError(error) || isErrorLikeObject(error)) return error.message 13 | if (isString(error)) return error 14 | 15 | return unknownError 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/markdown.ts: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js' 2 | import { Remarkable } from 'remarkable' 3 | import { linkify } from 'remarkable/linkify' 4 | 5 | const markdown = new Remarkable({ 6 | html: true, 7 | breaks: true, 8 | xhtmlOut: true, 9 | typographer: true, 10 | highlight: (str, lang) => { 11 | if (lang && hljs.getLanguage(lang)) { 12 | try { 13 | return hljs.highlight(str, { language: lang }).value 14 | } catch (_) { 15 | console.log(_) 16 | } 17 | } 18 | try { 19 | return hljs.highlightAuto(str).value 20 | } catch (_) { 21 | console.log(_) 22 | } 23 | return '' // use external default escaping 24 | }, 25 | }).use(linkify) 26 | 27 | markdown.inline.ruler.enable(['sup', 'sub']) 28 | markdown.core.ruler.enable(['abbr']) 29 | markdown.block.ruler.enable(['footnote', 'deflist']) 30 | 31 | export default markdown 32 | -------------------------------------------------------------------------------- /src/utils/schema.ts: -------------------------------------------------------------------------------- 1 | import { InputType, type SchemaField } from '@models/JSONSchema' 2 | import { capitalize, entries } from 'lodash' 3 | 4 | const getEnumValues = (property: Record, definitions: Record): any[] | undefined => { 5 | if (property['$ref']) { 6 | const name = (property['$ref'] as string).split('/').at(-1) 7 | if (!name) return undefined 8 | return definitions[name].enum 9 | } else if (property.allOf && Array.isArray(property.allOf)) { 10 | return getEnumValues(property.allOf[0], definitions) 11 | } else return undefined 12 | } 13 | 14 | const getComponentType = (value: any, definitions: Record) => { 15 | if (value.extra && value.extra.type && typeof value.extra.type == 'string' && value.extra.type.toLowerCase() == 'textarea') 16 | return 'textarea' 17 | else if (getEnumValues(value, definitions)) return 'select' 18 | else return 'input' 19 | } 20 | 21 | export const generateVeeObject = (schema: Record | undefined) => { 22 | if (!schema) return [] 23 | return entries(schema.properties).map(([key, value]: [string, any]) => { 24 | return { 25 | name: key, 26 | as: getComponentType(value, schema['$defs']), 27 | label: value.title ?? capitalize(key), 28 | description: value.description, 29 | type: value.format ?? (value.type ? InputType[value.type as keyof typeof InputType] : undefined), 30 | rules: value.default == undefined ? 'required' : '', 31 | default: value.default, 32 | children: getEnumValues(value, schema['$defs'])?.map(v => ({ value: v, text: v })), 33 | } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/typeGuards.ts: -------------------------------------------------------------------------------- 1 | import { ApiError } from 'ccat-api' 2 | /** 3 | * This module defines and export a collection of Typescript type guards commonly used throughout the 4 | * application. 5 | */ 6 | 7 | /** 8 | * A type guard that takes a value of unknown type and returns a boolean indicating whether the value is of 9 | * type string 10 | * @param value 11 | */ 12 | export const isString = (value: unknown): value is string => !!(value && typeof value === 'string') 13 | 14 | /** 15 | * A type guard that takes a value of unknown type and returns a boolean indicating whether the value is of 16 | * type Error 17 | * @param value 18 | */ 19 | export const isError = (value: unknown): value is Error => value instanceof Error 20 | 21 | /** 22 | * A type guard that takes a value of unknown type and returns a boolean indicating whether the value is of 23 | * type ApiError 24 | * @param value 25 | */ 26 | export const isApiError = (value: unknown): value is ApiError => value instanceof ApiError 27 | 28 | /** 29 | * A type guard that takes a value of unknown type and returns a boolean indicating whether the value has an 30 | * error-like message property of type string. 31 | * @param value 32 | */ 33 | export const isErrorLikeObject = (value: unknown): value is { message: string } => { 34 | return !!(value && typeof value === 'object' && 'message' in value) 35 | } 36 | -------------------------------------------------------------------------------- /src/views/AuthView.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 65 | -------------------------------------------------------------------------------- /src/views/EmbeddersView.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 69 | -------------------------------------------------------------------------------- /src/views/ErrorView.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 187 | 188 |