├── .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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
11 | We're sorry but the app doesn't work properly without JavaScript enabled. Please enable it to continue.
12 |
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 |
4 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/components/DynamicForm.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
104 |
105 |
--------------------------------------------------------------------------------
/src/components/ErrorBox.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 | {{ text }}
19 |
20 |
21 |
22 | {{ error }}
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/components/Header.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
35 |
38 |
39 | Home
40 |
41 |
42 | Memory
43 |
44 |
45 | Plugins
46 |
47 |
48 |
49 | Settings
50 |
51 |
52 |
53 | Docs
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/src/components/InputBox.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
58 |
59 |
--------------------------------------------------------------------------------
/src/components/MemorySelect.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
23 |
24 |
25 |
26 | {{ col }}
27 |
28 |
29 |
30 |
31 |
35 |
38 |
39 | {{ Math.floor(item.score * 1000) / 1000 }}
40 |
41 |
42 |
43 | {{ item.metadata.docstring ? `${item.metadata.docstring}` : item.page_content }}
44 |
45 |
46 |
{{ item.metadata.source }} {{ item.metadata.name ? `(${item.metadata.name})` : '' }}
47 |
{{ new Date(item.metadata.when * 1000).toLocaleString() }}
48 |
49 |
52 |
53 | {{ selectedItem === item.id ? 'Hide Metadata' : 'View Metadata' }}
54 |
55 |
56 |
57 |
58 |
59 |
62 | {{ el }}
63 |
64 |
65 |
66 |
67 |
68 |
69 | No {{ selectedCollection }} memories were used.
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/src/components/MessageBox.vue:
--------------------------------------------------------------------------------
1 |
58 |
59 |
60 |
61 |
62 | {{ sender === 'bot' ? '😺' : '🙂' }}
63 |
64 |
68 |
69 |
70 |
Cheshire Cat is thinking...
71 |
75 |
76 |
82 |
83 |
84 |
85 | Your browser doesn't support HTML video. Here is a
86 | link to the video instead.
87 |
88 |
89 |
90 |
91 |
92 |
{{ file.name.substring(0, file.name.lastIndexOf('.')) }}
93 |
{{ fileTypeSize }}
94 |
95 |
96 |
97 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
Triggered Tool
117 |
{{ data[0][0] }}
118 |
119 |
120 |
Tool Input
121 |
{{ data[0][1] }}
122 |
123 |
124 |
125 |
126 | Tool Output
127 |
128 |
{{ data[1] }}
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/src/components/ModalBox.vue:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
34 |
35 |
43 |
44 |
45 |
46 |
47 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/components/NotificationStack.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
20 | {{ notification.text }}
21 |
22 |
23 |
24 |
25 |
26 |
39 |
--------------------------------------------------------------------------------
/src/components/Pagination.vue:
--------------------------------------------------------------------------------
1 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 1
52 |
53 |
54 | ...
55 |
56 | {{ currentPage }}
57 |
58 |
59 | ...
60 |
61 |
65 | {{ pageCount }}
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/src/components/SelectBox.vue:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
47 |
48 |
51 | {{ selected.label }}
52 |
53 |
54 |
61 |
64 |
70 |
76 | {{ element.label }}
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/src/components/SidePanel.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
32 |
40 |
41 |
42 |
43 |
44 |
45 |
53 |
54 |
55 |
56 |
57 | {{ title }}
58 |
59 |
60 | Close panel
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/src/components/UserDropdown.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
52 |
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 |
45 |
46 |
47 |
48 |
updateProperties(e.value)" />
53 |
54 |
55 |
56 |
57 |
58 |
{{ currentSchema?.description }}
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/views/EmbeddersView.vue:
--------------------------------------------------------------------------------
1 |
43 |
44 |
45 |
46 |
51 |
52 |
updateProperties(e.value)" />
57 |
58 |
59 |
60 |
61 |
62 |
{{ currentSchema?.description }}
63 |
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/views/ErrorView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 404
7 |
8 | Cat Not Found
9 |
10 | You’ve stumbled down the wrong rabbit hole!
11 |
12 |
13 | Go back to Home!
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/views/HomeView.vue:
--------------------------------------------------------------------------------
1 |
187 |
188 |
189 |
196 |
197 |
198 |
199 | Drop
200 | files
201 | to send to the Cheshire Cat, meow!
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
219 |
220 | {{ messagesState.error }}
221 |
222 |
223 |
😺
224 |
225 |
226 | Cheshire Cat is thinking...
227 |
228 |
229 |
230 |
231 |
236 | {{ msg }}
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 | `${p}${capitalize(c.sender)}: ${c.text}\n`, ''))">
252 |
253 |
254 |
255 | Export conversation
256 |
257 |
258 |
259 |
263 |
264 |
265 |
266 | Upload memories
267 |
268 |
269 |
270 |
274 |
275 |
276 |
277 | Upload url
278 |
279 |
280 |
281 |
285 |
286 |
287 |
288 | Upload file
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 | Clear conversation
297 |
298 |
299 |
300 |
301 |
302 |
310 |
311 |
315 |
316 |
317 |
318 |
319 |
325 |
326 |
327 |
328 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
Insert URL
339 |
Write down the URL you want the Cat to digest :
340 |
341 |
Send
342 |
343 |
344 |
345 |
346 |
347 |
--------------------------------------------------------------------------------
/src/views/MemoryView.vue:
--------------------------------------------------------------------------------
1 |
185 |
186 |
187 |
188 |
189 |
190 |
197 |
198 |
199 | K memories
200 |
201 |
207 |
208 |
209 |
214 |
215 |
216 |
217 |
218 |
Wipe collection
219 |
220 | Are you sure you want to wipe
221 |
222 | {{ selectCollection?.selected.label.toLowerCase() }}
223 |
224 | the collections?
225 |
226 |
227 | Are you sure you want to wipe the
228 |
229 | {{ selectCollection?.selected.label.toLowerCase() }}
230 |
231 | collection?
232 |
233 |
234 | No
235 | Yes
236 |
237 |
238 |
239 |
240 |
245 |
359 |
360 |
361 |
365 |
366 | Wipe
367 |
368 |
373 |
374 |
375 |
376 |
377 | {{ callOutput.embedder }}
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
{{ capitalize(key) }}
387 |
388 |
389 |
390 |
391 |
392 | {{ capitalize(data) }}
393 |
394 |
395 |
402 |
{{ data }}
403 |
404 |
405 |
406 |
411 |
412 | Delete memory point
413 |
414 |
415 |
416 |
417 |
418 |
423 |
--------------------------------------------------------------------------------
/src/views/PluginsView.vue:
--------------------------------------------------------------------------------
1 |
103 |
104 |
105 |
106 |
107 |
114 |
115 |
116 |
121 | {{ k }}:
122 | {{ v.current }}
123 |
124 |
125 |
126 |
127 |
Installed plugins: {{ pluginsState.data?.installed?.length ?? 0 }}
128 |
129 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 | {{ upperFirst(item.name)[0] }}
155 |
156 |
157 |
158 |
159 |
160 |
199 |
200 |
210 |
211 |
212 | Tested with Cat {{ getCompatibleVersionText(item) }}
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 | {{ tag.trim() }}
221 |
222 |
223 |
224 |
229 |
230 |
231 |
236 |
237 |
238 | {
246 | // TODO: Fix this workaround used to prevent checkbox switching when an error occurs
247 | const res = await togglePlugin(item.id, item.name, item.active ?? false)
248 | item.active = res ? item.active : false
249 | }
250 | " />
251 | On
252 | Off
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
No plugins found with this name.
261 |
262 |
263 |
264 |
265 |
🪝 Hooks
266 |
267 |
Priority {{ index }} :
268 |
269 |
- {{ name }}
270 |
271 |
272 |
273 |
🛠️ Tools
274 |
- {{ name }}
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
Remove plugin
285 |
286 | Are you sure you want to remove the
287 |
288 | {{ selectedPlugin?.name }}
289 |
290 | plugin?
291 |
292 |
293 | No
294 | Yes
295 |
296 |
297 |
298 |
299 |
300 |
301 |
--------------------------------------------------------------------------------
/src/views/ProvidersView.vue:
--------------------------------------------------------------------------------
1 |
43 |
44 |
45 |
46 |
47 |
48 |
updateProperties(e.value)" />
53 |
54 |
55 |
56 |
57 |
58 |
{{ currentSchema?.description }}
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/views/SettingsView.vue:
--------------------------------------------------------------------------------
1 |
74 |
75 |
76 |
77 |
78 |
79 | Cheshire Cat AI - Version
80 |
81 | {{ cat ? cat.version : 'unknown' }}
82 |
83 |
84 |
{{ cat.status }}
85 |
86 |
87 |
Large Language Model
88 |
Set and configure your favourite LLM from a list of supported providers
89 |
94 | Configure
95 |
96 |
97 |
98 |
Embedder
99 |
Set a language embedder to help the Cat remember conversations and documents
100 |
105 | Configure
106 |
107 |
108 |
109 |
Auth Handler
110 |
Set an auth handler to manage how your application authenticates with the Cat
111 |
116 | Configure
117 |
118 |
119 |
120 |
121 |
122 |
Users Management
123 |
{
128 | currentUser = {
129 | id: '',
130 | username: '',
131 | permissions: {},
132 | }
133 | editPanel?.togglePanel()
134 | }
135 | ">
136 |
137 | Add new user
138 |
139 |
140 |
141 |
142 |
143 |
144 | ID
145 | Name
146 | Permissions
147 | Actions
148 |
149 |
150 |
151 |
152 | {{ index + 1 }}
153 | {{ item.id }}
154 | {{ item.username }}
155 |
156 | {{
157 | Object.keys(item.permissions ?? {})
158 | .filter(r => item.permissions?.[r]?.length)
159 | .map(r => startCase(lowerCase(r)))
160 | .join(' - ')
161 | }}
162 |
163 |
164 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
Are you sure you want to delete this user?
204 |
205 | You are trying to delete
206 | {{ currentUser!.username }}
207 | . This action cannot be undone!
208 |
209 |
210 | No
211 | Yes
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 | Username
221 |
222 |
227 |
228 |
229 |
230 | Password
231 |
232 |
237 |
238 |
239 |
Permissions
240 |
241 |
242 | {{ startCase(lowerCase(r)) }}
243 |
244 |
245 |
(userPermissions[r] = l)">
246 |
247 |
250 | {{ value.join(', ') }}
251 |
252 |
253 |
260 |
262 |
263 |
269 | {{ perm }}
270 |
271 |
272 |
273 |
274 |
275 |
276 |
All
277 |
278 |
279 |
280 |
281 |
282 |
283 | Cancel
284 |
285 |
286 |
287 | {{ currentUser?.id ? 'Save' : 'Create' }}
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 |
3 | module.exports = {
4 | content: ['./index.html', './src/**/*.{html,js,ts,vue}'],
5 | theme: {
6 | extend: {
7 | transitionProperty: {
8 | height: 'height',
9 | spacing: 'margin, padding',
10 | width: 'width',
11 | fadetransform: 'opacity, transform',
12 | },
13 | maxWidth: {
14 | '1/2': '50%',
15 | },
16 | minWidth: {
17 | '1/2': '50%',
18 | },
19 | },
20 | },
21 | plugins: [require('@tailwindcss/forms'), require('daisyui')],
22 | darkMode: ['class', '[data-theme="dark"]'],
23 | daisyui: {
24 | logs: false,
25 | themes: [
26 | {
27 | light: {
28 | primary: '#0E8f7E',
29 | secondary: '#C6FFF7',
30 | accent: '#14CC9E',
31 | neutral: '#383938',
32 | 'base-100': '#F4F4F5',
33 | info: '#38BDF8',
34 | 'info-content': '#F4F4F5',
35 | success: '#2DC659',
36 | 'success-content': '#F4F4F5',
37 | warning: '#EAB308',
38 | 'warning-content': '#F4F4F5',
39 | error: '#EF4444',
40 | 'error-content': '#F4F4F5',
41 | },
42 | },
43 | {
44 | dark: {
45 | primary: '#14CC9E',
46 | secondary: '#C6FFF7',
47 | accent: '#0E8f7E',
48 | neutral: '#F4F4F5',
49 | 'base-100': '#383938',
50 | info: '#38BDF8',
51 | 'info-content': '#383938',
52 | success: '#2DC659',
53 | 'success-content': '#383938',
54 | warning: '#EAB308',
55 | 'warning-content': '#383938',
56 | error: '#EF4444',
57 | 'error-content': '#383938',
58 | },
59 | },
60 | ],
61 | },
62 | }
63 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.dom.json",
3 | "include": [
4 | "src/globals.d.ts",
5 | "src/**/*",
6 | "src/**/*.vue",
7 | ".eslintrc.json",
8 | "components.d.ts",
9 | "auto-imports.d.ts",
10 | "unit/**/*.test.ts",
11 | "e2e/**/*.spec.ts"
12 | ],
13 | "compilerOptions": {
14 | "target": "ESNext",
15 | "module": "ESNext",
16 | "lib": ["ESNext", "DOM"],
17 | "types": ["vitest/globals", "vite/client", "node", "jsdom"],
18 | "moduleResolution": "node",
19 | "resolveJsonModule": true,
20 | "isolatedModules": false,
21 | "noEmit": true,
22 | "allowJs": true,
23 | "removeComments": true,
24 | "importHelpers": true,
25 | "forceConsistentCasingInFileNames": true,
26 | "esModuleInterop": true,
27 | "strict": true,
28 | "allowSyntheticDefaultImports": true,
29 | "composite": false,
30 | "baseUrl": ".",
31 | "paths": {
32 | "@/*": ["src/*"],
33 | "@assets/*": ["src/assets/*"],
34 | "@components/*": ["src/components/*"],
35 | "@stores/*": ["src/stores/*"],
36 | "@views/*": ["src/views/*"],
37 | "@models/*": ["src/models/*"],
38 | "@services/*": ["src/services/*"],
39 | "@utils/*": ["src/utils/*"]
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@tsconfig/node18/tsconfig.json", "@vue/tsconfig/tsconfig.json"],
3 | "include": ["vite.config.*", "vitest.config.*", "playwright.config.*"],
4 | "compilerOptions": {
5 | "composite": true,
6 | "module": "ESNext",
7 | "types": ["node", "vitest"]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/unit/MessageBox.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import { mount } from '@vue/test-utils'
3 | import MessageBox from '@components/MessageBox.vue'
4 |
5 | describe('MessageBox', () => {
6 | it('renders properly for user', () => {
7 | const wrapper = mount(MessageBox, {
8 | props: {
9 | sender: 'user',
10 | text: 'Hello dear cat!',
11 | when: new Date(),
12 | },
13 | })
14 |
15 | expect(wrapper.find('.chat-image').text()).toContain('🙂')
16 |
17 | expect(wrapper.find('.chat-bubble p').html()).toContain('Hello dear cat!
')
18 |
19 | expect(wrapper.find('.chat-footer button').exists()).toBe(false)
20 |
21 | expect(wrapper.findComponent({ name: 'SidePanel' }).exists()).toBe(false)
22 | })
23 | it('renders properly for bot', () => {
24 | const wrapper = mount(MessageBox, {
25 | props: {
26 | sender: 'bot',
27 | text: 'Hello dear human!',
28 | when: new Date(),
29 | why: {
30 | input: 'Hello dear human!',
31 | intermediate_steps: [],
32 | memory: {},
33 | },
34 | },
35 | })
36 |
37 | expect(wrapper.find('.chat-image').text()).toContain('😺')
38 |
39 | expect(wrapper.find('.chat-bubble p').html()).toContain('Hello dear human!
')
40 |
41 | expect(wrapper.find('.chat-footer button').exists()).toBe(true)
42 |
43 | expect(wrapper.findComponent({ name: 'SidePanel' }).exists()).toBe(true)
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/unit/ModalBox.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import { mount } from '@vue/test-utils'
3 | import ModalBox from '@components/ModalBox.vue'
4 |
5 | describe('ModalBox', () => {
6 | it('renders properly', () => {
7 | const wrapper = mount(ModalBox, {
8 | slots: {
9 | default: 'My slot content',
10 | },
11 | })
12 |
13 | expect(wrapper.exists()).toBe(true)
14 |
15 | // TODO: is empty (wtf?)
16 | console.log('Slot:', wrapper.html())
17 |
18 | //expect(wrapper.getComponent({ name: 'DialogPanel' }).text()).toContain('All')
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/unit/NotificationStack.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import { createTestingPinia } from '@pinia/testing'
3 | import { mount } from '@vue/test-utils'
4 | import { useNotifications } from '@stores/useNotifications'
5 | import NotificationStack from '@components/NotificationStack.vue'
6 |
7 | describe('NotificationStack', () => {
8 | it('renders properly', () => {
9 | const wrapper = mount(NotificationStack, {
10 | global: {
11 | plugins: [
12 | createTestingPinia({
13 | stubActions: false,
14 | fakeApp: true,
15 | }),
16 | ],
17 | },
18 | })
19 |
20 | const { showNotification } = useNotifications()
21 |
22 | showNotification({
23 | text: 'Test',
24 | type: 'success',
25 | })
26 |
27 | expect(showNotification).toHaveBeenCalledTimes(1)
28 |
29 | // TODO: Doesn't show the v-for generated div
30 | console.log('text:', wrapper.html())
31 | //expect(wrapper.find(".alert[key='n_1']").text()).toContain('Test')
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/unit/SelectBox.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import { mount } from '@vue/test-utils'
3 | import SelectBox from '@components/SelectBox.vue'
4 |
5 | describe('SelectBox', () => {
6 | it('renders properly', () => {
7 | const wrapper = mount(SelectBox, {
8 | props: {
9 | list: [{ label: 'All', value: 'all' }],
10 | },
11 | })
12 |
13 | expect(wrapper.getComponent({ name: 'ListboxButton' }).text()).toContain('All')
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/vite.config.mts:
--------------------------------------------------------------------------------
1 | import tsconfigPaths from 'vite-tsconfig-paths'
2 | import { configDefaults, defineConfig } from 'vitest/config'
3 | import vue from '@vitejs/plugin-vue'
4 | import Icons from 'unplugin-icons/vite'
5 | import IconsResolver from 'unplugin-icons/resolver'
6 | import { HeadlessUiResolver, VueUseComponentsResolver } from 'unplugin-vue-components/resolvers'
7 | import Components from 'unplugin-vue-components/vite'
8 | import Unfonts from 'unplugin-fonts/vite'
9 | import AutoImport from 'unplugin-auto-import/vite'
10 |
11 | export default defineConfig({
12 | plugins: [
13 | vue(),
14 | AutoImport({
15 | dts: true,
16 | imports: ['vue', 'vue-router', '@vueuse/core', 'pinia', 'vitest'],
17 | eslintrc: {
18 | enabled: true,
19 | },
20 | dirs: ['./src/composables', './src/utils'],
21 | }),
22 | Components({
23 | dts: true,
24 | resolvers: [HeadlessUiResolver({ prefix: '' }), IconsResolver({ prefix: '' }), VueUseComponentsResolver()],
25 | }),
26 | Icons({ autoInstall: true }),
27 | Unfonts({
28 | custom: {
29 | families: [
30 | {
31 | name: 'Rubik',
32 | local: 'Rubik',
33 | src: './src/assets/fonts/*.ttf',
34 | },
35 | ],
36 | display: 'auto',
37 | preload: true,
38 | prefetch: false,
39 | },
40 | }),
41 | tsconfigPaths(),
42 | {
43 | name: 'configure-token',
44 | configureServer(server) {
45 | return () => {
46 | server.middlewares.use(async (_, res, next) => {
47 | const output = await (
48 | await fetch('http://localhost:1865/auth/token', {
49 | method: 'POST',
50 | headers: {
51 | 'Content-Type': 'application/json',
52 | },
53 | body: JSON.stringify({
54 | username: 'admin',
55 | password: 'admin',
56 | }),
57 | })
58 | ).json()
59 | res.setHeader('Set-Cookie', `ccat_user_token=${output.access_token}`)
60 | next()
61 | })
62 | }
63 | },
64 | },
65 | ],
66 | test: {
67 | environment: 'jsdom',
68 | globals: true,
69 | exclude: [...configDefaults.exclude, 'e2e/*'],
70 | },
71 | server: {
72 | port: 3000,
73 | open: false,
74 | host: true,
75 | },
76 | build: {
77 | outDir: 'dist',
78 | assetsDir: 'assets',
79 | cssCodeSplit: false,
80 | rollupOptions: {
81 | output: {
82 | minifyInternalExports: true,
83 | entryFileNames: 'assets/cat.js',
84 | assetFileNames: info => `assets/${info.name?.endsWith('css') ? 'cat' : '[name]'}[extname]`,
85 | chunkFileNames: 'chunk.js',
86 | manualChunks: () => 'chunk.js',
87 | generatedCode: {
88 | preset: 'es2015',
89 | constBindings: true,
90 | objectShorthand: true,
91 | },
92 | },
93 | },
94 | },
95 | })
96 |
--------------------------------------------------------------------------------