mirror of
https://github.com/open-webui/open-webui.git
synced 2024-11-25 16:33:05 +08:00
chat feature added
This commit is contained in:
parent
5cd4946df2
commit
5e03670f1e
13
.eslintignore
Normal file
13
.eslintignore
Normal file
@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
30
.eslintrc.cjs
Normal file
30
.eslintrc.cjs
Normal file
@ -0,0 +1,30 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
13
.prettierignore
Normal file
13
.prettierignore
Normal file
@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:latest
|
||||
|
||||
WORKDIR /app
|
||||
ENV ENV prod
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
CMD [ "node", "./build/index.js"]
|
63
README.md
Normal file
63
README.md
Normal file
@ -0,0 +1,63 @@
|
||||
# Ollama Web UI 👋
|
||||
|
||||
ChatGPT-Style Web Interface for Ollama 🦙
|
||||
|
||||
![Ollama Web UI Demo](./demo.gif)
|
||||
|
||||
## Features ⭐
|
||||
|
||||
- 🖥️ **Intuitive Interface**: Our chat interface takes inspiration from ChatGPT, ensuring a user-friendly experience.
|
||||
- 📱 **Responsive Design**: Enjoy a seamless experience on both desktop and mobile devices.
|
||||
- ⚡ **Swift Responsiveness**: Enjoy fast and responsive performance.
|
||||
- 🚀 **Effortless Setup**: Install seamlessly using Docker for a hassle-free experience.
|
||||
- 🤖 **Multiple Model Support**: Seamlessly switch between different chat models for diverse interactions.
|
||||
- 🌟 **Continuous Updates**: We are committed to improving Ollama Web UI with regular updates and new features.
|
||||
|
||||
## How to Install 🚀
|
||||
|
||||
### Using Docker 🐳
|
||||
|
||||
```bash
|
||||
docker build -t ollama-webui .
|
||||
docker run -d -p 3000:3000 --name ollama-webui --restart always ollama-webui
|
||||
```
|
||||
|
||||
Your Ollama Web UI should now be hosted at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
|
||||
|
||||
## What's Next? 🚀
|
||||
|
||||
### To-Do List 📝
|
||||
|
||||
Here are some exciting tasks on our to-do list:
|
||||
|
||||
- 📜 **Chat History**: Effortlessly access and manage your conversation history.
|
||||
- 📤📥 **Import/Export Chat History**: Seamlessly move your chat data in and out of the platform.
|
||||
- 🎨 **Customization**: Tailor your chat environment with personalized themes and styles.
|
||||
- 📥🗑️ **Download/Delete Models**: Easily acquire or remove models directly from the web UI.
|
||||
- ⚙️ **Advanced Parameters Support**: Harness the power of advanced settings for fine-tuned control.
|
||||
- 📚 **Enhanced Documentation**: Elevate your setup and customization experience with improved, comprehensive documentation.
|
||||
- 🌟 **User Interface Enhancement**: Elevate the user interface to deliver a smoother, more enjoyable interaction.
|
||||
- 📲🖥️ **Cross-Device Responsiveness**: Seamlessly transition between desktop and mobile for a consistent experience.
|
||||
- 🚀 **Integration with Messaging Platforms**: Explore possibilities for integrating with popular messaging platforms like Slack and Discord.
|
||||
- 🧐 **User Testing and Feedback Gathering**: Conduct thorough user testing to gather insights and refine our offerings based on valuable user feedback.
|
||||
|
||||
Feel free to contribute and help us make Ollama Web UI even better! 🙌
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
A big shoutout to our amazing contributors who have helped make this project possible! 🙏
|
||||
|
||||
- [Jeffrey Morgan (Creator of Ollama)](https://github.com/jmorganca)
|
||||
- [Timothy J. Baek](https://github.com/tjbck)
|
||||
|
||||
## License 📜
|
||||
|
||||
This project is licensed under the [MIT License](LICENSE) - see the [LICENSE](LICENSE) file for details. 📄
|
||||
|
||||
## Support 💬
|
||||
|
||||
If you have any questions, suggestions, or need assistance, please open an issue or join our [Discord community](https://discord.gg/ollama) to connect with us! 🤝
|
||||
|
||||
---
|
||||
|
||||
Let's make Ollama Web UI even more amazing together! 💪
|
6258
package-lock.json
generated
Normal file
6258
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
package.json
Normal file
39
package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "ollama-webui",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.4.3",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"marked": "^9.1.0",
|
||||
"svelte-french-toast": "^1.2.0"
|
||||
}
|
||||
}
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
5
run.sh
Normal file
5
run.sh
Normal file
@ -0,0 +1,5 @@
|
||||
docker stop ollama-webui || true
|
||||
docker rm ollama-webui || true
|
||||
docker build -t ollama-webui .
|
||||
docker run -d -p 3000:3000 --name ollama-webui --restart always ollama-webui
|
||||
docker image prune -f
|
41
src/app.css
Normal file
41
src/app.css
Normal file
@ -0,0 +1,41 @@
|
||||
@font-face {
|
||||
font-family: 'Arimo';
|
||||
src: url('/assets/fonts/Arimo-Variable.ttf');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply bg-gray-800;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
--tw-border-opacity: 1;
|
||||
background-color: rgba(217, 217, 227, 0.8);
|
||||
border-color: rgba(255, 255, 255, var(--tw-border-opacity));
|
||||
border-radius: 9999px;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
height: 1rem;
|
||||
width: 0.5rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
select {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");
|
||||
background-position: right 0.5rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.5em 1.5em;
|
||||
padding-right: 2.5rem;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
/* for Firefox */
|
||||
-moz-appearance: none;
|
||||
/* for Chrome */
|
||||
-webkit-appearance: none;
|
||||
}
|
12
src/app.d.ts
vendored
Normal file
12
src/app.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
12
src/app.html
Normal file
12
src/app.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
19
src/lib/components/chat/SettingsModal.svelte
Normal file
19
src/lib/components/chat/SettingsModal.svelte
Normal file
@ -0,0 +1,19 @@
|
||||
<script>
|
||||
import Modal from '../common/Modal.svelte';
|
||||
export let show = false;
|
||||
</script>
|
||||
|
||||
<Modal bind:show>
|
||||
<div class="mt-3 p-3 rounded-lg bg-gray-900">
|
||||
<label for="models" class="block mb-2 text-sm font-medium text-gray-200">Select a model</label>
|
||||
<select
|
||||
id="models"
|
||||
class="border border-gray-600 bg-gray-700 text-gray-200 text-sm rounded-lg block w-full p-2.5 placeholder-gray-400"
|
||||
>
|
||||
<option value="US">United States</option>
|
||||
<option value="CA">Canada</option>
|
||||
<option value="FR">France</option>
|
||||
<option value="DE">Germany</option>
|
||||
</select>
|
||||
</div>
|
||||
</Modal>
|
40
src/lib/components/common/Modal.svelte
Normal file
40
src/lib/components/common/Modal.svelte
Normal file
@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { fade, blur } from 'svelte/transition';
|
||||
|
||||
export let show = true;
|
||||
let mounted = false;
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
});
|
||||
|
||||
$: if (mounted) {
|
||||
if (show) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'unset';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="fixed top-0 right-0 left-0 bottom-0 bg-stone-900/50 w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="m-auto min-h-52 max-w-full w-[30rem] bg-stone-800 rounded-lg p-5 mx-3 shadow-3xl"
|
||||
transition:fade={{ delay: 100, duration: 200 }}
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
33
src/lib/components/common/Overlay.svelte
Normal file
33
src/lib/components/common/Overlay.svelte
Normal file
@ -0,0 +1,33 @@
|
||||
<script>
|
||||
import Spinner from './Spinner.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let content = '';
|
||||
|
||||
export let opacity = 1;
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
{#if show}
|
||||
<div class="absolute w-full h-full flex">
|
||||
<div
|
||||
class="absolute rounded"
|
||||
style="inset: -10px; opacity: {opacity}; backdrop-filter: blur(5px);"
|
||||
/>
|
||||
|
||||
<div class="flex w-full flex-col justify-center">
|
||||
<div class=" py-3">
|
||||
<Spinner className="ml-2" />
|
||||
</div>
|
||||
|
||||
{#if content !== ''}
|
||||
<div class="text-center text-gray-100 text-xs font-medium z-50">
|
||||
{content}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<slot />
|
||||
</div>
|
24
src/lib/components/common/Spinner.svelte
Normal file
24
src/lib/components/common/Spinner.svelte
Normal file
@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
export let className: string = 'text-white';
|
||||
export let theme: 'blue' | 'white' | 'black' = 'white';
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center text-center {className}">
|
||||
<svg
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5 {theme === 'blue'
|
||||
? 'text-sky-600'
|
||||
: theme === 'white'
|
||||
? 'text-white'
|
||||
: 'text-gray-600'} "
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
243
src/lib/components/layout/Navbar.svelte
Normal file
243
src/lib/components/layout/Navbar.svelte
Normal file
@ -0,0 +1,243 @@
|
||||
<script lang="ts">
|
||||
let show = false;
|
||||
let navElement;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class=" fixed top-0 flex flex-row justify-center bg-stone-100/5 text-gray-200 backdrop-blur-xl w-full z-30"
|
||||
>
|
||||
<div class="basis-full px-5">
|
||||
<nav class="py-3" id="nav">
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="pl-2">
|
||||
<button
|
||||
class=" cursor-pointer p-1 flex hover:bg-gray-700 rounded-lg transition"
|
||||
on:click={() => {
|
||||
show = !show;
|
||||
}}
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10zm0 5.25a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class=" self-center">Ollama Web UI</div>
|
||||
|
||||
<div class="pr-2">
|
||||
<button
|
||||
class=" cursor-pointer p-1 flex hover:bg-gray-700 rounded-lg transition"
|
||||
on:click={() => {
|
||||
location.href = location.href;
|
||||
console.log('new chat');
|
||||
}}
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
bind:this={navElement}
|
||||
class="h-screen {show
|
||||
? ''
|
||||
: '-translate-x-72'} w-72 fixed top-0 left-0 z-40 transition bg-gray-900 text-gray-200 shadow-2xl text-sm
|
||||
"
|
||||
>
|
||||
<div class="p-2.5 my-auto flex flex-col justify-between h-screen">
|
||||
<div class=" flex justify-center space-x-2">
|
||||
<button
|
||||
class=" cursor-pointer flex-grow rounded-md border border-gray-600 p-3 flex"
|
||||
on:click={() => {
|
||||
location.href = location.href;
|
||||
console.log('new chat');
|
||||
}}
|
||||
>
|
||||
<div class="self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class=" self-center">New Chat</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class=" cursor-pointer w-12 rounded-md border border-gray-600 flex"
|
||||
on:click={() => {
|
||||
show = !show;
|
||||
}}
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 4.25A2.25 2.25 0 015.25 2h5.5A2.25 2.25 0 0113 4.25v2a.75.75 0 01-1.5 0v-2a.75.75 0 00-.75-.75h-5.5a.75.75 0 00-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 00.75-.75v-2a.75.75 0 011.5 0v2A2.25 2.25 0 0110.75 18h-5.5A2.25 2.25 0 013 15.75V4.25z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M19 10a.75.75 0 00-.75-.75H8.704l1.048-.943a.75.75 0 10-1.004-1.114l-2.5 2.25a.75.75 0 000 1.114l2.5 2.25a.75.75 0 101.004-1.114l-1.048-.943h9.546A.75.75 0 0019 10z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="my-3 flex flex-col space-y-1 overflow-y-scroll">
|
||||
<button class=" flex rounded-md p-4 hover:bg-gray-800 transition">
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.076-4.076a1.526 1.526 0 011.037-.443 48.282 48.282 0 005.68-.494c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">
|
||||
We're currently working on bringing you the ability to access your chat history. Stay
|
||||
tuned for updates, and thank you for your patience!
|
||||
</div>
|
||||
</button>
|
||||
<!-- {#each Array(100) as a, i}
|
||||
<button
|
||||
class=" flex rounded-md p-4 hover:bg-gray-800 transition whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.076-4.076a1.526 1.526 0 011.037-.443 48.282 48.282 0 005.68-.494c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center overflow-hidden">{i} Chat History</div>
|
||||
</button>
|
||||
{/each} -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div
|
||||
class="h-screen fixed top-0 left-0 z-30 text-sm
|
||||
"
|
||||
>
|
||||
<div class="pl-2 pt-2">
|
||||
<button
|
||||
class=" cursor-pointer p-3 flex hover:bg-gray-700 rounded-lg transition"
|
||||
on:click={() => {
|
||||
show = !show;
|
||||
}}
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10zm0 5.25a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
-->
|
||||
|
||||
<!--
|
||||
|
||||
<div class="h-screen fixed top-0 right-0 z-30 text-sm">
|
||||
<div class="pr-2 pt-2">
|
||||
<button
|
||||
class=" cursor-pointer p-3 flex hover:bg-gray-700 rounded-lg transition"
|
||||
on:click={() => {
|
||||
chatHistory = {};
|
||||
}}
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8.34 1.804A1 1 0 019.32 1h1.36a1 1 0 01.98.804l.295 1.473c.497.144.971.342 1.416.587l1.25-.834a1 1 0 011.262.125l.962.962a1 1 0 01.125 1.262l-.834 1.25c.245.445.443.919.587 1.416l1.473.294a1 1 0 01.804.98v1.361a1 1 0 01-.804.98l-1.473.295a6.95 6.95 0 01-.587 1.416l.834 1.25a1 1 0 01-.125 1.262l-.962.962a1 1 0 01-1.262.125l-1.25-.834a6.953 6.953 0 01-1.416.587l-.294 1.473a1 1 0 01-.98.804H9.32a1 1 0 01-.98-.804l-.295-1.473a6.957 6.957 0 01-1.416-.587l-1.25.834a1 1 0 01-1.262-.125l-.962-.962a1 1 0 01-.125-1.262l.834-1.25a6.957 6.957 0 01-.587-1.416l-1.473-.294A1 1 0 011 10.68V9.32a1 1 0 01.804-.98l1.473-.295c.144-.497.342-.971.587-1.416l-.834-1.25a1 1 0 01.125-1.262l.962-.962A1 1 0 015.38 3.03l1.25.834a6.957 6.957 0 011.416-.587l.294-1.473zM13 10a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div> -->
|
7
src/lib/contants.ts
Normal file
7
src/lib/contants.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { browser, dev } from '$app/environment';
|
||||
|
||||
export const ENDPOINT = dev
|
||||
? 'http://127.0.0.1:11434'
|
||||
: browser
|
||||
? 'http://127.0.0.1:11434'
|
||||
: 'http://host.docker.internal:11434';
|
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
14
src/routes/+layout.svelte
Normal file
14
src/routes/+layout.svelte
Normal file
@ -0,0 +1,14 @@
|
||||
<script>
|
||||
import { Toaster } from 'svelte-french-toast';
|
||||
|
||||
import '../app.css';
|
||||
|
||||
import '../tailwind.css';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Ollama</title>
|
||||
</svelte:head>
|
||||
|
||||
<slot />
|
||||
<Toaster />
|
24
src/routes/+page.server.ts
Normal file
24
src/routes/+page.server.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { ENDPOINT } from '$lib/contants';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ url, fetch }) => {
|
||||
const models = await fetch(`${ENDPOINT}/api/tags`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
return {
|
||||
models: models
|
||||
};
|
||||
};
|
261
src/routes/+page.svelte
Normal file
261
src/routes/+page.svelte
Normal file
@ -0,0 +1,261 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import Navbar from '$lib/components/layout/Navbar.svelte';
|
||||
|
||||
import { marked } from 'marked';
|
||||
|
||||
import type { PageData } from './$types';
|
||||
import { ENDPOINT } from '$lib/contants';
|
||||
|
||||
export let data: PageData;
|
||||
$: ({ models } = data);
|
||||
|
||||
let selectedModel = '';
|
||||
let prompt = '';
|
||||
let context = '';
|
||||
|
||||
let chatHistory = {};
|
||||
|
||||
let textareaElement = '';
|
||||
|
||||
const submitPrompt = async () => {
|
||||
console.log('submitPrompt');
|
||||
if (selectedModel !== '') {
|
||||
console.log(prompt);
|
||||
|
||||
let user_prompt = prompt;
|
||||
chatHistory[Object.keys(chatHistory).length] = {
|
||||
role: 'user',
|
||||
content: user_prompt
|
||||
};
|
||||
prompt = '';
|
||||
textareaElement.style.height = '';
|
||||
|
||||
const res = await fetch(`${ENDPOINT}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: selectedModel,
|
||||
prompt: user_prompt,
|
||||
context: context != '' ? context : undefined
|
||||
})
|
||||
});
|
||||
|
||||
chatHistory[Object.keys(chatHistory).length] = {
|
||||
role: 'assistant',
|
||||
content: ''
|
||||
};
|
||||
|
||||
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
// toast.success(value);
|
||||
try {
|
||||
let data = JSON.parse(value);
|
||||
console.log(data);
|
||||
|
||||
if (data.done == false) {
|
||||
if (
|
||||
chatHistory[Object.keys(chatHistory).length - 1].content == '' &&
|
||||
data.response == '\n'
|
||||
) {
|
||||
continue;
|
||||
} else {
|
||||
chatHistory[Object.keys(chatHistory).length - 1].content += data.response;
|
||||
}
|
||||
} else {
|
||||
context = data.context;
|
||||
console.log(context);
|
||||
chatHistory[Object.keys(chatHistory).length - 1].done = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
} else {
|
||||
toast.error('Model not selected');
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text) => {
|
||||
if (!navigator.clipboard) {
|
||||
var textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.top = '0';
|
||||
textArea.style.left = '0';
|
||||
textArea.style.position = 'fixed';
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
var successful = document.execCommand('copy');
|
||||
var msg = successful ? 'successful' : 'unsuccessful';
|
||||
console.log('Fallback: Copying text command was ' + msg);
|
||||
} catch (err) {
|
||||
console.error('Fallback: Oops, unable to copy', err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(text).then(
|
||||
function () {
|
||||
console.log('Async: Copying to clipboard was successful!');
|
||||
toast.success('Copying to clipboard was successful!');
|
||||
},
|
||||
function (err) {
|
||||
console.error('Async: Could not copy text: ', err);
|
||||
}
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="app text-gray-100">
|
||||
<div class=" bg-gray-800 min-h-screen overflow-auto flex flex-row">
|
||||
<Navbar />
|
||||
|
||||
<div class="min-h-screen w-full flex justify-center">
|
||||
<div class=" py-2.5 flex flex-col justify-between w-full">
|
||||
<div class="max-w-2xl mx-auto w-full px-2.5 mt-14">
|
||||
<div class="p-3 rounded-lg bg-gray-900">
|
||||
<div>
|
||||
<label for="models" class="block mb-2 text-sm font-medium text-gray-200">Model</label>
|
||||
<select
|
||||
id="models"
|
||||
class="outline-none border border-gray-600 bg-gray-700 text-gray-200 text-sm rounded-lg block w-full p-2.5 placeholder-gray-400"
|
||||
bind:value={selectedModel}
|
||||
disabled={Object.keys(chatHistory).length != 0}
|
||||
>
|
||||
<option value="" selected>Select a model</option>
|
||||
|
||||
{#each models.models as model}
|
||||
<option value={model.name}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" h-full mb-32 w-full flex flex-col">
|
||||
{#if Object.keys(chatHistory).length == 0}
|
||||
<div class="m-auto text-4xl text-gray-600 font-bold text-center">Ollama</div>
|
||||
{:else}
|
||||
{#each Object.keys(chatHistory) as messageIdx}
|
||||
<div class=" w-full {chatHistory[messageIdx].role == 'user' ? '' : ' bg-gray-700'}">
|
||||
<div class="flex justify-between p-5 py-10 max-w-3xl mx-auto rounded-lg">
|
||||
<div class="space-x-7 flex">
|
||||
<div class="">
|
||||
<img
|
||||
src="/{chatHistory[messageIdx].role == 'user' ? 'user' : 'favicon'}.png"
|
||||
class=" max-w-[32px] object-cover rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="whitespace-pre-line">
|
||||
{@html marked.parse(chatHistory[messageIdx].content)}
|
||||
<!-- {} -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if chatHistory[messageIdx].role != 'user' && chatHistory[messageIdx].done}
|
||||
<button
|
||||
class="p-1 rounded hover:bg-gray-700 transition"
|
||||
on:click={() => {
|
||||
copyToClipboard(chatHistory[messageIdx].content);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fixed bottom-0 w-full">
|
||||
<!-- <hr class=" mb-3 border-gray-600" /> -->
|
||||
|
||||
<div class=" bg-gradient-to-t from-gray-900 pt-5">
|
||||
<div class="max-w-3xl p-2.5 -mb-0.5 mx-auto inset-x-0">
|
||||
<form class=" flex shadow-sm relative w-full" on:submit|preventDefault={submitPrompt}>
|
||||
<textarea
|
||||
class="rounded-xl bg-gray-700 outline-none w-full py-3 px-5 pr-12 resize-none"
|
||||
placeholder="Send a message"
|
||||
bind:this={textareaElement}
|
||||
bind:value={prompt}
|
||||
on:keypress={(e) => {
|
||||
if (e.keyCode == 13 && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (prompt !== '' && e.keyCode == 13 && !e.shiftKey) {
|
||||
submitPrompt();
|
||||
}
|
||||
}}
|
||||
rows="1"
|
||||
on:input={() => {
|
||||
textareaElement.style.height = '';
|
||||
textareaElement.style.height = Math.min(textareaElement.scrollHeight, 200) + 'px';
|
||||
}}
|
||||
/>
|
||||
<div class=" absolute right-0 bottom-0">
|
||||
<div class="pr-3 pb-2">
|
||||
<button
|
||||
class="{prompt !== ''
|
||||
? 'bg-emerald-600 text-gray-100 hover:bg-emerald-700 '
|
||||
: 'text-gray-600 disabled'} transition rounded p-2"
|
||||
type="submit"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
class="w-4 h-4"
|
||||
><path
|
||||
d="M.5 1.163A1 1 0 0 1 1.97.28l12.868 6.837a1 1 0 0 1 0 1.766L1.969 15.72A1 1 0 0 1 .5 14.836V10.33a1 1 0 0 1 .816-.983L8.5 8 1.316 6.653A1 1 0 0 1 .5 5.67V1.163Z"
|
||||
fill="currentColor"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-2.5 text-xs text-gray-500 text-center">
|
||||
LLM models may produce inaccurate information about people, places, or facts.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <main class="w-full flex justify-center">
|
||||
<div class="max-w-lg w-screen p-5" />
|
||||
</main> -->
|
||||
</div>
|
||||
</div>
|
18
src/tailwind.css
Normal file
18
src/tailwind.css
Normal file
@ -0,0 +1,18 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
font-family: 'Arimo', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu,
|
||||
Cantarell, 'Noto Sans', sans-serif, 'Helvetica Neue', Arial, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: 'Arimo', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu,
|
||||
Cantarell, 'Noto Sans', sans-serif, 'Helvetica Neue', Arial, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
BIN
static/assets/fonts/Arimo-Variable.ttf
Normal file
BIN
static/assets/fonts/Arimo-Variable.ttf
Normal file
Binary file not shown.
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 90 KiB |
BIN
static/user.png
Normal file
BIN
static/user.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
18
svelte.config.js
Normal file
18
svelte.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
24
tailwind.config.js
Normal file
24
tailwind.config.js
Normal file
@ -0,0 +1,24 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
gray: {
|
||||
50: '#f7f7f8',
|
||||
100: '#ececf1',
|
||||
200: '#d9d9e3',
|
||||
300: '#c5c5d2',
|
||||
400: '#acacbe',
|
||||
500: '#8e8ea0',
|
||||
600: '#565869',
|
||||
700: '#40414f',
|
||||
800: '#343541',
|
||||
900: '#202123',
|
||||
950: '#050509'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
};
|
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
6
vite.config.ts
Normal file
6
vite.config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
Loading…
Reference in New Issue
Block a user