diff --git a/backend/open_webui/apps/webui/models/projects.py b/backend/open_webui/apps/webui/models/projects.py index 5b9f07090..4debbbe28 100644 --- a/backend/open_webui/apps/webui/models/projects.py +++ b/backend/open_webui/apps/webui/models/projects.py @@ -2,6 +2,7 @@ import json import logging import time from typing import Optional +import uuid from open_webui.apps.webui.internal.db import Base, get_db from open_webui.env import SRC_LOG_LEVELS @@ -65,7 +66,6 @@ class ProjectResponse(BaseModel): class ProjectForm(BaseModel): - id: str name: str description: str data: Optional[dict] = None @@ -79,6 +79,7 @@ class ProjectTable: project = ProjectModel( **{ **form_data.model_dump(), + "id": str(uuid.uuid4()), "user_id": user_id, "created_at": int(time.time()), "updated_at": int(time.time()), diff --git a/backend/open_webui/apps/webui/routers/projects.py b/backend/open_webui/apps/webui/routers/projects.py index ed47b41b2..493bde99e 100644 --- a/backend/open_webui/apps/webui/routers/projects.py +++ b/backend/open_webui/apps/webui/routers/projects.py @@ -46,21 +46,32 @@ async def get_projects(id: Optional[str] = None, user=Depends(get_verified_user) @router.post("/create", response_model=Optional[ProjectResponse]) async def create_new_project(form_data: ProjectForm, user=Depends(get_admin_user)): - project = Projects.get_project_by_id(form_data.id) - if project is None: - project = Projects.insert_new_project(user.id, form_data) + project = Projects.insert_new_project(user.id, form_data) - if project: - return project - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.FILE_EXISTS, - ) + if project: + return project else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.ID_TAKEN, + detail=ERROR_MESSAGES.FILE_EXISTS, + ) + + +############################ +# GetProjectById +############################ + + +@router.get("/{id}", response_model=Optional[ProjectResponse]) +async def get_projects(id: str, user=Depends(get_verified_user)): + project = Projects.get_project_by_id(id=id) + + if project: + return project + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, ) @@ -69,8 +80,9 @@ async def create_new_project(form_data: ProjectForm, user=Depends(get_admin_user ############################ -@router.post("/update", response_model=Optional[ProjectResponse]) +@router.post("/{id}/update", response_model=Optional[ProjectResponse]) async def update_project_by_id( + id: str, form_data: ProjectForm, user=Depends(get_admin_user), ): @@ -89,7 +101,7 @@ async def update_project_by_id( ############################ -@router.delete("/delete", response_model=bool) +@router.delete("/{id}/delete", response_model=bool) async def delete_project_by_id(id: str, user=Depends(get_admin_user)): result = Projects.delete_project_by_id(id=id) return result diff --git a/src/lib/apis/projects/index.ts b/src/lib/apis/projects/index.ts index 8fad3ffd8..af448d1bb 100644 --- a/src/lib/apis/projects/index.ts +++ b/src/lib/apis/projects/index.ts @@ -1,6 +1,6 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; -export const createNewProject = async (token: string, id: string, name: string) => { +export const createNewProject = async (token: string, name: string, description: string) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/projects/create`, { @@ -11,8 +11,8 @@ export const createNewProject = async (token: string, id: string, name: string) authorization: `Bearer ${token}` }, body: JSON.stringify({ - id: id, - name: name + name: name, + description: description }) }) .then(async (res) => { diff --git a/src/lib/components/admin/Settings/WebSearch.svelte b/src/lib/components/admin/Settings/WebSearch.svelte index 0a0c2eb16..ddda39b10 100644 --- a/src/lib/components/admin/Settings/WebSearch.svelte +++ b/src/lib/components/admin/Settings/WebSearch.svelte @@ -2,7 +2,7 @@ import { getRAGConfig, updateRAGConfig } from '$lib/apis/retrieval'; import Switch from '$lib/components/common/Switch.svelte'; - import { documents, models } from '$lib/stores'; + import { models } from '$lib/stores'; import { onMount, getContext } from 'svelte'; import { toast } from 'svelte-sonner'; import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; diff --git a/src/lib/components/chat/MessageInput/Commands.svelte b/src/lib/components/chat/MessageInput/Commands.svelte index bb153f647..f9c0cc25f 100644 --- a/src/lib/components/chat/MessageInput/Commands.svelte +++ b/src/lib/components/chat/MessageInput/Commands.svelte @@ -114,7 +114,7 @@ files = [ ...files, { - type: e?.detail?.type ?? 'file', + type: e?.detail?.meta?.legacy ? 'file' : 'project', ...e.detail, status: 'processed' } diff --git a/src/lib/components/workspace/Projects.svelte b/src/lib/components/workspace/Projects.svelte index 1a37c8282..0eda768f4 100644 --- a/src/lib/components/workspace/Projects.svelte +++ b/src/lib/components/workspace/Projects.svelte @@ -111,12 +111,13 @@ </div> </div> -<hr class=" dark:border-gray-850 my-2.5" /> +<hr class=" border-gray-50 dark:border-gray-850 my-2.5" /> <div class="my-3 mb-5 grid md:grid-cols-2 gap-2"> {#each filteredProjects as project} + {JSON.stringify(project)} <button - class=" flex space-x-4 cursor-pointer text-left w-full px-4 py-3 border dark:border-gray-850 dark:hover:bg-gray-850 rounded-xl" + class=" flex space-x-4 cursor-pointer text-left w-full px-4 py-3 border border-gray-50 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 rounded-xl" > <div class=" w-full"> <div class="flex items-center justify-between -mt-1"> @@ -147,7 +148,7 @@ </div> {:else} <div - class="bg-green-500/20 text-green-700 dark:text-green-200 rounded uppercase text-xs px-1" + class="bg-green-500/20 text-green-700 dark:text-green-200 rounded uppercase text-xs font-bold px-1" > {$i18n.t('Project')} </div> diff --git a/src/lib/components/workspace/Projects/CreateProject.svelte b/src/lib/components/workspace/Projects/CreateProject.svelte index e69de29bb..75dda32a5 100644 --- a/src/lib/components/workspace/Projects/CreateProject.svelte +++ b/src/lib/components/workspace/Projects/CreateProject.svelte @@ -0,0 +1,138 @@ +<script> + import { goto } from '$app/navigation'; + import { getContext } from 'svelte'; + const i18n = getContext('i18n'); + + import { createNewProject, getProjects } from '$lib/apis/projects'; + import { toast } from 'svelte-sonner'; + import { projects } from '$lib/stores'; + + let loading = false; + + let name = ''; + let description = ''; + + const submitHandler = async () => { + loading = true; + + const res = await createNewProject(localStorage.token, name, description).catch((e) => { + toast.error(e); + }); + + if (res) { + toast.success($i18n.t('Project created successfully.')); + projects.set(await getProjects(localStorage.token)); + goto(`/workspace/projects/${res.id}`); + } + + loading = false; + }; +</script> + +<div class="w-full max-h-full"> + <button + class="flex space-x-1" + on:click={() => { + goto('/workspace/projects'); + }} + > + <div class=" self-center"> + <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="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div> + </button> + + <form + class="flex flex-col max-w-lg mx-auto mt-10 mb-10" + on:submit|preventDefault={() => { + submitHandler(); + }} + > + <div class=" w-full flex flex-col justify-center"> + <div class=" text-2xl font-medium font-primary mb-2.5">Create a project</div> + + <div class="w-full flex flex-col gap-2.5"> + <div class="w-full"> + <div class=" text-sm mb-2">What are you working on?</div> + + <div class="w-full mt-1"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + bind:value={name} + placeholder={`Name your project`} + required + /> + </div> + </div> + + <div> + <div class="text-sm mb-2">What are you trying to achieve?</div> + + <div class=" w-full mt-1"> + <textarea + class="w-full resize-none rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + rows="4" + bind:value={description} + placeholder={`Describe your project, its goals, and objectives`} + required + /> + </div> + </div> + </div> + </div> + + <div class="flex justify-end mt-2"> + <div> + <button + class=" text-sm px-4 py-2 transition rounded-lg {loading + ? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800' + : ' bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800'} flex" + type="submit" + disabled={loading} + > + <div class=" self-center font-medium">{$i18n.t('Create Project')}</div> + + {#if loading} + <div class="ml-1.5 self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style><path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /><path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /></svg + > + </div> + {/if} + </button> + </div> + </div> + </form> +</div> diff --git a/src/lib/components/workspace/Projects/EditProject.svelte b/src/lib/components/workspace/Projects/Project.svelte similarity index 100% rename from src/lib/components/workspace/Projects/EditProject.svelte rename to src/lib/components/workspace/Projects/Project.svelte diff --git a/src/routes/(app)/workspace/+layout.svelte b/src/routes/(app)/workspace/+layout.svelte index 6f69cffec..a3a91133e 100644 --- a/src/routes/(app)/workspace/+layout.svelte +++ b/src/routes/(app)/workspace/+layout.svelte @@ -99,7 +99,7 @@ </div> </div> - <hr class=" my-2 dark:border-gray-850" /> + <hr class=" my-2 border-gray-100 dark:border-gray-850" /> <div class=" py-1 px-5 flex-1 max-h-full overflow-y-auto"> <slot /> diff --git a/src/routes/(app)/workspace/projects/[id]/+page.svelte b/src/routes/(app)/workspace/projects/[id]/+page.svelte new file mode 100644 index 000000000..d92e7ff5f --- /dev/null +++ b/src/routes/(app)/workspace/projects/[id]/+page.svelte @@ -0,0 +1,7 @@ +<script> + import { page } from '$app/stores'; + + import Project from '$lib/components/workspace/Projects/Project.svelte'; +</script> + +<Project /> diff --git a/src/routes/(app)/workspace/projects/edit/+page.svelte b/src/routes/(app)/workspace/projects/edit/+page.svelte deleted file mode 100644 index 121a5dfcf..000000000 --- a/src/routes/(app)/workspace/projects/edit/+page.svelte +++ /dev/null @@ -1,5 +0,0 @@ -<script> - import EditProject from '$lib/components/workspace/Projects/EditProject.svelte'; -</script> - -<EditProject />