"docs/vscode:/vscode.git/clone" did not exist on "cd3aa153a4e4974802385f209ad343149af02c07"
Unverified Commit 9c1aa924 authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge branch 'main' into feat/delete-message

parents a3a0e183 b0f3ae57
namespace: ollama-namespace namespace: open-webui
ollama: ollama:
replicaCount: 1 replicaCount: 1
image: ollama/ollama:latest image: ollama/ollama:latest
servicePort: 11434 servicePort: 11434
resources: resources:
limits: requests:
cpu: "2000m" cpu: "2000m"
memory: "2Gi" memory: "2Gi"
limits:
cpu: "4000m"
memory: "4Gi"
nvidia.com/gpu: "0" nvidia.com/gpu: "0"
volumeSize: 1Gi volumeSize: 30Gi
nodeSelector: {} nodeSelector: {}
tolerations: [] tolerations: []
service: service:
...@@ -19,19 +22,22 @@ ollama: ...@@ -19,19 +22,22 @@ ollama:
webui: webui:
replicaCount: 1 replicaCount: 1
image: ghcr.io/ollama-webui/ollama-webui:main image: ghcr.io/open-webui/open-webui:main
servicePort: 8080 servicePort: 8080
resources: resources:
limits: requests:
cpu: "500m" cpu: "500m"
memory: "500Mi" memory: "500Mi"
limits:
cpu: "1000m"
memory: "1Gi"
ingress: ingress:
enabled: true enabled: true
annotations: annotations:
# Use appropriate annotations for your Ingress controller, e.g., for NGINX: # Use appropriate annotations for your Ingress controller, e.g., for NGINX:
# nginx.ingress.kubernetes.io/rewrite-target: / # nginx.ingress.kubernetes.io/rewrite-target: /
host: ollama.minikube.local host: open-webui.minikube.local
volumeSize: 1Gi volumeSize: 2Gi
nodeSelector: {} nodeSelector: {}
tolerations: [] tolerations: []
service: service:
......
...@@ -2,7 +2,7 @@ apiVersion: v1 ...@@ -2,7 +2,7 @@ apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: ollama-service name: ollama-service
namespace: ollama-namespace namespace: open-webui
spec: spec:
selector: selector:
app: ollama app: ollama
......
...@@ -2,7 +2,7 @@ apiVersion: apps/v1 ...@@ -2,7 +2,7 @@ apiVersion: apps/v1
kind: StatefulSet kind: StatefulSet
metadata: metadata:
name: ollama name: ollama
namespace: ollama-namespace namespace: open-webui
spec: spec:
serviceName: "ollama" serviceName: "ollama"
replicas: 1 replicas: 1
...@@ -20,9 +20,13 @@ spec: ...@@ -20,9 +20,13 @@ spec:
ports: ports:
- containerPort: 11434 - containerPort: 11434
resources: resources:
limits: requests:
cpu: "2000m" cpu: "2000m"
memory: "2Gi" memory: "2Gi"
limits:
cpu: "4000m"
memory: "4Gi"
nvidia.com/gpu: "0"
volumeMounts: volumeMounts:
- name: ollama-volume - name: ollama-volume
mountPath: /root/.ollama mountPath: /root/.ollama
...@@ -34,4 +38,4 @@ spec: ...@@ -34,4 +38,4 @@ spec:
accessModes: [ "ReadWriteOnce" ] accessModes: [ "ReadWriteOnce" ]
resources: resources:
requests: requests:
storage: 1Gi storage: 30Gi
\ No newline at end of file \ No newline at end of file
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: ollama-namespace name: open-webui
\ No newline at end of file \ No newline at end of file
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: ollama-webui-deployment name: open-webui-deployment
namespace: ollama-namespace namespace: open-webui
spec: spec:
replicas: 1 replicas: 1
selector: selector:
matchLabels: matchLabels:
app: ollama-webui app: open-webui
template: template:
metadata: metadata:
labels: labels:
app: ollama-webui app: open-webui
spec: spec:
containers: containers:
- name: ollama-webui - name: open-webui
image: ghcr.io/ollama-webui/ollama-webui:main image: ghcr.io/open-webui/open-webui:main
ports: ports:
- containerPort: 8080 - containerPort: 8080
resources: resources:
limits: requests:
cpu: "500m" cpu: "500m"
memory: "500Mi" memory: "500Mi"
limits:
cpu: "1000m"
memory: "1Gi"
env: env:
- name: OLLAMA_API_BASE_URL - name: OLLAMA_API_BASE_URL
value: "http://ollama-service.ollama-namespace.svc.cluster.local:11434/api" value: "http://ollama-service.open-webui.svc.cluster.local:11434/api"
tty: true tty: true
volumeMounts:
- name: webui-volume
mountPath: /app/backend/data
volumes:
- name: webui-volume
persistentVolumeClaim:
claimName: ollama-webui-pvc
\ No newline at end of file
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: ollama-webui-ingress name: open-webui-ingress
namespace: ollama-namespace namespace: open-webui
#annotations: #annotations:
# Use appropriate annotations for your Ingress controller, e.g., for NGINX: # Use appropriate annotations for your Ingress controller, e.g., for NGINX:
# nginx.ingress.kubernetes.io/rewrite-target: / # nginx.ingress.kubernetes.io/rewrite-target: /
spec: spec:
rules: rules:
- host: ollama.minikube.local - host: open-webui.minikube.local
http: http:
paths: paths:
- path: / - path: /
pathType: Prefix pathType: Prefix
backend: backend:
service: service:
name: ollama-webui-service name: open-webui-service
port: port:
number: 8080 number: 8080
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
labels:
app: ollama-webui
name: ollama-webui-pvc
namespace: ollama-namespace
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 2Gi
\ No newline at end of file
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: ollama-webui-service name: open-webui-service
namespace: ollama-namespace namespace: open-webui
spec: spec:
type: NodePort # Use LoadBalancer if you're on a cloud that supports it type: NodePort # Use LoadBalancer if you're on a cloud that supports it
selector: selector:
app: ollama-webui app: open-webui
ports: ports:
- protocol: TCP - protocol: TCP
port: 8080 port: 8080
......
resources: resources:
- base/ollama-namespace.yaml - base/open-webui.yaml
- base/ollama-service.yaml - base/ollama-service.yaml
- base/ollama-statefulset.yaml - base/ollama-statefulset.yaml
- base/webui-deployment.yaml - base/webui-deployment.yaml
......
...@@ -2,7 +2,7 @@ apiVersion: apps/v1 ...@@ -2,7 +2,7 @@ apiVersion: apps/v1
kind: StatefulSet kind: StatefulSet
metadata: metadata:
name: ollama name: ollama
namespace: ollama-namespace namespace: open-webui
spec: spec:
selector: selector:
matchLabels: matchLabels:
......
{ {
"name": "ollama-webui", "name": "open-webui",
"version": "0.0.1", "version": "0.0.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ollama-webui", "name": "open-webui",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@sveltejs/adapter-node": "^1.3.1", "@sveltejs/adapter-node": "^1.3.1",
......
{ {
"name": "ollama-webui", "name": "open-webui",
"version": "0.0.1", "version": "0.1.0-101",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev --host", "dev": "vite dev --host",
......
#!/bin/bash #!/bin/bash
image_name="ollama-webui" image_name="open-webui"
container_name="ollama-webui" container_name="open-webui"
host_port=3000 host_port=3000
container_port=8080 container_port=8080
......
...@@ -261,3 +261,60 @@ export const toggleSignUpEnabledStatus = async (token: string) => { ...@@ -261,3 +261,60 @@ export const toggleSignUpEnabledStatus = async (token: string) => {
return res; return res;
}; };
export const getJWTExpiresDuration = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/token/expires`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const updateJWTExpiresDuration = async (token: string, duration: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/token/expires/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
duration: duration
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
import { IMAGES_API_BASE_URL } from '$lib/constants';
export const getImageGenerationEnabledStatus = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/enabled`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res;
};
export const toggleImageGenerationEnabledStatus = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/enabled/toggle`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res;
};
export const getAUTOMATIC1111Url = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/url`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.AUTOMATIC1111_BASE_URL;
};
export const updateAUTOMATIC1111Url = async (token: string = '', url: string) => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/url/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
url: url
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.AUTOMATIC1111_BASE_URL;
};
export const getDiffusionModels = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/models`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res;
};
export const getDefaultDiffusionModel = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/models/default`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.model;
};
export const updateDefaultDiffusionModel = async (token: string = '', model: string) => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/models/default/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
model: model
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.model;
};
export const imageGenerations = async (token: string = '', prompt: string) => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/generations`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
prompt: prompt
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res;
};
import { WEBUI_API_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
export const getBackendConfig = async () => { export const getBackendConfig = async () => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/`, { const res = await fetch(`${WEBUI_BASE_URL}/api/config`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
......
...@@ -133,9 +133,19 @@ export const getOllamaModels = async (token: string = '') => { ...@@ -133,9 +133,19 @@ export const getOllamaModels = async (token: string = '') => {
}); });
}; };
export const generateTitle = async (token: string = '', model: string, prompt: string) => { // TODO: migrate to backend
export const generateTitle = async (
token: string = '',
template: string,
model: string,
prompt: string
) => {
let error = null; let error = null;
template = template.replace(/{{prompt}}/g, prompt);
console.log(template);
const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, { const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, {
method: 'POST', method: 'POST',
headers: { headers: {
...@@ -144,7 +154,7 @@ export const generateTitle = async (token: string = '', model: string, prompt: s ...@@ -144,7 +154,7 @@ export const generateTitle = async (token: string = '', model: string, prompt: s
}, },
body: JSON.stringify({ body: JSON.stringify({
model: model, model: model,
prompt: `Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': ${prompt}`, prompt: template,
stream: false stream: false
}) })
}) })
......
<script lang="ts"> <script lang="ts">
import { import {
getDefaultUserRole, getDefaultUserRole,
getJWTExpiresDuration,
getSignUpEnabledStatus, getSignUpEnabledStatus,
toggleSignUpEnabledStatus, toggleSignUpEnabledStatus,
updateDefaultUserRole updateDefaultUserRole,
updateJWTExpiresDuration
} from '$lib/apis/auths'; } from '$lib/apis/auths';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
export let saveHandler: Function; export let saveHandler: Function;
let signUpEnabled = true; let signUpEnabled = true;
let defaultUserRole = 'pending'; let defaultUserRole = 'pending';
let JWTExpiresIn = '';
const toggleSignUpEnabled = async () => { const toggleSignUpEnabled = async () => {
signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token); signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token);
...@@ -19,9 +22,14 @@ ...@@ -19,9 +22,14 @@
defaultUserRole = await updateDefaultUserRole(localStorage.token, role); defaultUserRole = await updateDefaultUserRole(localStorage.token, role);
}; };
const updateJWTExpiresDurationHandler = async (duration) => {
JWTExpiresIn = await updateJWTExpiresDuration(localStorage.token, duration);
};
onMount(async () => { onMount(async () => {
signUpEnabled = await getSignUpEnabledStatus(localStorage.token); signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
defaultUserRole = await getDefaultUserRole(localStorage.token); defaultUserRole = await getDefaultUserRole(localStorage.token);
JWTExpiresIn = await getJWTExpiresDuration(localStorage.token);
}); });
</script> </script>
...@@ -29,6 +37,7 @@ ...@@ -29,6 +37,7 @@
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
// console.log('submit'); // console.log('submit');
updateJWTExpiresDurationHandler(JWTExpiresIn);
saveHandler(); saveHandler();
}} }}
> >
...@@ -94,6 +103,29 @@ ...@@ -94,6 +103,29 @@
</select> </select>
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700 my-3" />
<div class=" w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">JWT Expiration</div>
</div>
<div class="flex mt-2 space-x-2">
<input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="text"
placeholder={`e.g.) "30m","1h", "10d". `}
bind:value={JWTExpiresIn}
/>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Valid time units: <span class=" text-gray-300 font-medium"
>'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.</span
>
</div>
</div>
</div> </div>
</div> </div>
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
import ResponseMessage from './Messages/ResponseMessage.svelte'; import ResponseMessage from './Messages/ResponseMessage.svelte';
import Placeholder from './Messages/Placeholder.svelte'; import Placeholder from './Messages/Placeholder.svelte';
import Spinner from '../common/Spinner.svelte'; import Spinner from '../common/Spinner.svelte';
import { imageGenerations } from '$lib/apis/images';
export let chatId = ''; export let chatId = '';
export let sendPrompt: Function; export let sendPrompt: Function;
...@@ -339,6 +340,16 @@ ...@@ -339,6 +340,16 @@
{copyToClipboard} {copyToClipboard}
{continueGeneration} {continueGeneration}
{regenerateResponse} {regenerateResponse}
on:save={async (e) => {
console.log('save', e);
const message = e.detail;
history.messages[message.id] = message;
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
}}
/> />
{/if} {/if}
</div> </div>
......
...@@ -2,21 +2,25 @@ ...@@ -2,21 +2,25 @@
import toast from 'svelte-french-toast'; import toast from 'svelte-french-toast';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { marked } from 'marked'; import { marked } from 'marked';
import { settings } from '$lib/stores';
import tippy from 'tippy.js'; import tippy from 'tippy.js';
import auto_render from 'katex/dist/contrib/auto-render.mjs'; import auto_render from 'katex/dist/contrib/auto-render.mjs';
import 'katex/dist/katex.min.css'; import 'katex/dist/katex.min.css';
import { createEventDispatcher } from 'svelte';
import { onMount, tick } from 'svelte'; import { onMount, tick } from 'svelte';
const dispatch = createEventDispatcher();
import { config, settings } from '$lib/stores';
import { synthesizeOpenAISpeech } from '$lib/apis/openai';
import { imageGenerations } from '$lib/apis/images';
import { extractSentences } from '$lib/utils';
import Name from './Name.svelte'; import Name from './Name.svelte';
import ProfileImage from './ProfileImage.svelte'; import ProfileImage from './ProfileImage.svelte';
import Skeleton from './Skeleton.svelte'; import Skeleton from './Skeleton.svelte';
import CodeBlock from './CodeBlock.svelte'; import CodeBlock from './CodeBlock.svelte';
import { synthesizeOpenAISpeech } from '$lib/apis/openai';
import { extractSentences } from '$lib/utils';
export let modelfiles = []; export let modelfiles = [];
export let message; export let message;
export let siblings; export let siblings;
...@@ -43,6 +47,8 @@ ...@@ -43,6 +47,8 @@
let loadingSpeech = false; let loadingSpeech = false;
let generatingImage = false;
$: tokens = marked.lexer(message.content); $: tokens = marked.lexer(message.content);
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
...@@ -81,7 +87,9 @@ ...@@ -81,7 +87,9 @@
}<br/> }<br/>
prompt_token/s: ${ prompt_token/s: ${
Math.round( Math.round(
((message.info.prompt_eval_count ?? 0) / (message.info.prompt_eval_duration / 1000000000)) * 100 ((message.info.prompt_eval_count ?? 0) /
(message.info.prompt_eval_duration / 1000000000)) *
100
) / 100 ?? 'N/A' ) / 100 ?? 'N/A'
} tokens<br/> } tokens<br/>
total_duration: ${ total_duration: ${
...@@ -114,10 +122,11 @@ ...@@ -114,10 +122,11 @@
// customised options // customised options
// • auto-render specific keys, e.g.: // • auto-render specific keys, e.g.:
delimiters: [ delimiters: [
{ left: '$$', right: '$$', display: true }, { left: '$$', right: '$$', display: false },
// { left: '$', right: '$', display: false }, { left: '$', right: '$', display: false },
{ left: '\\(', right: '\\)', display: true }, { left: '\\(', right: '\\)', display: false },
{ left: '\\[', right: '\\]', display: true } { left: '\\[', right: '\\]', display: false },
{ left: '[ ', right: ' ]', display: false }
], ],
// • rendering keys, e.g.: // • rendering keys, e.g.:
throwOnError: false throwOnError: false
...@@ -264,6 +273,23 @@ ...@@ -264,6 +273,23 @@
renderStyling(); renderStyling();
}; };
const generateImage = async (message) => {
generatingImage = true;
const res = await imageGenerations(localStorage.token, message.content);
console.log(res);
if (res) {
message.files = res.images.map((image) => ({
type: 'image',
url: `data:image/png;base64,${image}`
}));
dispatch('save', message);
}
generatingImage = false;
};
onMount(async () => { onMount(async () => {
await tick(); await tick();
renderStyling(); renderStyling();
...@@ -292,6 +318,18 @@ ...@@ -292,6 +318,18 @@
{#if message.content === ''} {#if message.content === ''}
<Skeleton /> <Skeleton />
{:else} {:else}
{#if message.files}
<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
{#each message.files as file}
<div>
{#if file.type === 'image'}
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
{/if}
</div>
{/each}
</div>
{/if}
<div <div
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-li:-mb-4 whitespace-pre-line" class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-li:-mb-4 whitespace-pre-line"
> >
...@@ -592,6 +630,71 @@ ...@@ -592,6 +630,71 @@
{/if} {/if}
</button> </button>
{#if $config.images}
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => {
if (!generatingImage) {
generateImage(message);
}
}}
>
{#if generatingImage}
<svg
class=" w-4 h-4"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_S1WN {
animation: spinner_MGfb 0.8s linear infinite;
animation-delay: -0.8s;
}
.spinner_Km9P {
animation-delay: -0.65s;
}
.spinner_JApP {
animation-delay: -0.5s;
}
@keyframes spinner_MGfb {
93.75%,
100% {
opacity: 0.2;
}
}
</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
class="spinner_S1WN spinner_Km9P"
cx="12"
cy="12"
r="3"
/><circle
class="spinner_S1WN spinner_JApP"
cx="20"
cy="12"
r="3"
/></svg
>
{:else}
<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 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
/>
</svg>
{/if}
</button>
{/if}
{#if message.info} {#if message.info}
<button <button
class=" {isLastMessage class=" {isLastMessage
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment