├── .env
├── .eslintignore
├── .eslintrc.cjs
├── .github
└── workflows
│ └── publish.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── images
├── chat-graph.png
├── dark-mode.png
├── folders.png
└── light-mode.png
├── package.json
├── pnpm-lock.yaml
├── postcss.config.cjs
├── src-tauri
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── build.rs
├── icons
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── 32x32.png
│ ├── Square107x107Logo.png
│ ├── Square142x142Logo.png
│ ├── Square150x150Logo.png
│ ├── Square284x284Logo.png
│ ├── Square30x30Logo.png
│ ├── Square310x310Logo.png
│ ├── Square44x44Logo.png
│ ├── Square71x71Logo.png
│ ├── Square89x89Logo.png
│ ├── StoreLogo.png
│ ├── icon.icns
│ ├── icon.ico
│ └── icon.png
├── src
│ └── main.rs
└── tauri.conf.json
├── src
├── app.d.ts
├── app.html
├── app.postcss
├── lib
│ ├── assets
│ │ └── xpress-ai-logo-white.png
│ ├── backend
│ │ ├── Anthropic.ts
│ │ ├── BackendFactory.ts
│ │ ├── OpenAI.ts
│ │ └── types.ts
│ ├── components
│ │ ├── CodeRenderer.svelte
│ │ ├── ConversationGraph.svelte
│ │ ├── EditableString.svelte
│ │ ├── Folder.svelte
│ │ ├── Menu.svelte
│ │ ├── MessageCard.svelte
│ │ ├── SubMenu.svelte
│ │ └── dialogs.ts
│ ├── stores
│ │ ├── schema.ts
│ │ ├── technologicStores.ts
│ │ └── utils.ts
│ └── translations
│ │ ├── index.ts
│ │ ├── lang.json
│ │ ├── translations.ts
│ │ └── util.ts
└── routes
│ ├── +layout.svelte
│ ├── +layout.ts
│ ├── +page.svelte
│ ├── [conversationId]
│ ├── +page.svelte
│ └── conversationBroker.ts
│ └── settings
│ ├── backends
│ ├── +page.svelte
│ ├── [backendName]
│ │ └── +page.svelte
│ └── new
│ │ └── +page.svelte
│ └── backup
│ └── +page.svelte
├── static
└── favicon.svg
├── svelte.config.js
├── tailwind.config.cjs
├── tsconfig.json
└── vite.config.ts
/.env:
--------------------------------------------------------------------------------
1 | #BACKEND=https://api.openai.com
2 | #PUBLIC_MODEL=gpt-3.5-turbo
3 |
4 | BACKEND=http://100.82.217.33:5000
5 | PUBLIC_MODEL=rwkv-raven-14b-eng-more
6 |
7 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
10 | # Ignore files for PNPM, NPM and YARN
11 | pnpm-lock.yaml
12 | package-lock.json
13 | yarn.lock
14 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: '@typescript-eslint/parser',
4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
5 | plugins: ['svelte3', '@typescript-eslint'],
6 | ignorePatterns: ['*.cjs'],
7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
8 | settings: {
9 | 'svelte3/typescript': () => require('typescript')
10 | },
11 | parserOptions: {
12 | sourceType: 'module',
13 | ecmaVersion: 2020
14 | },
15 | env: {
16 | browser: true,
17 | es2017: true,
18 | node: true
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish To Technologic Site
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 |
7 | jobs:
8 | build:
9 | runs-on: self-hosted
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - name: Install dependencies
15 | run: pnpm install --no-frozen-lockfile
16 |
17 | - name: Build site
18 | run: pnpm run build
19 |
20 | - name: Copying to technologic.xpress.ai
21 | run: rsync -r -v build/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/technologic.xpress.ai/
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env.*
7 | !.env.example
8 | vite.config.js.timestamp-*
9 | vite.config.ts.timestamp-*
10 | /.idea
11 | /src-tauri/target
12 | .env
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
10 | # Ignore files for PNPM, NPM and YARN
11 | pnpm-lock.yaml
12 | package-lock.json
13 | yarn.lock
14 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 100,
6 | "plugins": ["prettier-plugin-svelte"],
7 | "pluginSearchDirs": ["."],
8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Xpress AI
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Technologic - AI Chat
2 |
3 | 
4 | 
5 | 
6 |
7 | Technologic is a powerful, feature-rich AI Chatbot Client that is designed to work seamlessly with OpenAI's API or any
8 | compatible backend (such as your [Xpress AI](https://www.xpress.ai) agents). With a user-friendly interface and the ability to organize, modify, and manage your conversations,
9 | Technologic brings you a next-level chatting experience with your AI assistant.
10 |
11 | **[Demo: https://technologic.xpress.ai/](https://technologic.xpress.ai/)**
12 |
13 | 
14 |
15 | ## Features
16 |
17 | - **Secure Storage**: Your conversations are stored locally on your computer using your browser's IndexedDB storage.
18 | - **Backend Compatibility**: Works with any backend compatible with OpenAI's API.
19 | - **Bring Your Own API Key**: Easily configure your OpenAI API key or any other compatible backend (e.g. [xai-llm-server](https://github.com/XpressAI/xai-llm-server)).
20 | - **Organize Conversations**: Keep your conversations tidy by organizing them into folders.
21 | - **Message Modification**: Edit and modify messages, both sent and received, as needed.
22 | - **Custom Personality**: Support for "System Messages" to give your chatbot a unique personality (if supported by the backend).
23 | - **Fork Conversations**: Easily branch off into different topics without losing the context of previous conversations.
24 | - **Elaborate**: Use the "Go on" feature to prompt the bot to expand on its last message.
25 | - **Merge Messages**: Combine messages to avoid fragmentation or incomplete code blocks.
26 | - **View Raw Message**: Access the raw text of any message with the flip of a switch.
27 |
28 | ## Screenshots
29 |
30 | ### Light Mode
31 | 
32 |
33 | ### Dark Mode
34 | 
35 |
36 | ### Folders
37 | 
38 |
39 | ### Conversation Graph
40 | 
41 |
42 | ## Installation
43 |
44 | Clone the repository and navigate to the project directory:
45 |
46 | ```
47 | git clone https://github.com/XpressAI/technologic.git
48 | cd technologic
49 | ```
50 |
51 | Install the dependencies:
52 |
53 | ```
54 | pnpm install
55 | ```
56 |
57 | Start the development server:
58 | ```
59 | pnpm run dev
60 | ```
61 |
62 | ## Configuration
63 |
64 | The entire configuration happens through the browser. Just click the little cog icon on the bottom right corner of the
65 | sidebar.
66 |
67 |
68 | ## Developing
69 |
70 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
71 |
72 | ```bash
73 | pnpm run dev
74 |
75 | # or start the server and open the app in a new browser tab
76 | pnpm run dev -- --open
77 | ```
78 |
79 | ## Usage
80 |
81 | 1. Configure the backend by pasting your OpenAI API key or any other compatible API key into the backend configuration.
82 |
83 | 2. Start chatting with the AI by typing your message in the input field and hitting "Send".
84 |
85 | 3. Organize your conversations by creating folders and moving conversations into them.
86 |
87 | 4. Modify messages by clicking on the message you want to edit.
88 |
89 | 5. Use the "Go on" feature by hitting "Send" with an empty message to prompt the bot to elaborate on its last message.
90 |
91 | 6. Merge messages by selecting two or more fragmented messages and clicking on the "Merge" button.
92 |
93 | 7. View the raw text of a message by clicking the "Flip" button on the message.
94 |
95 | ## FAQs
96 |
97 | **Q: Is my data secure?**
98 |
99 | A: Yes, Technologic stores all your conversations locally on your computer using your browser's IndexedDB storage.
100 |
101 | **Q: Can I use Technologic with other backends apart from OpenAI's API?**
102 |
103 | A: Absolutely! Technologic is designed to work with any backend that is compatible with OpenAI's API. Just configure the backend and you're good to go.
104 |
105 | **Q: How can I give my AI chatbot a custom personality?**
106 |
107 | A: Technologic supports "System Messages" that allow you to give your chatbot a unique personality. This feature is available if your chosen backend supports it.
108 |
109 | ## Contributing
110 |
111 | We welcome contributions to Technologic!
112 |
113 | ## License
114 | Technologic is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
115 |
--------------------------------------------------------------------------------
/images/chat-graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/images/chat-graph.png
--------------------------------------------------------------------------------
/images/dark-mode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/images/dark-mode.png
--------------------------------------------------------------------------------
/images/folders.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/images/folders.png
--------------------------------------------------------------------------------
/images/light-mode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/images/light-mode.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "technologic",
3 | "version": "0.2.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "vite dev",
7 | "build": "vite build",
8 | "preview": "vite preview",
9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
11 | "lint": "prettier --plugin-search-dir . --check . && eslint .",
12 | "format": "prettier --plugin-search-dir . --write .",
13 | "native-dev": "tauri dev",
14 | "native-build": "tauri build"
15 | },
16 | "devDependencies": {
17 | "@floating-ui/dom": "^1.3.0",
18 | "@fontsource/quicksand": "^4.5.12",
19 | "@skeletonlabs/skeleton": "^1.7.1",
20 | "@sveltejs/adapter-auto": "^2.1.0",
21 | "@sveltejs/adapter-static": "^2.0.2",
22 | "@sveltejs/kit": "^1.20.2",
23 | "@tailwindcss/typography": "^0.5.9",
24 | "@tauri-apps/cli": "^1.4.0",
25 | "@typescript-eslint/eslint-plugin": "^5.59.11",
26 | "@typescript-eslint/parser": "^5.59.11",
27 | "autoprefixer": "^10.4.14",
28 | "eslint": "^8.42.0",
29 | "eslint-config-prettier": "^8.8.0",
30 | "eslint-plugin-svelte3": "^4.0.0",
31 | "highlight.js": "^11.8.0",
32 | "localforage": "^1.10.0",
33 | "postcss": "^8.4.24",
34 | "postcss-load-config": "^4.0.1",
35 | "prettier": "^2.8.8",
36 | "prettier-plugin-svelte": "^2.10.1",
37 | "process": "^0.11.10",
38 | "svelte": "^3.59.1",
39 | "svelte-check": "^3.4.3",
40 | "svelte-dnd-action": "^0.9.22",
41 | "svelte-legos": "^0.1.9",
42 | "svelte-markdown": "^0.2.3",
43 | "svelte-preprocess": "^4.10.7",
44 | "sveltekit-i18n": "^2.4.2",
45 | "tailwindcss": "^3.3.2",
46 | "tslib": "^2.5.3",
47 | "typescript": "^5.1.3",
48 | "vite": "^4.3.9"
49 | },
50 | "type": "module",
51 | "dependencies": {
52 | "@tabler/icons-svelte": "^2.22.0"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | const tailwindcss = require('tailwindcss');
2 | const autoprefixer = require('autoprefixer');
3 |
4 | const config = {
5 | plugins: [
6 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind,
7 | tailwindcss(),
8 | //But others, like autoprefixer, need to run after,
9 | autoprefixer
10 | ]
11 | };
12 |
13 | module.exports = config;
14 |
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "technologic"
3 | version = "0.1.0"
4 | description = "Technologic is a user-friendly AI Chatbot Client packed with features to enhance your chatting experience. Securely store conversations, modify messages, and easily organize them - all while enjoying compatibility with various backends, including OpenAI's API."
5 | authors = ["Paul Dubs", "Eduardo Gonzalez"]
6 | license = "MIT"
7 | repository = "https://github.com/XpressAI/technologic/"
8 | default-run = "technologic"
9 | edition = "2021"
10 | rust-version = "1.59"
11 |
12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13 |
14 | [build-dependencies]
15 | tauri-build = { version = "1.2.1", features = [] }
16 |
17 | [dependencies]
18 | serde_json = "1.0"
19 | serde = { version = "1.0", features = ["derive"] }
20 | tauri = { version = "1.2.5", features = [] }
21 |
22 | [features]
23 | # by default Tauri runs in production mode
24 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
25 | default = [ "custom-protocol" ]
26 | # this feature is used for production builds where `devPath` points to the filesystem
27 | # DO NOT remove this
28 | custom-protocol = [ "tauri/custom-protocol" ]
29 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(
2 | all(not(debug_assertions), target_os = "windows"),
3 | windows_subsystem = "windows"
4 | )]
5 |
6 | fn main() {
7 | tauri::Builder::default()
8 | .run(tauri::generate_context!())
9 | .expect("error while running tauri application");
10 | }
11 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json",
3 | "build": {
4 | "beforeBuildCommand": "pnpm run build",
5 | "beforeDevCommand": "pnpm run dev",
6 | "devPath": "http://localhost:5173",
7 | "distDir": "../build"
8 | },
9 | "package": {
10 | "productName": "technologic",
11 | "version": "0.1.0"
12 | },
13 | "tauri": {
14 | "allowlist": {
15 | "all": false
16 | },
17 | "bundle": {
18 | "active": true,
19 | "category": "DeveloperTool",
20 | "copyright": "",
21 | "deb": {
22 | "depends": []
23 | },
24 | "externalBin": [],
25 | "icon": [
26 | "icons/32x32.png",
27 | "icons/128x128.png",
28 | "icons/128x128@2x.png",
29 | "icons/icon.icns",
30 | "icons/icon.ico"
31 | ],
32 | "identifier": "ai.xpress.technologic",
33 | "longDescription": "",
34 | "macOS": {
35 | "entitlements": null,
36 | "exceptionDomain": "",
37 | "frameworks": [],
38 | "providerShortName": null,
39 | "signingIdentity": null
40 | },
41 | "resources": [],
42 | "shortDescription": "",
43 | "targets": "all",
44 | "windows": {
45 | "certificateThumbprint": null,
46 | "digestAlgorithm": "sha256",
47 | "timestampUrl": ""
48 | }
49 | },
50 | "security": {
51 | "csp": null
52 | },
53 | "updater": {
54 | "active": false
55 | },
56 | "windows": [
57 | {
58 | "fullscreen": false,
59 | "height": 600,
60 | "resizable": true,
61 | "title": "Technologic",
62 | "width": 800
63 | }
64 | ]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://kit.svelte.dev/docs/types#app
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface Platform {}
9 | }
10 | }
11 |
12 | export {};
13 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 | %sveltekit.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/app.postcss:
--------------------------------------------------------------------------------
1 | /* Write your global styles here, in PostCSS syntax */
2 |
3 | html,
4 | body {
5 | @apply h-full overflow-hidden;
6 | }
7 |
8 | body {
9 | @apply bg-surface-50-900-token;
10 | }
11 |
12 | body {
13 | background-image: radial-gradient(
14 | at 0% 0%,
15 | rgba(var(--color-secondary-500) / 0.33) 0px,
16 | transparent 50%
17 | ),
18 | radial-gradient(at 98% 1%, rgba(var(--color-error-500) / 0.33) 0px, transparent 50%);
19 | }
20 |
21 | :root {
22 | --theme-font-family-base: 'Quicksand', sans-serif;
23 | --theme-font-family-heading: 'Quicksand', sans-serif;
24 | /* ... */
25 | }
26 |
27 | .streaming-message {
28 | margin-left: 0.5em;
29 | width: 0.5em;
30 | height: 1em;
31 | display: inline-block;
32 | background-color: rgb(var(--color-surface-500));
33 | }
34 |
--------------------------------------------------------------------------------
/src/lib/assets/xpress-ai-logo-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XpressAI/technologic/HEAD/src/lib/assets/xpress-ai-logo-white.png
--------------------------------------------------------------------------------
/src/lib/backend/Anthropic.ts:
--------------------------------------------------------------------------------
1 | import type { BackendConfiguration } from '$lib/stores/schema';
2 | import type { Backend, Message } from '$lib/backend/types';
3 | import type { ConversationStore } from "$lib/stores/schema";
4 | import {get} from "svelte/store";
5 | import type { BackendFactory } from "./types";
6 |
7 | export const anthropicBackendFactory: BackendFactory = {
8 | createBackend
9 | };
10 |
11 | export function createBackend(configuration: BackendConfiguration, model: string): Backend {
12 | // according to docs: https://docs.anthropic.com/claude/reference/messages_post
13 | // temperature default is 1.0, I set it to 0.9 to make it slightly less random
14 | const temperature = 0.9;
15 |
16 | function request(payload: any) {
17 |
18 | let baseUrl = configuration.url;
19 | if(baseUrl.startsWith("http://0.0.0.0")){
20 | baseUrl = "";
21 | }
22 |
23 | const headers = new Headers();
24 | headers.append('Content-Type', 'application/json');
25 | headers.append('x-api-key',`${configuration.token}`);
26 | headers.append('anthropic-version', '2023-06-01');
27 |
28 | return fetch(`${baseUrl}/messages`, {
29 | method: 'POST',
30 | headers: headers,
31 | body: JSON.stringify(payload)
32 | });
33 | }
34 |
35 | async function sendMessage(history: Message[]): Promise {
36 | const payload = {
37 | model: model,
38 | max_tokens: 1024,
39 | messages: history.filter((h) => h.role !== 'system'),
40 | system: history.find((h) => h.role == 'system')?.content
41 | };
42 |
43 | const response = await request(payload);
44 |
45 | const out = await response.json();
46 | const content = out.content[0];
47 |
48 | return {
49 | role: out.role,
50 | content: content.text,
51 | };
52 | }
53 |
54 | async function sendMessageAndStream(
55 | history: Message[],
56 | onMessage: (message: string, done: boolean) => Promise
57 | ) {
58 | const payload = {
59 | model: model,
60 | max_tokens: 1024,
61 | temperature: temperature,
62 | messages: history.filter((h) => h.content.length > 0),
63 | stream: true
64 | };
65 |
66 | const response = await request(payload);
67 |
68 | const reader = response.body?.getReader();
69 | if (!reader) {
70 | throw new Error('Could not get reader from response body');
71 | }
72 |
73 | const decoder = new TextDecoder('utf-8');
74 | let out = '';
75 | while (true) {
76 | const { done, value } = await reader.read();
77 | if (done) {
78 | break;
79 | }
80 |
81 | const chunk = decoder.decode(value, { stream: true });
82 | out += chunk;
83 |
84 | let eventSeparatorIndex;
85 | while ((eventSeparatorIndex = out.indexOf('\n\n')) !== -1) {
86 | const data = out.slice(0, eventSeparatorIndex);
87 |
88 | if (data.match(/^data: \[DONE\]/)) {
89 | await onMessage('', true); // send end message.
90 | return;
91 | }
92 | const json = data.match(/data: (.*)/);
93 | if (json && json.length >= 1) {
94 |
95 | const event = JSON.parse(json[1]);
96 |
97 | out = out.slice(eventSeparatorIndex + 2);
98 |
99 | switch (event.type) {
100 | // case 'message_start':
101 | case 'content_block_start':
102 | await onMessage('', false); // send start message.
103 | break;
104 |
105 | // case 'content_block_stop':
106 | case 'message_stop':
107 | await onMessage('', true); // send end message.
108 | return;
109 |
110 | case 'content_block_delta':
111 | await onMessage(event.delta.text, false);
112 | break;
113 |
114 | case 'message_delta':
115 | case 'ping':
116 | // ignore
117 | }
118 | } else {
119 | console.warn('no json match foound.');
120 | await onMessage('', false); // send start message.
121 | return;
122 | }
123 | }
124 | }
125 | }
126 |
127 | async function renameConversationWithSummary(currentConversation: ConversationStore) {
128 | const summarizeMessage = 'Using the same language, in at most 3 words summarize the conversation between assistant and user.'
129 |
130 | const systemMessage: Message = {
131 | role: 'system',
132 | content: summarizeMessage
133 | };
134 |
135 | // system prompt alone might not be enough, specially not with other OpenAI-API-compatible models...
136 | // therefore we just add a "user" message that is the same as the system prompt, to "trigger" the model
137 | // to write an "assistant" message to the users request.
138 | const userMessage: Message = {
139 | role: 'user',
140 | content: summarizeMessage,
141 | };
142 |
143 | const history = get(currentConversation.history);
144 | const filteredHistory = history.filter((msg) => msg.role === 'user' || msg.role === 'assistant');
145 |
146 | const response = await sendMessage([...filteredHistory, systemMessage, userMessage]);
147 |
148 | const newTitle = response.content;
149 | if (newTitle) {
150 | await currentConversation.rename(newTitle);
151 | }
152 | }
153 |
154 |
155 | return {
156 | get api() {
157 | return configuration.api;
158 | },
159 | get name() {
160 | return configuration.name;
161 | },
162 | get model() {
163 | return model;
164 | },
165 | get temperature() {
166 | return temperature;
167 | },
168 |
169 | sendMessage,
170 | sendMessageAndStream,
171 | renameConversationWithSummary
172 | };
173 | }
174 |
--------------------------------------------------------------------------------
/src/lib/backend/BackendFactory.ts:
--------------------------------------------------------------------------------
1 | import type { Backend, BackendFactory } from "./types";
2 | import type { BackendConfiguration } from "$lib/stores/schema";
3 | import { openAIBackendFactory } from '$lib/backend/OpenAI';
4 | import { anthropicBackendFactory } from '$lib/backend/Anthropic';
5 |
6 | const backends: { [key: string]: BackendFactory } = {
7 | openai: openAIBackendFactory,
8 | anthropic: anthropicBackendFactory,
9 | openchat: openAIBackendFactory, // can use the same API as OpenAI
10 | }
11 |
12 | export function createBackend(config: BackendConfiguration, model: string): Backend {
13 | let backendFactory = backends[config.api];
14 |
15 | if (!backendFactory) {
16 | console.warn(`No matching backend factory found for api type '${config.api}', using OpenAI API backend instead`);
17 | backendFactory = backends.openai;
18 | }
19 |
20 | return backendFactory.createBackend(config, model);
21 | }
22 |
--------------------------------------------------------------------------------
/src/lib/backend/OpenAI.ts:
--------------------------------------------------------------------------------
1 | import type { BackendConfiguration } from '$lib/stores/schema';
2 | import type { Backend, Message } from '$lib/backend/types';
3 | import type { ConversationStore } from "$lib/stores/schema";
4 | import {get} from "svelte/store";
5 | import type { BackendFactory } from "./types";
6 |
7 | export const openAIBackendFactory: BackendFactory = {
8 | createBackend
9 | };
10 |
11 | export function createBackend(configuration: BackendConfiguration, model: string): Backend {
12 | const temperature = 0.7;
13 |
14 | function request(payload: any) {
15 | let baseUrl = configuration.url;
16 | if(baseUrl.startsWith("http://0.0.0.0")){
17 | baseUrl = "";
18 | }
19 | return fetch(`${baseUrl}/chat/completions`, {
20 | method: 'POST',
21 | body: JSON.stringify(payload),
22 | headers: {
23 | 'Content-Type': 'application/json',
24 | Authorization: `Bearer ${configuration.token}`
25 | }
26 | });
27 | }
28 |
29 | async function sendMessage(history: Message[]): Promise {
30 | if (model.startsWith('o1') || model.startsWith('o3')) {
31 | const payload = {
32 | model: model,
33 | reasoning_effort: 'medium',
34 | response_format: {
35 | type: 'text'
36 | },
37 | messages: history
38 | };
39 | const response = await request(payload);
40 |
41 | const out = await response.json();
42 | return out.choices[0].message;
43 | } else {
44 | const payload = {
45 | model: model,
46 | temperature: temperature,
47 | messages: history
48 | };
49 | const response = await request(payload);
50 |
51 | const out = await response.json();
52 | return out.choices[0].message;
53 | }
54 | }
55 |
56 | async function sendMessageAndStream(
57 | history: Message[],
58 | onMessage: (message: string, done: boolean) => Promise
59 | ) {
60 | let payload = null;
61 |
62 | if (model.startsWith('o1') || model.startsWith('o3')) {
63 | payload = {
64 | model: model,
65 | reasoning_effort: 'medium',
66 | response_format: {
67 | type: 'text'
68 | },
69 | messages: history,
70 | stream: true
71 | };
72 | } else {
73 | payload = {
74 | model: model,
75 | temperature: temperature,
76 | messages: history,
77 | stream: true
78 | };
79 | }
80 |
81 | const response = await request(payload);
82 |
83 | const reader = response.body?.getReader();
84 | if (!reader) {
85 | throw new Error('Could not get reader from response body');
86 | }
87 |
88 | const decoder = new TextDecoder('utf-8');
89 | let out = '';
90 | while (true) {
91 | const { done, value } = await reader.read();
92 | if (done) {
93 | break;
94 | }
95 | const chunk = decoder.decode(value, { stream: true });
96 | out += chunk;
97 |
98 | let eventSeparatorIndex;
99 | while ((eventSeparatorIndex = out.indexOf('\n\n')) !== -1) {
100 | const data = out.slice(0, eventSeparatorIndex);
101 |
102 | if (data.match(/^data: \[DONE\]/)) {
103 | await onMessage('', true); // send end message.
104 | return;
105 | }
106 |
107 | const event = JSON.parse(data.replace(/^data: /, ''));
108 |
109 | out = out.slice(eventSeparatorIndex + 2);
110 |
111 | if (event.choices[0].finish_reason === 'stop') {
112 | await onMessage('', true); // send end message.
113 | } else if (event.choices[0].role === 'assistant') {
114 | await onMessage('', false); // send start message.
115 | } else {
116 | await onMessage(event.choices[0].delta.content, false);
117 | }
118 | }
119 | }
120 | }
121 |
122 | function escapeHtml(unsafeText: string) {
123 | const div = document.createElement('div');
124 | div.textContent = unsafeText;
125 | return div.innerHTML;
126 | }
127 |
128 | function limitTitle(title: string) {
129 | const words = title.split(' ');
130 | if (words.length <= 7) {
131 | return title;
132 | }
133 | return words.slice(0, 7).join(' ') + '...';
134 | }
135 |
136 | async function renameConversationWithSummary(currentConversation: ConversationStore) {
137 | const summarizeMessage = 'Using the same language, in at most 7 words summarize the conversation between assistant and user before this message to serve as a title. Respond with ONLY the summary, no other commentary or acknowledgements of this instruction.'
138 |
139 | const systemMessage: Message = {
140 | role: 'system',
141 | content: summarizeMessage,
142 | };
143 |
144 | // system prompt alone might not be enough, specially not with other OpenAI-API-compatible models...
145 | // therefore we just add a "user" message that is the same as the system prompt, to "trigger" the model
146 | // to write an "assistant" message to the users request.
147 | const userMessage: Message = {
148 | role: 'user',
149 | content: summarizeMessage,
150 | };
151 |
152 | const history = get(currentConversation.history);
153 | const filteredHistory = history.filter((msg) => msg.role === 'user' || msg.role === 'assistant');
154 |
155 | const response = await sendMessage([...filteredHistory, systemMessage, userMessage]);
156 | const newTitle = limitTitle(escapeHtml(response.content.replace(/[\s\S]*?<\/think>/g, '')));
157 | if (newTitle) {
158 | await currentConversation.rename(newTitle);
159 | }
160 | }
161 |
162 |
163 | return {
164 | get api() {
165 | return configuration.api;
166 | },
167 | get name() {
168 | return configuration.name;
169 | },
170 | get model() {
171 | return model;
172 | },
173 | get temperature() {
174 | return temperature;
175 | },
176 |
177 | sendMessage,
178 | sendMessageAndStream,
179 | renameConversationWithSummary
180 | };
181 | }
182 |
--------------------------------------------------------------------------------
/src/lib/backend/types.ts:
--------------------------------------------------------------------------------
1 | import type { BackendConfiguration } from "$lib/stores/schema";
2 | import type { ConversationStore } from "$lib/stores/schema";
3 |
4 | export interface TextContent {
5 | type: 'text';
6 | text: string;
7 | }
8 |
9 | export interface ImageContent {
10 | type: 'image_url';
11 | image_url: {
12 | url: string; // url or f"data:image/jpeg;base64,{base64_image}"
13 | };
14 | }
15 |
16 | export type ContentItem = TextContent | ImageContent;
17 |
18 | export interface Message {
19 | role: string;
20 | content: string | ContentItem[];
21 | name?: string;
22 | }
23 |
24 | export interface Backend {
25 | readonly api: string;
26 | readonly name: string;
27 | readonly model: string;
28 | readonly temperature: number;
29 |
30 | sendMessage(history: Message[]): Promise;
31 | sendMessageAndStream(
32 | history: Message[],
33 | onMessage: (message: string, done: boolean) => Promise
34 | ): Promise;
35 | renameConversationWithSummary(currentConversation: ConversationStore): Promise;
36 | }
37 |
38 | export interface BackendFactory {
39 | createBackend(configuration: BackendConfiguration, model: string): Backend;
40 | }
41 |
--------------------------------------------------------------------------------
/src/lib/components/CodeRenderer.svelte:
--------------------------------------------------------------------------------
1 |
28 |
29 | {#if loadedLangs[actualLang]}
30 |
31 | {:else}
32 |
33 | {/if}
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/components/ConversationGraph.svelte:
--------------------------------------------------------------------------------
1 |
79 |
80 |
81 |
82 |
Chat Graph
83 |
84 |
163 |
164 |
165 |
169 |
170 |
171 | {#if hoveredMessageId}
172 |
173 | {:else}
174 | Hover over a node to preview its message.
175 | {/if}
176 |
177 |
178 |
179 |
--------------------------------------------------------------------------------
/src/lib/components/EditableString.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 | {#if isEditing}
15 |
16 |
17 |
26 |
27 | {:else}
28 |
29 |
{value}
30 |
35 |
36 | {/if}
37 |
--------------------------------------------------------------------------------
/src/lib/components/Folder.svelte:
--------------------------------------------------------------------------------
1 |
59 |
60 |
129 |
130 |
140 |
--------------------------------------------------------------------------------
/src/lib/components/Menu.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
27 |
--------------------------------------------------------------------------------
/src/lib/components/MessageCard.svelte:
--------------------------------------------------------------------------------
1 |
83 |
84 | {#if placeholder}
85 |
101 | {:else if isEditing}
102 |
106 |
107 |
108 |
109 |
117 |
118 |
119 |
129 |
139 |
149 |
150 |
151 |
152 | {:else}
153 |
157 |
158 | {#if Array.isArray(contentItems)}
159 | {#each contentItems as item}
160 | {#if item.type === 'text'}
161 | {#if isSource}
162 |
163 | {:else}
164 |
165 | {/if}
166 | {:else if item.type === 'image_url'}
167 |
168 | {/if}
169 | {/each}
170 | {:else}
171 | {#if isSource}
172 |
173 | {:else}
174 |
175 | {/if}
176 | {/if}
177 |
178 |
179 |
180 | {#if alternativesCount > 1}
181 |
182 |
191 | {selfPosition} / {alternativesCount}
192 |
201 |
202 | {/if}
203 | {#if msg.role === 'assistant'}
204 |
205 | {source}
206 |
207 | {:else if msg.role ==='system'}
208 |
209 | System Message
210 |
211 | {/if}
212 |
213 |
214 | {#if msg.role === 'assistant'}
215 |
225 | {/if}
226 |
236 |
246 |
256 |
266 |
276 |
277 |
278 |
279 | {/if}
280 |
281 |
294 |
--------------------------------------------------------------------------------
/src/lib/components/SubMenu.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
41 |
--------------------------------------------------------------------------------
/src/lib/components/dialogs.ts:
--------------------------------------------------------------------------------
1 | import { modalStore } from '@skeletonlabs/skeleton';
2 |
3 | export async function alert(message: string) {
4 | return new Promise((resolve, reject) => {
5 | modalStore.trigger({
6 | type: 'alert',
7 | body: message,
8 | response: (response: any) => {
9 | resolve(response);
10 | }
11 | })
12 | })
13 | }
14 |
15 | export async function confirm(message: string) {
16 | return new Promise((resolve, reject) => {
17 | modalStore.trigger({
18 | type: 'confirm',
19 | body: message,
20 | response: (response: boolean) => {
21 | resolve(response);
22 | }
23 | })
24 | })
25 | }
26 |
27 | export async function prompt(message: string, value?: string) {
28 | return new Promise((resolve, reject) => {
29 | modalStore.trigger({
30 | type: 'prompt',
31 | body: message,
32 | value: value,
33 | response: (response: string) => {
34 | resolve(response);
35 | }
36 | })
37 | })
38 | }
39 |
--------------------------------------------------------------------------------
/src/lib/stores/schema.ts:
--------------------------------------------------------------------------------
1 | import type { Message } from '$lib/backend/types';
2 | import type {Readable, Writable} from "svelte/store";
3 |
4 | export interface BackendConfiguration {
5 | api: string;
6 | name: string;
7 | url: string;
8 | token?: string;
9 | models: string[];
10 | defaultModel: string;
11 | }
12 |
13 | export interface CurrentBackend {
14 | name: string;
15 | model: string;
16 | }
17 |
18 | export interface Configuration {
19 | backends: BackendConfiguration[];
20 | backend: CurrentBackend;
21 | }
22 |
23 | export interface Folder {
24 | name: string;
25 | folders: Folder[];
26 | conversations: string[];
27 | }
28 |
29 | export interface FolderContent {
30 | type: 'folder' | 'conversation';
31 | id: string;
32 | path: string[];
33 | item: ResolvedFolder | ConversationStub;
34 | }
35 |
36 | export interface ResolvedFolder {
37 | id: string;
38 | name: string;
39 | path: string[];
40 | contents: FolderContent[];
41 | }
42 |
43 | export interface MessageSource {
44 | backend: string;
45 | model: string;
46 | }
47 |
48 | export interface MessageContainer {
49 | id: string;
50 | message: Message;
51 | isStreaming: boolean;
52 | source?: MessageSource;
53 | }
54 |
55 | interface MessageKV {
56 | [id: string]: MessageContainer;
57 | }
58 |
59 | export interface Link {
60 | from?: string;
61 | to: string;
62 | }
63 |
64 | export interface Conversation {
65 | id: string;
66 | title: string;
67 | messages: MessageKV;
68 | graph: Link[];
69 | lastMessageId?: string;
70 | isUntitled: boolean;
71 | }
72 |
73 | export interface MessageAlternative {
74 | self: string;
75 | messageIds: string[];
76 | }
77 |
78 | export interface MessageThread {
79 | messages: MessageAlternative[];
80 | }
81 |
82 | export interface ConversationDB {
83 | [id: string]: ConversationStub;
84 | }
85 |
86 | export interface ConversationStub {
87 | id: string;
88 | title: string;
89 | }
90 |
91 | export interface FolderStore extends Readable {
92 | raw: Writable;
93 | moveItemToFolder(item: FolderContent, target: ResolvedFolder): void;
94 | renameFolder(target: ResolvedFolder, newName: string): void;
95 | addFolder(parent: ResolvedFolder, name: string): void;
96 | removeFolder(target: ResolvedFolder): void
97 | }
98 |
99 | export interface ConversationStore extends Readable {
100 | messageThread: Readable;
101 | history: Readable;
102 | setLastMessageId(id: string): void;
103 | selectMessageThreadThrough(message: MessageContainer): Promise;
104 | rename(title: string): Promise;
105 | addMessage(msg: Message, source: MessageSource, isStreaming: boolean, parentMessageId?: string): Promise
106 | replaceMessage(orig: MessageContainer, newMsg: MessageContainer): Promise;
107 | deleteMessage(orig: MessageContainer): Promise;
108 | }
109 |
110 | export interface StubDB {
111 | [key: string]: R;
112 | }
113 |
114 | export interface ConversationsRepository {
115 | initialized: Writable;
116 | list: Writable>;
117 | get: (conversationId: string) => ConversationStore;
118 | create: () => Promise;
119 | duplicate: (conversationId: string) => Promise;
120 | delete: (conversationId: string) => Promise;
121 | events: EventTarget
122 | }
123 |
--------------------------------------------------------------------------------
/src/lib/stores/technologicStores.ts:
--------------------------------------------------------------------------------
1 | import { writable, get, derived } from 'svelte/store';
2 | import type { Readable, Writable } from 'svelte/store';
3 | import localforage from 'localforage';
4 | import type { Message } from '$lib/backend/types';
5 | import type {
6 | BackendConfiguration,
7 | Configuration,
8 | Conversation,
9 | ConversationsRepository, ConversationStore, ConversationStub,
10 | Folder,
11 | FolderContent, FolderStore,
12 | MessageAlternative,
13 | MessageContainer,
14 | MessageSource,
15 | MessageThread,
16 | ResolvedFolder, StubDB
17 | } from './schema';
18 | import { createItemStore } from '$lib/stores/utils';
19 | import { throwError } from 'svelte-preprocess/dist/modules/errors';
20 | import { createBackend } from "$lib/backend/BackendFactory";
21 |
22 | // these can be configured in the frontend (except for 'api')
23 | function defaultBackends(): BackendConfiguration[] {
24 | return [
25 | {
26 | api: 'openai', // readonly, must exist in BackendFactory#backends
27 | name: 'OpenAI',
28 | url: 'https://api.openai.com/v1',
29 | models: ['gpt-3.5-turbo'],
30 | defaultModel: 'gpt-3.5-turbo',
31 | token: 'YOUR_TOKEN_HERE',
32 | },
33 | {
34 | api: 'anthropic', // readonly, must exist in BackendFactory#backends
35 | name: 'Anthropic',
36 | url: 'https://api.anthropic.com/v1',
37 | models: ['claude-3-opus-20240229'],
38 | defaultModel: 'claude-3-opus-20240229',
39 | token: 'YOUR_API_KEY_HERE',
40 | },
41 | {
42 | api: 'openchat', // readonly, must exist in BackendFactory#backends
43 | name: 'OpenChat',
44 | url: 'http://localhost:18888/v1',
45 | models: ['openchat_3.5'],
46 | defaultModel: 'openchat_3.5',
47 | token: 'YOUR_TOKEN_HERE', // if its locally hosted, the token probably does not matter
48 | }
49 | ];
50 | }
51 |
52 | const configStore = createItemStore('technologic', 'config', 'config', {
53 | backends: defaultBackends(),
54 | backend: {
55 | name: defaultBackends()[0].name,
56 | model: defaultBackends()[0].defaultModel
57 | }
58 | });
59 |
60 | const currentBackend = derived(configStore, ($configStore) => {
61 | const backendConfig = $configStore.backends.find((it) => it.name === $configStore.backend.name);
62 | if (backendConfig === undefined) {
63 | throw new Error('No backend config found');
64 | }
65 | return createBackend(backendConfig, $configStore.backend.model);
66 | });
67 |
68 |
69 | function createDB(database: string, table: string, stubConverter: (item: T) => R){
70 | const db = localforage.createInstance({
71 | name: database,
72 | storeName: table,
73 | driver: localforage.INDEXEDDB
74 | });
75 |
76 | const initialized = writable(false);
77 | const ready = db.ready().then(() => initialized.set(true));
78 |
79 | const activeSubscriptions = new Map>();
80 |
81 | const stubList = writable>({});
82 | initialized.subscribe(async (isInitialized) => {
83 | if(!isInitialized) return {};
84 | const newValue: StubDB = {};
85 | await db.iterate((value: T, key) => {
86 | newValue[key] = stubConverter(value);
87 | });
88 | stubList.set(newValue);
89 | });
90 |
91 | return {
92 | initialized,
93 | list: stubList,
94 | getItem(key: string): Readable {
95 | if(activeSubscriptions.has(key)){
96 | return activeSubscriptions.get(key)!;
97 | }
98 |
99 | const itemStore: Writable = writable(null);
100 | activeSubscriptions.set(key, itemStore);
101 |
102 | ready.then(() => {
103 | db.getItem(key).then((value) => {
104 | itemStore.set(value);
105 | })
106 | });
107 |
108 | return itemStore;
109 | },
110 | async setItem(key: string, value: T){
111 | await ready;
112 | await db.setItem(key, value)
113 | stubList.update((it) => {
114 | return {
115 | ...it,
116 | [key]: stubConverter(value)
117 | }
118 | });
119 |
120 | if(activeSubscriptions.has(key)){
121 | activeSubscriptions.get(key)?.set(value);
122 | }
123 | },
124 | async nextKey(){
125 | const keys = (await db.keys()).map(it => parseInt(it, 10))
126 | return keys.length == 0 ? "1" : (Math.max(...keys) + 1).toString();
127 | },
128 | async removeItem(key: string){
129 | await ready;
130 | await db.removeItem(key);
131 | stubList.update((it) => {
132 | const newValue = {...it};
133 | delete newValue[key];
134 | return newValue;
135 | });
136 |
137 | if(activeSubscriptions.has(key)){
138 | activeSubscriptions.get(key)?.set(null);
139 | activeSubscriptions.delete(key);
140 | }
141 | }
142 | }
143 | }
144 |
145 | function createConversationsRepository(database: string, table: string): ConversationsRepository {
146 | const eventTarget = new EventTarget();
147 | const db = createDB(database, table,(it) => {
148 | return {
149 | id: it.id,
150 | title: it.title
151 | }
152 | });
153 |
154 | async function createConversation(){
155 | const conversation: Conversation = {
156 | id: await db.nextKey(),
157 | title: "Untitled",
158 | isUntitled: true,
159 | messages: {},
160 | graph: [],
161 | lastMessageId: undefined
162 | };
163 | await db.setItem(conversation.id, conversation);
164 | eventTarget.dispatchEvent(new CustomEvent("create", {detail: conversation.id}));
165 | return getConversation(conversation.id);
166 | }
167 |
168 | async function duplicateConversation(conversationId: string){
169 | const conversation = get(await db.getItem(conversationId))!;
170 | const newConversation = {
171 | ...conversation,
172 | id: await db.nextKey(),
173 | title: conversation.title + " (copy)",
174 | };
175 | await db.setItem(newConversation.id, newConversation);
176 | eventTarget.dispatchEvent(new CustomEvent("clone", {detail: {orig: conversationId, clone: newConversation.id}}));
177 | return getConversation(newConversation.id);
178 | }
179 |
180 | async function deleteConversation(conversationId: string) {
181 | await db.removeItem(conversationId);
182 | eventTarget.dispatchEvent(new CustomEvent("delete", {detail: conversationId}));
183 | }
184 | function getConversation(conversationId: string): ConversationStore {
185 | const _currentConversation = db.getItem(conversationId);
186 | const messageThread: Readable = derived(
187 | _currentConversation,
188 | ($currentConversation) => {
189 | const messages: MessageAlternative[] = [];
190 | if ($currentConversation !== null) {
191 | const lastMessageId = $currentConversation.lastMessageId;
192 |
193 | if (lastMessageId !== undefined) {
194 | let currentMessageId: string | undefined = lastMessageId;
195 | while (currentMessageId !== undefined) {
196 | const parentMessageId = $currentConversation.graph.find(
197 | (it) => it.to === currentMessageId
198 | )?.from;
199 | const siblingMessageIds = $currentConversation.graph.filter(
200 | (it) => it.from === parentMessageId
201 | );
202 | messages.unshift({
203 | self: currentMessageId,
204 | messageIds: siblingMessageIds?.map((it) => it.to) ?? []
205 | });
206 | currentMessageId = parentMessageId;
207 | }
208 | }
209 | }
210 |
211 | return {
212 | messages: messages
213 | } as MessageThread;
214 | }
215 | );
216 | const history = derived([messageThread, _currentConversation], ([$messageThread, $currentConversation]) =>
217 | $messageThread.messages.map(
218 | (msg) => $currentConversation?.messages[msg.self].message
219 | )
220 | );
221 |
222 | return {
223 | ..._currentConversation,
224 | messageThread,
225 | history,
226 | async addMessage(msg: Message, source: MessageSource, isStreaming: boolean, parentMessageId?: string) {
227 | const conversation = get(_currentConversation)!;
228 |
229 | const container: MessageContainer = {
230 | id: Object.keys(conversation.messages ?? {}).length.toString(),
231 | source: source,
232 | isStreaming: isStreaming,
233 | message: msg
234 | };
235 |
236 | const updatedConversation: Conversation = {
237 | ...conversation,
238 | messages: {
239 | ...conversation.messages,
240 | [container.id]: container
241 | },
242 | graph: [...conversation.graph, {from: parentMessageId, to: container.id}],
243 | lastMessageId: container.id
244 | }
245 | await db.setItem(conversation.id, updatedConversation);
246 | return container;
247 | },
248 | async replaceMessage(orig: MessageContainer, newMessage: MessageContainer) {
249 | const conversation = get(_currentConversation)!;
250 | const updatedConversation: Conversation = {
251 | ...conversation,
252 | messages: {
253 | ...conversation.messages,
254 | [orig.id]: newMessage
255 | }
256 | };
257 | await db.setItem(conversation.id, updatedConversation);
258 | return newMessage;
259 | },
260 | async deleteMessage(orig: MessageContainer) {
261 | const conversation = get(_currentConversation)!;
262 | // Remove orig from graph and replace its parent/child connections s.t. they are connected directly
263 | const parentLink = conversation.graph.find((it) => it.to === orig.id);
264 | const childLinks = conversation.graph.filter((it) => it.from === orig.id);
265 | const newGraph = conversation.graph.filter(
266 | (it) => it.from !== orig.id && it.to !== orig.id
267 | );
268 | const newChildLinks = childLinks.map((it) => ({from: parentLink?.from, to: it.to}));
269 | const newConversation = {
270 | ...conversation,
271 | messages: Object.fromEntries(
272 | Object.entries(conversation.messages).filter(([key, value]) => key !== orig.id)
273 | ),
274 | graph: [...newGraph, ...newChildLinks],
275 | lastMessageId:
276 | conversation.lastMessageId === orig.id
277 | ? parentLink?.from
278 | : conversation.lastMessageId
279 | };
280 | await db.setItem(newConversation.id, newConversation);
281 | },
282 | async rename(title: string) {
283 | const conversation = get(_currentConversation)!;
284 | const newConversation = {
285 | ...conversation,
286 | isUntitled: false,
287 | title: title
288 | };
289 | await db.setItem(newConversation.id, newConversation);
290 | },
291 | async selectMessageThreadThrough(message: MessageContainer) {
292 | const conversation = get(_currentConversation)!;
293 | if (!message) return;
294 |
295 | let target = conversation.graph.find((it) => it.from === message.id)?.to;
296 | if (!target) {
297 | target = message.id;
298 | } else {
299 | while (target != null) {
300 | const next = conversation.graph.find((it) => it.from === target)?.to;
301 | if (next == null) break;
302 | target = next;
303 | }
304 | }
305 |
306 | const newConversation = {
307 | ...conversation,
308 | lastMessageId: target
309 | };
310 | await db.setItem(newConversation.id, newConversation);
311 | },
312 | async setLastMessageId(id: string) {
313 | const conversation = get(_currentConversation)!;
314 | const newConversation = {
315 | ...conversation,
316 | lastMessageId: id
317 | };
318 | await db.setItem(newConversation.id, newConversation);
319 | }
320 | }
321 | }
322 |
323 | return {
324 | initialized: db.initialized,
325 | list: db.list,
326 | get: getConversation,
327 | create: createConversation,
328 | duplicate: duplicateConversation,
329 | delete: deleteConversation,
330 | events: eventTarget
331 | }
332 | }
333 |
334 | export function createFolderStore(database: string, table: string, conversationList: Readable>, conversationEvents: EventTarget ): FolderStore {
335 | const initialFolderValue = {
336 | name: '/',
337 | folders: [],
338 | conversations: []
339 | };
340 |
341 | const rawFolderStore = createItemStore(
342 | database,
343 | table,
344 | table,
345 | initialFolderValue
346 | );
347 |
348 | function findFolder(start: Folder, path: string[]): Folder {
349 | return path.reduce(
350 | (curDir, searchPath): Folder => {
351 | const res = curDir.folders.find((dir) => dir.name === searchPath);
352 | return res ?? throwError('Folder not found');
353 | },
354 | { folders: [start] } as Folder
355 | );
356 | }
357 |
358 | function findConversationFolder(start: Folder, conversationId: string): Folder | null {
359 | if (start.conversations.includes(conversationId)) {
360 | return start;
361 | }
362 | for (const folder of start.folders) {
363 | const result = findConversationFolder(folder, conversationId);
364 | if (result !== null) {
365 | return result;
366 | }
367 | }
368 | return null;
369 | }
370 |
371 | conversationEvents.addEventListener("clone", (e: CustomEvent<{ orig: string, clone: string }>) => {
372 | const {orig, clone}= e.detail;
373 | rawFolderStore.update((root) => {
374 | const folder = findConversationFolder(root, orig)!;
375 | folder.conversations = [...folder.conversations, clone];
376 | return root;
377 | });
378 | });
379 | conversationEvents.addEventListener("create", (e: CustomEvent) => {
380 | const id = e.detail;
381 | rawFolderStore.update((root) => {
382 | root.conversations = [...root.conversations, id];
383 | return root;
384 | });
385 | });
386 | conversationEvents.addEventListener("delete", (e: CustomEvent) => {
387 | const id = e.detail;
388 | rawFolderStore.update((root) => {
389 | const folder = findConversationFolder(root, id)!;
390 | folder.conversations = folder.conversations.filter((it) => it !== id);
391 | return root;
392 | });
393 | });
394 |
395 | const folderStore = derived(
396 | [rawFolderStore, conversationList],
397 | ([$rawFolderStore, $allConversations]) => {
398 | function resolveFolder(folder: Folder, parents: Folder[]): ResolvedFolder {
399 | const folderPath = [...parents, folder];
400 | const path = folderPath.map((it) => it.name);
401 | const id = path.join('/');
402 | const contents: FolderContent[] = [
403 | ...folder.folders.map((it) => {
404 | const sub = resolveFolder(it, folderPath);
405 | return {
406 | id: sub.id,
407 | path: path,
408 | type: 'folder',
409 | item: sub
410 | } as FolderContent;
411 | }),
412 | ...folder.conversations.filter(it => $allConversations[it]).map((it) => {
413 | return {
414 | id: it,
415 | type: 'conversation',
416 | path,
417 | item: $allConversations[it]
418 | } as FolderContent;
419 | })
420 | ];
421 |
422 | return {
423 | id,
424 | contents,
425 | path,
426 | name: folder.name
427 | };
428 | }
429 |
430 | if (Object.keys($allConversations).length == 0) {
431 | return resolveFolder(initialFolderValue, []);
432 | } else {
433 | return resolveFolder($rawFolderStore, []);
434 | }
435 | }
436 |
437 |
438 | );
439 |
440 | return {
441 | ...folderStore,
442 | raw: rawFolderStore,
443 | addFolder(parent: ResolvedFolder, name: string) {
444 | rawFolderStore.update((folder) => {
445 | console.log(parent);
446 | const parentFolder = findFolder(folder, parent.path);
447 | parentFolder.folders.push({
448 | name: name,
449 | folders: [],
450 | conversations: []
451 | });
452 | return folder;
453 | });
454 | },
455 | moveItemToFolder(item: FolderContent, target: ResolvedFolder) {
456 | rawFolderStore.update((root) => {
457 | const targetFolder = findFolder(root, target.path);
458 | const sourceFolder = findFolder(root, item.path);
459 |
460 | if (item.type === 'conversation') {
461 | sourceFolder.conversations = sourceFolder.conversations.filter((it) => it !== item.id);
462 | targetFolder.conversations = [...targetFolder.conversations, item.id];
463 | } else {
464 | const folder: Folder = findFolder(sourceFolder, [
465 | sourceFolder.name,
466 | (item.item as ResolvedFolder).name
467 | ]);
468 | sourceFolder.folders = sourceFolder.folders.filter(
469 | (it) => it.name !== (item.item as ResolvedFolder).name
470 | );
471 | targetFolder.folders = [...targetFolder.folders, folder];
472 | }
473 |
474 | return root;
475 | });
476 | },
477 | removeFolder(target: ResolvedFolder) {
478 | rawFolderStore.update((root) => {
479 | const sourceFolder = findFolder(root, target.path.slice(0, -1));
480 | sourceFolder.folders = sourceFolder.folders.filter((it) => it.name !== target.name);
481 | return root;
482 | });
483 | },
484 | renameFolder(target: ResolvedFolder, newName: string) {
485 | rawFolderStore.update((root) => {
486 | const sourceFolder = findFolder(root, target.path);
487 | sourceFolder.name = newName;
488 | return root;
489 | });
490 | }
491 | }
492 | }
493 |
494 | const conversationStore = createConversationsRepository('technologic', 'conversations');
495 | const folderStore = createFolderStore('technologic', 'folders', conversationStore.list, conversationStore.events);
496 |
497 | async function dumpDatabase(){
498 | const conversations = localforage.createInstance({
499 | name: 'technologic',
500 | storeName: 'conversations',
501 | driver: localforage.INDEXEDDB
502 | });
503 |
504 | const dump = {
505 | folders: get(folderStore.raw),
506 | conversations: {},
507 | }
508 |
509 | await conversations.iterate((value, key) => {
510 | dump.conversations[key] = value;
511 | })
512 |
513 | return dump;
514 | }
515 |
516 | async function loadDatabase(dump){
517 | const conversations = localforage.createInstance({
518 | name: 'technologic',
519 | storeName: 'conversations',
520 | driver: localforage.INDEXEDDB
521 | });
522 |
523 | await conversations.clear();
524 | await folderStore.raw.set(dump.folders);
525 | for (const key of Object.keys(dump.conversations)) {
526 | await conversations.setItem(key, dump.conversations[key]);
527 | }
528 | }
529 |
530 | export {
531 | dumpDatabase,
532 | loadDatabase,
533 | configStore,
534 | currentBackend,
535 | conversationStore,
536 | folderStore
537 | };
538 |
--------------------------------------------------------------------------------
/src/lib/stores/utils.ts:
--------------------------------------------------------------------------------
1 | import { writable, get } from 'svelte/store';
2 | import type { Writable } from 'svelte/store';
3 | import localforage from 'localforage';
4 | import { browser } from '$app/environment';
5 |
6 | export function createItemStore(
7 | database: string,
8 | table: string,
9 | itemKey: string,
10 | initialValue: Type
11 | ): Writable {
12 | const store = writable(initialValue);
13 | if (browser) {
14 | const db = localforage.createInstance({
15 | name: database,
16 | storeName: table,
17 | driver: localforage.INDEXEDDB
18 | });
19 |
20 | const ready = db.ready().then(async () => {
21 | const item: Type | null = await db.getItem(itemKey);
22 | if (item !== null) {
23 | store.set(item);
24 | } else {
25 | store.set(initialValue);
26 | }
27 | });
28 |
29 | return {
30 | subscribe: store.subscribe,
31 | set: async (value: Type) => {
32 | await ready;
33 |
34 | await db.setItem(itemKey, value);
35 | store.set(value);
36 | },
37 | update: async (updater: (currentValue: Type) => Type) => {
38 | await ready;
39 |
40 | const currentValue = get(store);
41 | const newValue = updater(currentValue);
42 | await db.setItem(itemKey, newValue);
43 | store.set(newValue);
44 | }
45 | };
46 | }
47 |
48 | return store;
49 | }
50 |
--------------------------------------------------------------------------------
/src/lib/translations/index.ts:
--------------------------------------------------------------------------------
1 | import i18n, { type Config } from 'sveltekit-i18n';
2 | import { Locale } from './util';
3 | import translations from './translations';
4 |
5 | /** Setup up the initial configuration. Pass in en as the default locale. */
6 | const config: Config = {
7 | initLocale: Locale.en,
8 | translations,
9 | };
10 |
11 | export const { t, l, locales, locale, loadTranslations } = new i18n(config);
12 |
13 | /* Working with translations
14 | *
15 | * This project uses the sveltekit-i18n library to support different languages.
16 | * To add a language, you can start by adding the translations for that language
17 | * in the `translations.ts` file. This file contains a JavaScript object with the
18 | * locale (language code) as the root property, and the translations as properties
19 | * on the object. These translations can later be accessed accordingly.
20 | *
21 | * `src/translations/lang.json` acts as the menu of languages available. Once you've
22 | * completed the necessary translations, you can add the language code here, which
23 | * will make it selectable in the app settings menu.
24 | *
25 | * The fallback language is set to English (en) by default. Once the user selects
26 | * a language, the language is saved to local storage and is then used as the default
27 | * locale for subsequent sessions.
28 | *
29 | * Tips:
30 | * - If you need to work wth raw HTML tags within the translations, you can use raw
31 | * HTML as a string and then use Svelte's {@html} to parse the string into valid HTML.
32 | * This can be really useful because translations don't usually have hyperlinks at the
33 | * same position.
34 | */
35 |
--------------------------------------------------------------------------------
/src/lib/translations/lang.json:
--------------------------------------------------------------------------------
1 | {
2 | "en": "English",
3 | "ja": "日本語"
4 | }
5 |
--------------------------------------------------------------------------------
/src/lib/translations/translations.ts:
--------------------------------------------------------------------------------
1 | import lang from './lang.json';
2 |
3 | export default ({
4 | en: {
5 | lang,
6 | menu: {
7 | darkMode: "Dark Mode",
8 | backends: "Backends",
9 | backupRestore: "Backup/Restore"
10 | },
11 | main: {
12 | introduction: "Technologic is a powerful, feature-rich AI Chatbot Client that is designed to work seamlessly with OpenAI's API or any compatible backend. With a user-friendly interface and the ability to organize, modify, and manage your conversations, Technologic brings you a next-level chatting experience with your AI assistant.",
13 | featuresHeading: "Features",
14 | features: [
15 | {
16 | title: "Secure Storage",
17 | description: "Your conversations are stored locally on your computer using your browser's IndexedDB storage."
18 | },
19 | {
20 | title: "Backend Compatibility",
21 | description: "Works with any backend compatible with OpenAI's API."
22 | },
23 | {
24 | title: "Bring Your Own API Key",
25 | description: "Easily configure your OpenAI API key or any other compatible backend."
26 | },
27 | {
28 | title: "Organize Conversations",
29 | description: "Keep your conversations tidy by organizing them into folders."
30 | },
31 | {
32 | title: "Message Modification",
33 | description: "Edit and modify messages, both sent and received, as needed."
34 | },
35 | {
36 | title: "Custom Personality",
37 | description: "Support for 'System Messages' to give your chatbot a unique personality (if supported by the backend)."
38 | },
39 | {
40 | title: "Fork Conversations",
41 | description: "Easily branch off into different topics without losing the context of previous conversations."
42 | },
43 | {
44 | title: "Elaborate",
45 | description: "Use the 'Go on' feature to prompt the bot to expand on its last message."
46 | },
47 | {
48 | title: "Merge Messages",
49 | description: "Combine messages to avoid fragmentation or incomplete code blocks."
50 | },
51 | {
52 | title: "View Raw Message",
53 | description: "Access the raw text of any message with the flip of a switch."
54 | }
55 | ],
56 | xpressai: "Technologic was created by Xpress AI, a company that specializes in developing AI solutions. With a focus on delivering cutting-edge technology that enhances the user experience, Xpress.ai's team of experts developed 'Technologic' to provide a unique and powerful chat client that enables users to have more dynamic and engaging conversations.",
57 | backendConfigurationWarning: "You must set your OpenAI credentials in the Backend Settings to be able to use that backend.",
58 | startNewConversation: "Start new Conversation"
59 | },
60 | settings: {
61 | backend: {
62 | backends: "Backends",
63 | useModel: "Use Model"
64 | },
65 | backup: {
66 | backupRestore: "Backup/Restore",
67 | downloadDatabase: "Download Database",
68 | reloadDatabase: "Reload Database"
69 | }
70 | }
71 | },
72 | ja: {
73 | lang,
74 | menu: {
75 | darkMode: "表示",
76 | backends: "バックエンド",
77 | backupRestore: "バックアップ・リストア"
78 | },
79 | main: {
80 | introduction: "Technologic は、OpenAI の API または互換性のあるバックエンドとシームレスに動作できる機能が豊富な AI チャットボット クライアントです。直感的なUIと会話を整理、変更、管理する機能を備えた Technologic は、AI アシスタントとの次のレベルのチャット 体験を提供します。",
81 | featuresHeading: "特徴",
82 | features: [
83 | {
84 | title: "安全なデータストレージ",
85 | description: "ブラウザのIndexedDBを使用しているため、自分のデータがずっとローカルに保存されています。"
86 | },
87 | {
88 | title: "バックエンドの互換性",
89 | description: "OpenAIのAPIに互換性があれば、使用可能です。"
90 | },
91 | {
92 | title: "自分のAPIキーをそのまま使える",
93 | description: "OpenAI APIキーまたはその他の互換性のあるバックエンドを簡単に設定できます。"
94 | },
95 | {
96 | title: "チャットを整理する",
97 | description: "チャットをフォルダ構造に整理することが可能です。"
98 | },
99 | {
100 | title: "メッセージが変更可能",
101 | description: "必要に応じて、送信メッセージと受信メッセージの両方を編集および変更可能です。"
102 | },
103 | {
104 | title: "チャットの雰囲気をカスタマイズできる",
105 | description: "チャットボットに独自の個性を与える「システムメッセージ」の対応(バックエンドが対応されている場合のみ)"
106 | },
107 | {
108 | title: "チャットをフォークする",
109 | description: "以前のチャットのコンテキストを失うことなく、別のテーマに簡単に分岐することができます。"
110 | },
111 | {
112 | title: "更に詳しく説明する",
113 | description: "「Go on」機能を使用して、チャットの最後のプロンプトを更に詳しく説明させることができます。"
114 | },
115 | {
116 | title: "チャットを結合させる",
117 | description: "チャットを結合させることにより、メッセージおよびコードの断片化を避けることができます。"
118 | },
119 | {
120 | title: "メッセージの生データを確認できる",
121 | description: "メッセージの生データをいつでも確認することができます。"
122 | }
123 | ],
124 | xpressai: "Technologic は、AI ソリューションの開発を専門とする会社Xpress AIが作成されました。 Xpress.ai の専門家チームは、ユーザー エクスペリエンスを向上させる最先端のテクノロジーの提供に重点を置いて、ユーザーがよりダイナミックで魅力的なチャットを可能にするユニークで強力なチャット クライアントを提供する「Technologic」を開発しました。",
125 | backendConfigurationWarning: "バックエンドを使用できるようにするには、バックエンド設定でOpenAI関連情報を設定する必要があります。",
126 | startNewConversation: "新しいチャットを開始"
127 | },
128 | settings: {
129 | backend: {
130 | backends: "バックエンド",
131 | useModel: "モデル"
132 | },
133 | backup: {
134 | backupRestore: "バックアップ・リストア",
135 | downloadDatabase: "データベースをダウンロード",
136 | reloadDatabase: "データベースをリストア"
137 | }
138 | }
139 | }
140 | });
141 |
--------------------------------------------------------------------------------
/src/lib/translations/util.ts:
--------------------------------------------------------------------------------
1 | enum Locale {
2 | en = "en",
3 | jp = "ja"
4 | }
5 |
6 | /** Gets the language locale from local storage given a key. If no locale is found,
7 | * it resolves to use a default, which is passed in as the second parameter.*/
8 | function getLocaleFromLocalStorageWithDefault(key: string = "locale", defaultLocale: Locale = Locale.en): string {
9 | let locale = localStorage.getItem(key);
10 |
11 | if (locale) {
12 | return locale
13 | } else {
14 | return JSON.stringify(defaultLocale);
15 | }
16 |
17 | }
18 |
19 | /** Gets the users default locale via browser's navigator property */
20 | function getUserLocale(): string {
21 | if (navigator.languages != undefined)
22 | return navigator.languages[0];
23 | return navigator.language;
24 | }
25 |
26 | export {
27 | Locale,
28 | getLocaleFromLocalStorageWithDefault
29 | }
30 |
--------------------------------------------------------------------------------
/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
50 |
51 |
52 |
53 |
54 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/src/routes/+layout.ts:
--------------------------------------------------------------------------------
1 | export const ssr = false;
2 |
3 | // i18n
4 | import { loadTranslations } from '$lib/translations';
5 | import { Locale, getLocaleFromLocalStorageWithDefault } from '$lib/translations/util';
6 |
7 | export const load = async ({ url }: any) => {
8 | const { pathname } = url;
9 | const initLocale = JSON.parse(getLocaleFromLocalStorageWithDefault("locale", Locale.en));
10 |
11 | await loadTranslations(initLocale, pathname);
12 | return {};
13 | }
14 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
18 |
19 |
20 | {$t('main.introduction')}
21 |
22 |
23 |
24 |
{$t('main.featuresHeading')}
25 |
26 | {#each { length: 10 } as _, i}
27 | -
28 | {$t(`main.features.${i}.title`)}: {$t(
29 | `main.features.${i}.description`
30 | )}
31 |
{/each}
32 |
33 |
34 |
35 |
36 |
Made by Xpress AI
37 |
38 | {@html $t('main.xpressai')}
39 |
40 |
41 |
42 |
43 |
44 | {#if misconfiguredOpenAI}
45 |
46 | {@html $t('main.backendConfigurationWarning')}
47 |
48 | {/if}
49 |
--------------------------------------------------------------------------------
/src/routes/[conversationId]/+page.svelte:
--------------------------------------------------------------------------------
1 |
284 |
285 |
286 | {conversationTitle}
287 |
288 |
289 |
290 |
291 |
292 | {#if isRenaming || waiting}{/if}
293 | {conversationTitle}
294 | {#if !autoSend}
295 |
296 | {/if}
297 |
298 |
299 | {#if $currentConversation}
300 |
306 | {/if}
307 |
364 |
365 |
366 |
367 |
368 |
369 | {#each $currentMessageThread.messages as msgAlt}
370 | {@const msg = $currentConversation?.messages[msgAlt.self]}
371 | fork(msg)}
379 | on:prevThread={(e) => selectPrevThread(msg, msgAlt)}
380 | on:nextThread={(e) => selectNextThread(msg, msgAlt)}
381 | on:regenerate={(e) => regenerate(msg)}
382 | on:saveAndFork={(e) => saveAndFork(msg, e.detail.newContent, e.detail.newRole)}
383 | on:saveInPlace={(e) => saveInPlace(msg, e.detail.newContent, e.detail.newRole)}
384 | on:merge={(e) => merge(msg)}
385 | on:trash={(e) => trash(msg)}
386 | />
387 | {/each}
388 |
389 |
390 |
391 |
450 |
451 |
452 |
470 |
--------------------------------------------------------------------------------
/src/routes/[conversationId]/conversationBroker.ts:
--------------------------------------------------------------------------------
1 | import type {ConversationStore} from "$lib/stores/schema";
2 | import type {Backend, Message} from "$lib/backend/types";
3 | import {get} from "svelte/store";
4 |
5 | export async function generateAnswer(currentConversation: ConversationStore, backend: Backend){
6 | const history = get(currentConversation.history);
7 |
8 | const source = { backend: backend.name, model: backend.model };
9 |
10 | let responseMessage = await currentConversation.addMessage({role: 'assistant', content: ''}, source, true, get(currentConversation)!.lastMessageId);
11 | await backend.sendMessageAndStream(history, async (content, done) => {
12 | responseMessage = await currentConversation.replaceMessage(
13 | responseMessage,
14 | {
15 | ...responseMessage,
16 | isStreaming: !done,
17 | message: {
18 | ...responseMessage.message,
19 | content: responseMessage.message.content + (content ?? '')
20 | }
21 | },
22 | );
23 |
24 | if (done && get(currentConversation)?.isUntitled) {
25 | await backend.renameConversationWithSummary(currentConversation);
26 | }
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/src/routes/settings/backends/+page.svelte:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 | {$t('settings.backend.backends')}
41 |
70 |
71 |
72 |
73 | {$t('settings.backend.useModel')}:
74 |
90 |
91 |
--------------------------------------------------------------------------------
/src/routes/settings/backends/[backendName]/+page.svelte:
--------------------------------------------------------------------------------
1 |
50 |
51 |
52 |
60 |
61 |
65 |
74 |
82 |
86 |
97 |
98 |
Models
99 |
100 | {#if dto.models}
101 | {#each dto.models as model}
102 | -
103 |
104 |
107 |
108 | {/each}
109 | {/if}
110 | -
111 |
117 |
118 |
119 |
120 |
121 |
134 |
135 |
--------------------------------------------------------------------------------
/src/routes/settings/backends/new/+page.svelte:
--------------------------------------------------------------------------------
1 |
49 |
50 |
51 |
59 |
60 |
64 |
73 |
81 |
85 |
94 |
95 |
Models
96 |
97 | {#each dto.models as model}
98 | -
99 |
100 |
103 |
104 | {/each}
105 | -
106 |
112 |
113 |
114 |
115 |
116 |
129 |
130 |
--------------------------------------------------------------------------------
/src/routes/settings/backup/+page.svelte:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 | {$t('settings.backup.backupRestore')}
40 |
41 | {$t('settings.backup.reloadDatabase')}
42 |
43 |
--------------------------------------------------------------------------------
/static/favicon.svg:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import preprocess from 'svelte-preprocess';
2 | import adapter from '@sveltejs/adapter-static'
3 | import { vitePreprocess } from '@sveltejs/kit/vite';
4 |
5 | /** @type {import('@sveltejs/kit').Config} */
6 | const config = {
7 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors
8 | // for more information about preprocessors
9 | preprocess: [
10 | vitePreprocess(),
11 | preprocess({
12 | postcss: true
13 | })
14 | ],
15 |
16 | kit: {
17 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
18 | // If your environment is not supported or you settled on a specific environment, switch out the adapter.
19 | // See https://kit.svelte.dev/docs/adapters for more information about adapters.
20 | adapter: adapter({
21 | fallback: 'index.html'
22 | }),
23 | prerender: { entries: [] }
24 | }
25 | };
26 |
27 | export default config;
28 |
--------------------------------------------------------------------------------
/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | content: [
3 | './src/**/*.{html,js,svelte,ts}',
4 | require('path').join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}')
5 | ],
6 |
7 | darkMode: 'class',
8 | theme: {
9 | extend: {}
10 | },
11 |
12 | plugins: [
13 | require('@tailwindcss/typography'),
14 | ...require('@skeletonlabs/skeleton/tailwind/skeleton.cjs')()
15 | ]
16 | };
17 |
18 | module.exports = config;
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true
12 | }
13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
14 | //
15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
16 | // from the referenced tsconfig.json - TypeScript does not merge them in
17 | }
18 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 | import { defineConfig, } from 'vite';
3 |
4 | export default defineConfig(() => {
5 | return {
6 | plugins: [sveltekit()],
7 | };
8 | });
9 |
--------------------------------------------------------------------------------