enh: access control

This commit is contained in:
Timothy Jaeryang Baek 2024-11-16 17:09:15 -08:00
parent 227cca35e8
commit 73fe77c2da
9 changed files with 304 additions and 277 deletions

View File

@ -7,6 +7,8 @@ from open_webui.apps.webui.models.groups import Groups
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text, JSON from sqlalchemy import BigInteger, Column, String, Text, JSON
from open_webui.utils.access_control import has_access
#################### ####################
# Prompts DB Schema # Prompts DB Schema
#################### ####################
@ -107,58 +109,12 @@ class PromptsTable:
) -> list[PromptModel]: ) -> list[PromptModel]:
prompts = self.get_prompts() prompts = self.get_prompts()
groups = Groups.get_groups_by_member_id(user_id) return [
group_ids = [group.id for group in groups] prompt
for prompt in prompts
if permission == "write": if prompt.user_id == user_id
return [ or has_access(user_id, permission, prompt.access_control)
prompt ]
for prompt in prompts
if prompt.user_id == user_id
or (
prompt.access_control
and (
any(
group_id
in prompt.access_control.get(permission, {}).get(
"group_ids", []
)
for group_id in group_ids
)
or (
user_id
in prompt.access_control.get(permission, {}).get(
"user_ids", []
)
)
)
)
]
elif permission == "read":
return [
prompt
for prompt in prompts
if prompt.user_id == user_id
or prompt.access_control is None
or (
prompt.access_control
and (
any(
prompt.access_control.get(permission, {}).get(
"group_ids", []
)
in group_id
for group_id in group_ids
)
or (
user_id
in prompt.access_control.get(permission, {}).get(
"user_ids", []
)
)
)
)
]
def update_prompt_by_command( def update_prompt_by_command(
self, command: str, form_data: PromptForm self, command: str, form_data: PromptForm

View File

@ -8,6 +8,9 @@ from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text, JSON from sqlalchemy import BigInteger, Column, String, Text, JSON
from open_webui.utils.access_control import has_access
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"]) log.setLevel(SRC_LOG_LEVELS["MODELS"])
@ -133,6 +136,18 @@ class ToolsTable:
with get_db() as db: with get_db() as db:
return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()] return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()]
def get_tools_by_user_id(
self, user_id: str, permission: str = "write"
) -> list[ToolModel]:
tools = self.get_tools()
return [
tool
for tool in tools
if tool.user_id == user_id
or has_access(tool.access_control, user_id, permission)
]
def get_tool_valves_by_id(self, id: str) -> Optional[dict]: def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
try: try:
with get_db() as db: with get_db() as db:

View File

@ -14,7 +14,22 @@ router = APIRouter()
@router.get("/", response_model=list[PromptModel]) @router.get("/", response_model=list[PromptModel])
async def get_prompts(user=Depends(get_verified_user)): async def get_prompts(user=Depends(get_verified_user)):
return Prompts.get_prompts() if user.role == "admin":
prompts = Prompts.get_prompts()
else:
prompts = Prompts.get_prompts_by_user_id(user.id, "read")
return prompts
@router.get("/list", response_model=list[PromptModel])
async def get_prompt_list(user=Depends(get_verified_user)):
if user.role == "admin":
prompts = Prompts.get_prompts()
else:
prompts = Prompts.get_prompts_by_user_id(user.id, "write")
return prompts
############################ ############################
@ -23,7 +38,7 @@ async def get_prompts(user=Depends(get_verified_user)):
@router.post("/create", response_model=Optional[PromptModel]) @router.post("/create", response_model=Optional[PromptModel])
async def create_new_prompt(form_data: PromptForm, user=Depends(get_admin_user)): async def create_new_prompt(form_data: PromptForm, user=Depends(get_verified_user)):
prompt = Prompts.get_prompt_by_command(form_data.command) prompt = Prompts.get_prompt_by_command(form_data.command)
if prompt is None: if prompt is None:
prompt = Prompts.insert_new_prompt(user.id, form_data) prompt = Prompts.insert_new_prompt(user.id, form_data)
@ -67,7 +82,7 @@ async def get_prompt_by_command(command: str, user=Depends(get_verified_user)):
async def update_prompt_by_command( async def update_prompt_by_command(
command: str, command: str,
form_data: PromptForm, form_data: PromptForm,
user=Depends(get_admin_user), user=Depends(get_verified_user),
): ):
prompt = Prompts.update_prompt_by_command(f"/{command}", form_data) prompt = Prompts.update_prompt_by_command(f"/{command}", form_data)
if prompt: if prompt:
@ -85,6 +100,6 @@ async def update_prompt_by_command(
@router.delete("/command/{command}/delete", response_model=bool) @router.delete("/command/{command}/delete", response_model=bool)
async def delete_prompt_by_command(command: str, user=Depends(get_admin_user)): async def delete_prompt_by_command(command: str, user=Depends(get_verified_user)):
result = Prompts.delete_prompt_by_command(f"/{command}") result = Prompts.delete_prompt_by_command(f"/{command}")
return result return result

View File

@ -14,37 +14,54 @@ from open_webui.utils.utils import get_admin_user, get_verified_user
router = APIRouter() router = APIRouter()
############################ ############################
# GetToolkits # GetTools
############################ ############################
@router.get("/", response_model=list[ToolResponse]) @router.get("/", response_model=list[ToolResponse])
async def get_toolkits(user=Depends(get_verified_user)): async def get_tools(user=Depends(get_verified_user)):
toolkits = [toolkit for toolkit in Tools.get_tools()] if user.role == "admin":
return toolkits tools = Tools.get_tools()
else:
tools = Tools.get_tools_by_user_id(user.id, "read")
return tools
############################ ############################
# ExportToolKits # GetToolList
############################
@router.get("/list", response_model=list[ToolResponse])
async def get_tool_list(user=Depends(get_verified_user)):
if user.role == "admin":
tools = Tools.get_tools()
else:
tools = Tools.get_tools_by_user_id(user.id, "write")
return tools
############################
# ExportTools
############################ ############################
@router.get("/export", response_model=list[ToolModel]) @router.get("/export", response_model=list[ToolModel])
async def get_toolkits(user=Depends(get_admin_user)): async def export_tools(user=Depends(get_admin_user)):
toolkits = [toolkit for toolkit in Tools.get_tools()] tools = Tools.get_tools()
return toolkits return tools
############################ ############################
# CreateNewToolKit # CreateNewTools
############################ ############################
@router.post("/create", response_model=Optional[ToolResponse]) @router.post("/create", response_model=Optional[ToolResponse])
async def create_new_toolkit( async def create_new_tools(
request: Request, request: Request,
form_data: ToolForm, form_data: ToolForm,
user=Depends(get_admin_user), user=Depends(get_verified_user),
): ):
if not form_data.id.isidentifier(): if not form_data.id.isidentifier():
raise HTTPException( raise HTTPException(
@ -93,12 +110,12 @@ async def create_new_toolkit(
############################ ############################
# GetToolkitById # GetToolsById
############################ ############################
@router.get("/id/{id}", response_model=Optional[ToolModel]) @router.get("/id/{id}", response_model=Optional[ToolModel])
async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)): async def get_tools_by_id(id: str, user=Depends(get_verified_user)):
toolkit = Tools.get_tool_by_id(id) toolkit = Tools.get_tool_by_id(id)
if toolkit: if toolkit:
@ -111,16 +128,16 @@ async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)):
############################ ############################
# UpdateToolkitById # UpdateToolsById
############################ ############################
@router.post("/id/{id}/update", response_model=Optional[ToolModel]) @router.post("/id/{id}/update", response_model=Optional[ToolModel])
async def update_toolkit_by_id( async def update_tools_by_id(
request: Request, request: Request,
id: str, id: str,
form_data: ToolForm, form_data: ToolForm,
user=Depends(get_admin_user), user=Depends(get_verified_user),
): ):
try: try:
form_data.content = replace_imports(form_data.content) form_data.content = replace_imports(form_data.content)
@ -158,12 +175,14 @@ async def update_toolkit_by_id(
############################ ############################
# DeleteToolkitById # DeleteToolsById
############################ ############################
@router.delete("/id/{id}/delete", response_model=bool) @router.delete("/id/{id}/delete", response_model=bool)
async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin_user)): async def delete_tools_by_id(
request: Request, id: str, user=Depends(get_verified_user)
):
result = Tools.delete_tool_by_id(id) result = Tools.delete_tool_by_id(id)
if result: if result:
@ -180,7 +199,7 @@ async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin
@router.get("/id/{id}/valves", response_model=Optional[dict]) @router.get("/id/{id}/valves", response_model=Optional[dict])
async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)): async def get_tools_valves_by_id(id: str, user=Depends(get_verified_user)):
toolkit = Tools.get_tool_by_id(id) toolkit = Tools.get_tool_by_id(id)
if toolkit: if toolkit:
try: try:
@ -204,8 +223,8 @@ async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)):
@router.get("/id/{id}/valves/spec", response_model=Optional[dict]) @router.get("/id/{id}/valves/spec", response_model=Optional[dict])
async def get_toolkit_valves_spec_by_id( async def get_tools_valves_spec_by_id(
request: Request, id: str, user=Depends(get_admin_user) request: Request, id: str, user=Depends(get_verified_user)
): ):
toolkit = Tools.get_tool_by_id(id) toolkit = Tools.get_tool_by_id(id)
if toolkit: if toolkit:
@ -232,8 +251,8 @@ async def get_toolkit_valves_spec_by_id(
@router.post("/id/{id}/valves/update", response_model=Optional[dict]) @router.post("/id/{id}/valves/update", response_model=Optional[dict])
async def update_toolkit_valves_by_id( async def update_tools_valves_by_id(
request: Request, id: str, form_data: dict, user=Depends(get_admin_user) request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
): ):
toolkit = Tools.get_tool_by_id(id) toolkit = Tools.get_tool_by_id(id)
if toolkit: if toolkit:
@ -276,7 +295,7 @@ async def update_toolkit_valves_by_id(
@router.get("/id/{id}/valves/user", response_model=Optional[dict]) @router.get("/id/{id}/valves/user", response_model=Optional[dict])
async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user)): async def get_tools_user_valves_by_id(id: str, user=Depends(get_verified_user)):
toolkit = Tools.get_tool_by_id(id) toolkit = Tools.get_tool_by_id(id)
if toolkit: if toolkit:
try: try:
@ -295,7 +314,7 @@ async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user)
@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict]) @router.get("/id/{id}/valves/user/spec", response_model=Optional[dict])
async def get_toolkit_user_valves_spec_by_id( async def get_tools_user_valves_spec_by_id(
request: Request, id: str, user=Depends(get_verified_user) request: Request, id: str, user=Depends(get_verified_user)
): ):
toolkit = Tools.get_tool_by_id(id) toolkit = Tools.get_tool_by_id(id)
@ -318,7 +337,7 @@ async def get_toolkit_user_valves_spec_by_id(
@router.post("/id/{id}/valves/user/update", response_model=Optional[dict]) @router.post("/id/{id}/valves/user/update", response_model=Optional[dict])
async def update_toolkit_user_valves_by_id( async def update_tools_user_valves_by_id(
request: Request, id: str, form_data: dict, user=Depends(get_verified_user) request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
): ):
toolkit = Tools.get_tool_by_id(id) toolkit = Tools.get_tool_by_id(id)

View File

@ -69,6 +69,39 @@ export const getPrompts = async (token: string = '') => {
return res; return res;
}; };
export const getPromptList = async (token: string = '') => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/list`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getPromptByCommand = async (token: string, command: string) => { export const getPromptByCommand = async (token: string, command: string) => {
let error = null; let error = null;

View File

@ -3,11 +3,17 @@
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
import { onMount, getContext } from 'svelte';
import { WEBUI_NAME, config, prompts } from '$lib/stores';
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
import { error } from '@sveltejs/kit';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onMount, getContext } from 'svelte';
import { WEBUI_NAME, config, prompts as _prompts, user } from '$lib/stores';
import {
createNewPrompt,
deletePromptByCommand,
getPrompts,
getPromptList
} from '$lib/apis/prompts';
import PromptMenu from './Prompts/PromptMenu.svelte'; import PromptMenu from './Prompts/PromptMenu.svelte';
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte'; import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
@ -16,16 +22,18 @@
import ChevronRight from '../icons/ChevronRight.svelte'; import ChevronRight from '../icons/ChevronRight.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
let promptsImportInputElement: HTMLInputElement;
let importFiles = ''; let importFiles = '';
let query = ''; let query = '';
let promptsImportInputElement: HTMLInputElement;
let prompts = [];
let showDeleteConfirm = false; let showDeleteConfirm = false;
let deletePrompt = null; let deletePrompt = null;
let filteredItems = []; let filteredItems = [];
$: filteredItems = $prompts.filter((p) => query === '' || p.command.includes(query)); $: filteredItems = prompts.filter((p) => query === '' || p.command.includes(query));
const shareHandler = async (prompt) => { const shareHandler = async (prompt) => {
toast.success($i18n.t('Redirecting you to OpenWebUI Community')); toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
@ -60,8 +68,17 @@
const deleteHandler = async (prompt) => { const deleteHandler = async (prompt) => {
const command = prompt.command; const command = prompt.command;
await deletePromptByCommand(localStorage.token, command); await deletePromptByCommand(localStorage.token, command);
await prompts.set(await getPrompts(localStorage.token)); await init();
}; };
const init = async () => {
prompts = await getPromptList(localStorage.token);
await _prompts.set(await getPrompts(localStorage.token));
};
onMount(async () => {
await init();
});
</script> </script>
<svelte:head> <svelte:head>
@ -181,103 +198,98 @@
{/each} {/each}
</div> </div>
<div class=" flex justify-end w-full mb-3"> {#if $user?.role === 'admin'}
<div class="flex space-x-2"> <div class=" flex justify-end w-full mb-3">
<input <div class="flex space-x-2">
id="prompts-import-input" <input
bind:this={promptsImportInputElement} id="prompts-import-input"
bind:files={importFiles} bind:this={promptsImportInputElement}
type="file" bind:files={importFiles}
accept=".json" type="file"
hidden accept=".json"
on:change={() => { hidden
console.log(importFiles); on:change={() => {
console.log(importFiles);
const reader = new FileReader(); const reader = new FileReader();
reader.onload = async (event) => { reader.onload = async (event) => {
const savedPrompts = JSON.parse(event.target.result); const savedPrompts = JSON.parse(event.target.result);
console.log(savedPrompts); console.log(savedPrompts);
for (const prompt of savedPrompts) { for (const prompt of savedPrompts) {
await createNewPrompt( await createNewPrompt(
localStorage.token, localStorage.token,
prompt.command.charAt(0) === '/' ? prompt.command.slice(1) : prompt.command, prompt.command.charAt(0) === '/' ? prompt.command.slice(1) : prompt.command,
prompt.title, prompt.title,
prompt.content prompt.content
).catch((error) => { ).catch((error) => {
toast.error(error); toast.error(error);
return null; return null;
}); });
} }
await prompts.set(await getPrompts(localStorage.token)); prompts = await getPromptList(localStorage.token);
}; await _prompts.set(await getPrompts(localStorage.token));
};
reader.readAsText(importFiles[0]); reader.readAsText(importFiles[0]);
}} }}
/> />
<button <button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition" class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={() => { on:click={() => {
promptsImportInputElement.click(); promptsImportInputElement.click();
}} }}
> >
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Prompts')}</div> <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Prompts')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
fill="currentColor" fill="currentColor"
class="w-4 h-4" class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => {
// promptsImportInputElement.click();
let blob = new Blob([JSON.stringify($prompts)], {
type: 'application/json'
});
saveAs(blob, `prompts-export-${Date.now()}.json`);
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Prompts')}</div>
<div class=" self-center">
<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="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
<!-- <button
on:click={() => {
loadDefaultPrompts();
}}
> >
dd <path
</button> --> fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => {
// promptsImportInputElement.click();
let blob = new Blob([JSON.stringify(prompts)], {
type: 'application/json'
});
saveAs(blob, `prompts-export-${Date.now()}.json`);
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Prompts')}</div>
<div class=" self-center">
<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="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
</div>
</div> </div>
</div> {/if}
{#if $config?.features.enable_community_sharing} {#if $config?.features.enable_community_sharing}
<div class=" my-16"> <div class=" my-16">

View File

@ -4,7 +4,7 @@
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { WEBUI_NAME, config, prompts, tools } from '$lib/stores'; import { WEBUI_NAME, config, prompts, tools as _tools, user } from '$lib/stores';
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts'; import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@ -45,8 +45,9 @@
let showDeleteConfirm = false; let showDeleteConfirm = false;
let tools = [];
let filteredItems = []; let filteredItems = [];
$: filteredItems = $tools.filter( $: filteredItems = tools.filter(
(t) => (t) =>
query === '' || query === '' ||
t.name.toLowerCase().includes(query.toLowerCase()) || t.name.toLowerCase().includes(query.toLowerCase()) ||
@ -118,7 +119,7 @@
if (res) { if (res) {
toast.success($i18n.t('Tool deleted successfully')); toast.success($i18n.t('Tool deleted successfully'));
tools.set(await getTools(localStorage.token)); _tools.set(await getTools(localStorage.token));
} }
}; };
@ -324,80 +325,82 @@
{/each} {/each}
</div> </div>
<div class=" flex justify-end w-full mb-2"> {#if $user?.role === 'admin'}
<div class="flex space-x-2"> <div class=" flex justify-end w-full mb-2">
<input <div class="flex space-x-2">
id="documents-import-input" <input
bind:this={toolsImportInputElement} id="documents-import-input"
bind:files={importFiles} bind:this={toolsImportInputElement}
type="file" bind:files={importFiles}
accept=".json" type="file"
hidden accept=".json"
on:change={() => { hidden
console.log(importFiles); on:change={() => {
showConfirm = true; console.log(importFiles);
}} showConfirm = true;
/> }}
/>
<button <button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition" class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={() => { on:click={() => {
toolsImportInputElement.click(); toolsImportInputElement.click();
}} }}
> >
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Tools')}</div> <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Tools')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
fill="currentColor" fill="currentColor"
class="w-4 h-4" class="w-4 h-4"
> >
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z" d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
</svg> </svg>
</div> </div>
</button> </button>
<button <button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition" class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => { on:click={async () => {
const _tools = await exportTools(localStorage.token).catch((error) => { const _tools = await exportTools(localStorage.token).catch((error) => {
toast.error(error); toast.error(error);
return null; return null;
});
if (_tools) {
let blob = new Blob([JSON.stringify(_tools)], {
type: 'application/json'
}); });
saveAs(blob, `tools-export-${Date.now()}.json`);
}
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Tools')}</div>
<div class=" self-center"> if (_tools) {
<svg let blob = new Blob([JSON.stringify(_tools)], {
xmlns="http://www.w3.org/2000/svg" type: 'application/json'
viewBox="0 0 16 16" });
fill="currentColor" saveAs(blob, `tools-export-${Date.now()}.json`);
class="w-4 h-4" }
> }}
<path >
fill-rule="evenodd" <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Tools')}</div>
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd" <div class=" self-center">
/> <svg
</svg> xmlns="http://www.w3.org/2000/svg"
</div> viewBox="0 0 16 16"
</button> fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
</div>
</div> </div>
</div> {/if}
{#if $config?.features.enable_community_sharing} {#if $config?.features.enable_community_sharing}
<div class=" my-16"> <div class=" my-16">

View File

@ -1,19 +1,5 @@
<script> <script>
import { onMount } from 'svelte';
import { prompts } from '$lib/stores';
import { getPrompts } from '$lib/apis/prompts';
import Prompts from '$lib/components/workspace/Prompts.svelte'; import Prompts from '$lib/components/workspace/Prompts.svelte';
onMount(async () => {
await Promise.all([
(async () => {
prompts.set(await getPrompts(localStorage.token));
})()
]);
});
</script> </script>
{#if $prompts !== null} <Prompts />
<Prompts />
{/if}

View File

@ -1,19 +1,7 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { tools } from '$lib/stores';
import { getTools } from '$lib/apis/tools';
import Tools from '$lib/components/workspace/Tools.svelte'; import Tools from '$lib/components/workspace/Tools.svelte';
onMount(async () => {
await Promise.all([
(async () => {
tools.set(await getTools(localStorage.token));
})()
]);
});
</script> </script>
{#if $tools !== null} <Tools />
<Tools />
{/if}