Unverified Commit dbb83f98 authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge pull request #2856 from open-webui/dev

0.2.5
parents f28877f4 e1889b0c
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation? - [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
- [ ] **Testing:** Have you written and run sufficient tests for validating the changes? - [ ] **Testing:** Have you written and run sufficient tests for validating the changes?
- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards? - [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
- [ ] **Label:** To cleary categorize this pull request, assign a relevant label to the pull request title, using one of the following: - [ ] **Prefix:** To cleary categorize this pull request, prefix the pull request title, using one of the following:
- **BREAKING CHANGE**: Significant changes that may affect compatibility - **BREAKING CHANGE**: Significant changes that may affect compatibility
- **build**: Changes that affect the build system or external dependencies - **build**: Changes that affect the build system or external dependencies
- **ci**: Changes to our continuous integration processes or workflows - **ci**: Changes to our continuous integration processes or workflows
......
...@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. ...@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.5] - 2024-06-05
### Added
- **👥 Active Users Indicator**: Now you can see how many people are currently active and what they are running. This helps you gauge when performance might slow down due to a high number of users.
- **🗂️ Create Ollama Modelfile**: The option to create a modelfile for Ollama has been reintroduced in the Settings > Models section, making it easier to manage your models.
- **⚙️ Default Model Setting**: Added an option to set the default model from Settings > Interface. This feature is now easily accessible, especially convenient for mobile users as it was previously hidden.
- **🌐 Enhanced Translations**: We've improved the Chinese translations and added support for Turkmen and Norwegian languages to make the interface more accessible globally.
### Fixed
- **📱 Mobile View Improvements**: The UI now uses dvh (dynamic viewport height) instead of vh (viewport height), providing a better and more responsive experience for mobile users.
## [0.2.4] - 2024-06-03 ## [0.2.4] - 2024-06-03
### Added ### Added
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
[![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s) [![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck) [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/). Open WebUI is an [extensible](https://github.com/open-webui/pipelines), feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
![Open WebUI Demo](./demo.gif) ![Open WebUI Demo](./demo.gif)
......
...@@ -274,54 +274,57 @@ async def get_ollama_tags( ...@@ -274,54 +274,57 @@ async def get_ollama_tags(
@app.get("/api/version") @app.get("/api/version")
@app.get("/api/version/{url_idx}") @app.get("/api/version/{url_idx}")
async def get_ollama_versions(url_idx: Optional[int] = None): async def get_ollama_versions(url_idx: Optional[int] = None):
if app.state.config.ENABLE_OLLAMA_API:
if url_idx == None:
# returns lowest version
tasks = [
fetch_url(f"{url}/api/version")
for url in app.state.config.OLLAMA_BASE_URLS
]
responses = await asyncio.gather(*tasks)
responses = list(filter(lambda x: x is not None, responses))
if len(responses) > 0:
lowest_version = min(
responses,
key=lambda x: tuple(
map(int, re.sub(r"^v|-.*", "", x["version"]).split("."))
),
)
if url_idx == None: return {"version": lowest_version["version"]}
else:
# returns lowest version raise HTTPException(
tasks = [ status_code=500,
fetch_url(f"{url}/api/version") for url in app.state.config.OLLAMA_BASE_URLS detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
] )
responses = await asyncio.gather(*tasks)
responses = list(filter(lambda x: x is not None, responses))
if len(responses) > 0:
lowest_version = min(
responses,
key=lambda x: tuple(
map(int, re.sub(r"^v|-.*", "", x["version"]).split("."))
),
)
return {"version": lowest_version["version"]}
else: else:
raise HTTPException( url = app.state.config.OLLAMA_BASE_URLS[url_idx]
status_code=500,
detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
)
else:
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
r = None
try:
r = requests.request(method="GET", url=f"{url}/api/version")
r.raise_for_status()
return r.json() r = None
except Exception as e: try:
log.exception(e) r = requests.request(method="GET", url=f"{url}/api/version")
error_detail = "Open WebUI: Server Connection Error" r.raise_for_status()
if r is not None:
try: return r.json()
res = r.json() except Exception as e:
if "error" in res: log.exception(e)
error_detail = f"Ollama: {res['error']}" error_detail = "Open WebUI: Server Connection Error"
except: if r is not None:
error_detail = f"Ollama: {e}" try:
res = r.json()
if "error" in res:
error_detail = f"Ollama: {res['error']}"
except:
error_detail = f"Ollama: {e}"
raise HTTPException( raise HTTPException(
status_code=r.status_code if r else 500, status_code=r.status_code if r else 500,
detail=error_detail, detail=error_detail,
) )
else:
return {"version": False}
class ModelNameForm(BaseModel): class ModelNameForm(BaseModel):
......
...@@ -1164,6 +1164,30 @@ def reset_vector_db(user=Depends(get_admin_user)): ...@@ -1164,6 +1164,30 @@ def reset_vector_db(user=Depends(get_admin_user)):
CHROMA_CLIENT.reset() CHROMA_CLIENT.reset()
@app.get("/reset/uploads")
def reset_upload_dir(user=Depends(get_admin_user)) -> bool:
folder = f"{UPLOAD_DIR}"
try:
# Check if the directory exists
if os.path.exists(folder):
# Iterate over all the files and directories in the specified directory
for filename in os.listdir(folder):
file_path = os.path.join(folder, filename)
try:
if os.path.isfile(file_path) or os.path.islink(file_path):
os.unlink(file_path) # Remove the file or link
elif os.path.isdir(file_path):
shutil.rmtree(file_path) # Remove the directory
except Exception as e:
print(f"Failed to delete {file_path}. Reason: {e}")
else:
print(f"The directory {folder} does not exist")
except Exception as e:
print(f"Failed to process the directory {folder}. Reason: {e}")
return True
@app.get("/reset") @app.get("/reset")
def reset(user=Depends(get_admin_user)) -> bool: def reset(user=Depends(get_admin_user)) -> bool:
folder = f"{UPLOAD_DIR}" folder = f"{UPLOAD_DIR}"
......
import socketio
import asyncio
from apps.webui.models.users import Users
from utils.utils import decode_token
sio = socketio.AsyncServer(cors_allowed_origins=[], async_mode="asgi")
app = socketio.ASGIApp(sio, socketio_path="/ws/socket.io")
# Dictionary to maintain the user pool
USER_POOL = {}
USAGE_POOL = {}
# Timeout duration in seconds
TIMEOUT_DURATION = 3
@sio.event
async def connect(sid, environ, auth):
print("connect ", sid)
user = None
if auth and "token" in auth:
data = decode_token(auth["token"])
if data is not None and "id" in data:
user = Users.get_user_by_id(data["id"])
if user:
USER_POOL[sid] = user.id
print(f"user {user.name}({user.id}) connected with session ID {sid}")
print(len(set(USER_POOL)))
await sio.emit("user-count", {"count": len(set(USER_POOL))})
await sio.emit("usage", {"models": get_models_in_use()})
@sio.on("user-join")
async def user_join(sid, data):
print("user-join", sid, data)
auth = data["auth"] if "auth" in data else None
if auth and "token" in auth:
data = decode_token(auth["token"])
if data is not None and "id" in data:
user = Users.get_user_by_id(data["id"])
if user:
USER_POOL[sid] = user.id
print(f"user {user.name}({user.id}) connected with session ID {sid}")
print(len(set(USER_POOL)))
await sio.emit("user-count", {"count": len(set(USER_POOL))})
@sio.on("user-count")
async def user_count(sid):
print("user-count", sid)
await sio.emit("user-count", {"count": len(set(USER_POOL))})
def get_models_in_use():
# Aggregate all models in use
models_in_use = []
for model_id, data in USAGE_POOL.items():
models_in_use.append(model_id)
print(f"Models in use: {models_in_use}")
return models_in_use
@sio.on("usage")
async def usage(sid, data):
print(f'Received "usage" event from {sid}: {data}')
model_id = data["model"]
# Cancel previous callback if there is one
if model_id in USAGE_POOL:
USAGE_POOL[model_id]["callback"].cancel()
# Store the new usage data and task
if model_id in USAGE_POOL:
USAGE_POOL[model_id]["sids"].append(sid)
USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"]))
else:
USAGE_POOL[model_id] = {"sids": [sid]}
# Schedule a task to remove the usage data after TIMEOUT_DURATION
USAGE_POOL[model_id]["callback"] = asyncio.create_task(
remove_after_timeout(sid, model_id)
)
# Broadcast the usage data to all clients
await sio.emit("usage", {"models": get_models_in_use()})
async def remove_after_timeout(sid, model_id):
try:
print("remove_after_timeout", sid, model_id)
await asyncio.sleep(TIMEOUT_DURATION)
if model_id in USAGE_POOL:
print(USAGE_POOL[model_id]["sids"])
USAGE_POOL[model_id]["sids"].remove(sid)
USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"]))
if len(USAGE_POOL[model_id]["sids"]) == 0:
del USAGE_POOL[model_id]
print(f"Removed usage data for {model_id} due to timeout")
# Broadcast the usage data to all clients
await sio.emit("usage", {"models": get_models_in_use()})
except asyncio.CancelledError:
# Task was cancelled due to new 'usage' event
pass
@sio.event
async def disconnect(sid):
if sid in USER_POOL:
disconnected_user = USER_POOL.pop(sid)
print(f"user {disconnected_user} disconnected with session ID {sid}")
await sio.emit("user-count", {"count": len(USER_POOL)})
else:
print(f"Unknown session ID {sid} disconnected")
...@@ -84,3 +84,7 @@ class ERROR_MESSAGES(str, Enum): ...@@ -84,3 +84,7 @@ class ERROR_MESSAGES(str, Enum):
WEB_SEARCH_ERROR = ( WEB_SEARCH_ERROR = (
lambda err="": f"{err if err else 'Oops! Something went wrong while searching the web.'}" lambda err="": f"{err if err else 'Oops! Something went wrong while searching the web.'}"
) )
OLLAMA_API_DISABLED = (
"The Ollama API is disabled. Please enable it to use this feature."
)
...@@ -20,6 +20,8 @@ from starlette.exceptions import HTTPException as StarletteHTTPException ...@@ -20,6 +20,8 @@ from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import StreamingResponse, Response from starlette.responses import StreamingResponse, Response
from apps.socket.main import app as socket_app
from apps.ollama.main import app as ollama_app, get_all_models as get_ollama_models from apps.ollama.main import app as ollama_app, get_all_models as get_ollama_models
from apps.openai.main import app as openai_app, get_all_models as get_openai_models from apps.openai.main import app as openai_app, get_all_models as get_openai_models
...@@ -376,6 +378,9 @@ async def update_embedding_function(request: Request, call_next): ...@@ -376,6 +378,9 @@ async def update_embedding_function(request: Request, call_next):
return response return response
app.mount("/ws", socket_app)
app.mount("/ollama", ollama_app) app.mount("/ollama", ollama_app)
app.mount("/openai", openai_app) app.mount("/openai", openai_app)
......
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.2.4", "version": "0.2.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "open-webui", "name": "open-webui",
"version": "0.2.4", "version": "0.2.5",
"dependencies": { "dependencies": {
"@pyscript/core": "^0.4.32", "@pyscript/core": "^0.4.32",
"@sveltejs/adapter-node": "^1.3.1", "@sveltejs/adapter-node": "^1.3.1",
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
"marked": "^9.1.0", "marked": "^9.1.0",
"mermaid": "^10.9.1", "mermaid": "^10.9.1",
"pyodide": "^0.26.0-alpha.4", "pyodide": "^0.26.0-alpha.4",
"socket.io-client": "^4.7.5",
"sortablejs": "^1.15.2", "sortablejs": "^1.15.2",
"svelte-sonner": "^0.3.19", "svelte-sonner": "^0.3.19",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
...@@ -1214,6 +1215,11 @@ ...@@ -1214,6 +1215,11 @@
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"dev": true "dev": true
}, },
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
},
"node_modules/@sveltejs/adapter-auto": { "node_modules/@sveltejs/adapter-auto": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-2.1.1.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-2.1.1.tgz",
...@@ -3800,6 +3806,46 @@ ...@@ -3800,6 +3806,46 @@
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"node_modules/engine.io-client": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/engine.io-client/node_modules/ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
"integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/enquirer": { "node_modules/enquirer": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
...@@ -7949,6 +7995,32 @@ ...@@ -7949,6 +7995,32 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/socket.io-client": {
"version": "4.7.5",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/sorcery": { "node_modules/sorcery": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
...@@ -10142,6 +10214,14 @@ ...@@ -10142,6 +10214,14 @@
} }
} }
}, },
"node_modules/xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/xtend": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
......
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.2.4", "version": "0.2.5",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "npm run pyodide:fetch && vite dev --host", "dev": "npm run pyodide:fetch && vite dev --host",
...@@ -65,6 +65,7 @@ ...@@ -65,6 +65,7 @@
"marked": "^9.1.0", "marked": "^9.1.0",
"mermaid": "^10.9.1", "mermaid": "^10.9.1",
"pyodide": "^0.26.0-alpha.4", "pyodide": "^0.26.0-alpha.4",
"socket.io-client": "^4.7.5",
"sortablejs": "^1.15.2", "sortablejs": "^1.15.2",
"svelte-sonner": "^0.3.19", "svelte-sonner": "^0.3.19",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
......
...@@ -369,21 +369,29 @@ export const generateChatCompletion = async (token: string = '', body: object) = ...@@ -369,21 +369,29 @@ export const generateChatCompletion = async (token: string = '', body: object) =
return [res, controller]; return [res, controller];
}; };
export const createModel = async (token: string, tagName: string, content: string) => { export const createModel = async (
token: string,
tagName: string,
content: string,
urlIdx: string | null = null
) => {
let error = null; let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/create`, { const res = await fetch(
method: 'POST', `${OLLAMA_API_BASE_URL}/api/create${urlIdx !== null ? `/${urlIdx}` : ''}`,
headers: { {
Accept: 'application/json', method: 'POST',
'Content-Type': 'application/json', headers: {
Authorization: `Bearer ${token}` Accept: 'application/json',
}, 'Content-Type': 'application/json',
body: JSON.stringify({ Authorization: `Bearer ${token}`
name: tagName, },
modelfile: content body: JSON.stringify({
}) name: tagName,
}).catch((err) => { modelfile: content
})
}
).catch((err) => {
error = err; error = err;
return null; return null;
}); });
......
...@@ -359,6 +359,32 @@ export const scanDocs = async (token: string) => { ...@@ -359,6 +359,32 @@ export const scanDocs = async (token: string) => {
return res; return res;
}; };
export const resetUploadDir = async (token: string) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/reset/uploads`, {
method: 'GET',
headers: {
Accept: 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const resetVectorDB = async (token: string) => { export const resetVectorDB = async (token: string) => {
let error = null; let error = null;
......
...@@ -18,7 +18,8 @@ ...@@ -18,7 +18,8 @@
tags as _tags, tags as _tags,
WEBUI_NAME, WEBUI_NAME,
banners, banners,
user user,
socket
} from '$lib/stores'; } from '$lib/stores';
import { import {
convertMessagesToHistory, convertMessagesToHistory,
...@@ -280,6 +281,16 @@ ...@@ -280,6 +281,16 @@
} }
}; };
const getChatEventEmitter = async (modelId: string, chatId: string = '') => {
return setInterval(() => {
$socket?.emit('usage', {
action: 'chat',
model: modelId,
chat_id: chatId
});
}, 1000);
};
////////////////////////// //////////////////////////
// Ollama functions // Ollama functions
////////////////////////// //////////////////////////
...@@ -451,6 +462,8 @@ ...@@ -451,6 +462,8 @@
} }
responseMessage.userContext = userContext; responseMessage.userContext = userContext;
const chatEventEmitter = await getChatEventEmitter(model.id, _chatId);
if (webSearchEnabled) { if (webSearchEnabled) {
await getWebSearchResults(model.id, parentId, responseMessageId); await getWebSearchResults(model.id, parentId, responseMessageId);
} }
...@@ -460,6 +473,10 @@ ...@@ -460,6 +473,10 @@
} else if (model) { } else if (model) {
await sendPromptOllama(model, prompt, responseMessageId, _chatId); await sendPromptOllama(model, prompt, responseMessageId, _chatId);
} }
console.log('chatEventEmitter', chatEventEmitter);
if (chatEventEmitter) clearInterval(chatEventEmitter);
} else { } else {
toast.error($i18n.t(`Model {{modelId}} not found`, { modelId })); toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
} }
...@@ -542,6 +559,7 @@ ...@@ -542,6 +559,7 @@
const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => { const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
model = model.id; model = model.id;
const responseMessage = history.messages[responseMessageId]; const responseMessage = history.messages[responseMessageId];
// Wait until history/message have been updated // Wait until history/message have been updated
...@@ -1177,7 +1195,7 @@ ...@@ -1177,7 +1195,7 @@
{#if !chatIdProp || (loaded && chatIdProp)} {#if !chatIdProp || (loaded && chatIdProp)}
<div <div
class="min-h-screen max-h-screen {$showSidebar class="h-screen max-h-[100dvh] {$showSidebar
? 'md:max-w-[calc(100%-260px)]' ? 'md:max-w-[calc(100%-260px)]'
: ''} w-full max-w-full flex flex-col" : ''} w-full max-w-full flex flex-col"
> >
......
...@@ -286,7 +286,7 @@ ...@@ -286,7 +286,7 @@
{#each messages as message, messageIdx} {#each messages as message, messageIdx}
<div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}"> <div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}">
<div <div
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null class="flex flex-col justify-between px-5 mb-3 {$settings?.widescreenMode ?? null
? 'max-w-full' ? 'max-w-full'
: 'max-w-5xl'} mx-auto rounded-lg group" : 'max-w-5xl'} mx-auto rounded-lg group"
> >
......
...@@ -315,8 +315,8 @@ ...@@ -315,8 +315,8 @@
</select> </select>
</div> </div>
</div> </div>
<div class="flex items-center justify-between mb-1"> <div class="flex items-center justify-between my-1.5">
<div class="text-sm"> <div class="text-xs">
{$i18n.t('Allow non-local voices')} {$i18n.t('Allow non-local voices')}
</div> </div>
......
...@@ -16,11 +16,12 @@ ...@@ -16,11 +16,12 @@
let responseAutoCopy = false; let responseAutoCopy = false;
let titleAutoGenerateModel = ''; let titleAutoGenerateModel = '';
let titleAutoGenerateModelExternal = ''; let titleAutoGenerateModelExternal = '';
let fullScreenMode = false; let widescreenMode = false;
let titleGenerationPrompt = ''; let titleGenerationPrompt = '';
let splitLargeChunks = false; let splitLargeChunks = false;
// Interface // Interface
let defaultModelId = '';
let promptSuggestions = []; let promptSuggestions = [];
let showUsername = false; let showUsername = false;
let chatBubble = true; let chatBubble = true;
...@@ -31,9 +32,9 @@ ...@@ -31,9 +32,9 @@
saveSettings({ splitLargeChunks: splitLargeChunks }); saveSettings({ splitLargeChunks: splitLargeChunks });
}; };
const toggleFullScreenMode = async () => { const togglewidescreenMode = async () => {
fullScreenMode = !fullScreenMode; widescreenMode = !widescreenMode;
saveSettings({ fullScreenMode: fullScreenMode }); saveSettings({ widescreenMode: widescreenMode });
}; };
const toggleChatBubble = async () => { const toggleChatBubble = async () => {
...@@ -96,7 +97,8 @@ ...@@ -96,7 +97,8 @@
modelExternal: modelExternal:
titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined, titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined,
prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
} },
models: [defaultModelId]
}); });
}; };
...@@ -114,9 +116,11 @@ ...@@ -114,9 +116,11 @@
responseAutoCopy = $settings.responseAutoCopy ?? false; responseAutoCopy = $settings.responseAutoCopy ?? false;
showUsername = $settings.showUsername ?? false; showUsername = $settings.showUsername ?? false;
chatBubble = $settings.chatBubble ?? true; chatBubble = $settings.chatBubble ?? true;
fullScreenMode = $settings.fullScreenMode ?? false; widescreenMode = $settings.widescreenMode ?? false;
splitLargeChunks = $settings.splitLargeChunks ?? false; splitLargeChunks = $settings.splitLargeChunks ?? false;
chatDirection = $settings.chatDirection ?? 'LTR'; chatDirection = $settings.chatDirection ?? 'LTR';
defaultModelId = ($settings?.models ?? ['']).at(0);
}); });
</script> </script>
...@@ -195,16 +199,16 @@ ...@@ -195,16 +199,16 @@
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Full Screen Mode')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Widescreen Mode')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
on:click={() => { on:click={() => {
toggleFullScreenMode(); togglewidescreenMode();
}} }}
type="button" type="button"
> >
{#if fullScreenMode === true} {#if widescreenMode === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
...@@ -278,7 +282,30 @@ ...@@ -278,7 +282,30 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-850" />
<div class=" space-y-1 mb-3">
<div class="mb-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
</div>
</div>
<div class="flex-1 mr-2">
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={defaultModelId}
placeholder="Select a model"
>
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{#each $models.filter((model) => model.id) as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
{/each}
</select>
</div>
</div>
<hr class=" dark:border-gray-850" />
<div> <div>
<div class=" mb-2.5 text-sm font-medium flex"> <div class=" mb-2.5 text-sm font-medium flex">
......
...@@ -43,6 +43,13 @@ ...@@ -43,6 +43,13 @@
let modelTransferring = false; let modelTransferring = false;
let modelTag = ''; let modelTag = '';
let createModelLoading = false;
let createModelTag = '';
let createModelContent = '';
let createModelDigest = '';
let createModelPullProgress = null;
let digest = ''; let digest = '';
let pullProgress = null; let pullProgress = null;
...@@ -434,6 +441,83 @@ ...@@ -434,6 +441,83 @@
} }
}; };
const createModelHandler = async () => {
createModelLoading = true;
const res = await createModel(
localStorage.token,
createModelTag,
createModelContent,
selectedOllamaUrlIdx
).catch((error) => {
toast.error(error);
return null;
});
if (res && res.ok) {
const reader = res.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(splitStream('\n'))
.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
try {
let lines = value.split('\n');
for (const line of lines) {
if (line !== '') {
console.log(line);
let data = JSON.parse(line);
console.log(data);
if (data.error) {
throw data.error;
}
if (data.detail) {
throw data.detail;
}
if (data.status) {
if (
!data.digest &&
!data.status.includes('writing') &&
!data.status.includes('sha256')
) {
toast.success(data.status);
} else {
if (data.digest) {
createModelDigest = data.digest;
if (data.completed) {
createModelPullProgress =
Math.round((data.completed / data.total) * 1000) / 10;
} else {
createModelPullProgress = 100;
}
}
}
}
}
}
} catch (error) {
console.log(error);
toast.error(error);
}
}
}
models.set(await getModels());
createModelLoading = false;
createModelTag = '';
createModelContent = '';
createModelDigest = '';
createModelPullProgress = null;
};
onMount(async () => { onMount(async () => {
const ollamaConfig = await getOllamaConfig(localStorage.token); const ollamaConfig = await getOllamaConfig(localStorage.token);
...@@ -695,6 +779,77 @@ ...@@ -695,6 +779,77 @@
</div> </div>
</div> </div>
<div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('Create a model')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2 flex flex-col gap-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
modelTag: 'my-modelfile'
})}
bind:value={createModelTag}
disabled={createModelLoading}
/>
<textarea
bind:value={createModelContent}
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
rows="6"
placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`}
disabled={createModelLoading}
/>
</div>
<div class="flex self-start">
<button
class="px-2.5 py-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed"
on:click={() => {
createModelHandler();
}}
disabled={createModelLoading}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-4"
>
<path
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
</button>
</div>
</div>
{#if createModelDigest !== ''}
<div class="flex flex-col mt-1">
<div class="font-medium mb-1">{createModelTag}</div>
<div class="">
<div class="flex flex-row justify-between space-x-4 pr-2">
<div class=" flex-1">
<div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: {Math.max(15, createModelPullProgress ?? 0)}%"
>
{createModelPullProgress ?? 0}%
</div>
</div>
</div>
{#if createModelDigest}
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{createModelDigest}
</div>
{/if}
</div>
</div>
{/if}
</div>
<div class="pt-1"> <div class="pt-1">
<div class="flex justify-between items-center text-xs"> <div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">{$i18n.t('Experimental')}</div> <div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
{#if !dismissed} {#if !dismissed}
{#if mounted} {#if mounted}
<div <div
class=" top-0 left-0 right-0 p-2 mx-4 px-3 flex justify-center items-center relative rounded-xl border border-gray-50 dark:border-gray-850 text-gray-800 dark:text-gary-100 bg-white dark:bg-gray-900 backdrop-blur-xl z-40" class=" top-0 left-0 right-0 p-2 mx-4 px-3 flex justify-center items-center relative rounded-xl border border-gray-50 dark:border-gray-850 text-gray-800 dark:text-gary-100 bg-white dark:bg-gray-900 backdrop-blur-xl z-30"
transition:fade={{ delay: 100, duration: 300 }} transition:fade={{ delay: 100, duration: 300 }}
> >
<div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5"> <div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5">
......
...@@ -21,6 +21,10 @@ ...@@ -21,6 +21,10 @@
touch: touch touch: touch
}); });
} }
} else if (tooltipInstance && content === '') {
if (tooltipInstance) {
tooltipInstance.destroy();
}
} }
onDestroy(() => { onDestroy(() => {
......
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
getEmbeddingConfig, getEmbeddingConfig,
updateEmbeddingConfig, updateEmbeddingConfig,
getRerankingConfig, getRerankingConfig,
updateRerankingConfig updateRerankingConfig,
resetUploadDir
} from '$lib/apis/rag'; } from '$lib/apis/rag';
import { documents, models } from '$lib/stores'; import { documents, models } from '$lib/stores';
...@@ -24,6 +25,7 @@ ...@@ -24,6 +25,7 @@
let updateRerankingModelLoading = false; let updateRerankingModelLoading = false;
let showResetConfirm = false; let showResetConfirm = false;
let showResetUploadDirConfirm = false;
let embeddingEngine = ''; let embeddingEngine = '';
let embeddingModel = ''; let embeddingModel = '';
...@@ -496,99 +498,203 @@ ...@@ -496,99 +498,203 @@
{/if} {/if}
</div> </div>
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-850" />
{#if showResetConfirm} <div>
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition"> {#if showResetUploadDirConfirm}
<div class="flex items-center space-x-3"> <div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
<svg <div class="flex items-center space-x-3">
xmlns="http://www.w3.org/2000/svg" <svg
viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
fill="currentColor" viewBox="0 0 24 24"
class="w-4 h-4" fill="currentColor"
> class="size-4"
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" /> >
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z" d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
</svg> <path
<span>{$i18n.t('Are you sure?')}</span> d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
</div> />
</svg>
<span>{$i18n.t('Are you sure?')}</span>
</div>
<div class="flex space-x-1.5 items-center"> <div class="flex space-x-1.5 items-center">
<button <button
class="hover:text-white transition" class="hover:text-white transition"
on:click={() => { on:click={() => {
const res = resetVectorDB(localStorage.token).catch((error) => { const res = resetUploadDir(localStorage.token).catch((error) => {
toast.error(error); toast.error(error);
return null; return null;
}); });
if (res) {
toast.success($i18n.t('Success'));
}
showResetConfirm = false; if (res) {
}} toast.success($i18n.t('Success'));
> }
showResetUploadDirConfirm = false;
}}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class="hover:text-white transition"
type="button"
on:click={() => {
showResetUploadDirConfirm = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
</div>
{:else}
<button
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
showResetUploadDirConfirm = true;
}}
type="button"
>
<div class=" self-center mr-3">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
class="w-4 h-4" class="size-4"
> >
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
<path
d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
/>
</svg> </svg>
</button> </div>
<button <div class=" self-center text-sm font-medium">{$i18n.t('Reset Upload Directory')}</div>
class="hover:text-white transition" </button>
on:click={() => { {/if}
showResetConfirm = false;
}} {#if showResetConfirm}
> <div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
<div class="flex items-center space-x-3">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 16 16"
fill="currentColor" fill="currentColor"
class="w-4 h-4" class="w-4 h-4"
> >
<path <path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" fill-rule="evenodd"
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
clip-rule="evenodd"
/> />
</svg> </svg>
</button> <span>{$i18n.t('Are you sure?')}</span>
</div> </div>
</div>
{:else} <div class="flex space-x-1.5 items-center">
<button <button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" class="hover:text-white transition"
on:click={() => { on:click={() => {
showResetConfirm = true; const res = resetVectorDB(localStorage.token).catch((error) => {
}} toast.error(error);
> return null;
<div class=" self-center mr-3"> });
<svg
xmlns="http://www.w3.org/2000/svg" if (res) {
viewBox="0 0 16 16" toast.success($i18n.t('Success'));
fill="currentColor" }
class="w-4 h-4"
> showResetConfirm = false;
<path }}
fill-rule="evenodd" type="button"
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z" >
clip-rule="evenodd" <svg
/> xmlns="http://www.w3.org/2000/svg"
</svg> viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class="hover:text-white transition"
on:click={() => {
showResetConfirm = false;
}}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
</div> </div>
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div> {:else}
</button> <button
{/if} class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
showResetConfirm = true;
}}
type="button"
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div>
</button>
{/if}
</div>
</div> </div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
<button <button
......
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