forked from open-webui/open-webui
Merge pull request #548 from ollama-webui/custom-interface
feat: custom interface support
This commit is contained in:
commit
e2ec19b472
9 changed files with 231 additions and 73 deletions
|
@ -11,14 +11,15 @@ from apps.web.routers import (
|
|||
configs,
|
||||
utils,
|
||||
)
|
||||
from config import WEBUI_VERSION, WEBUI_AUTH
|
||||
from config import WEBUI_VERSION, WEBUI_AUTH, DEFAULT_MODELS, DEFAULT_PROMPT_SUGGESTIONS
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
origins = ["*"]
|
||||
|
||||
app.state.ENABLE_SIGNUP = True
|
||||
app.state.DEFAULT_MODELS = None
|
||||
app.state.DEFAULT_MODELS = DEFAULT_MODELS
|
||||
app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
@ -46,4 +47,5 @@ async def get_status():
|
|||
"version": WEBUI_VERSION,
|
||||
"auth": WEBUI_AUTH,
|
||||
"default_models": app.state.DEFAULT_MODELS,
|
||||
"default_prompt_suggestions": app.state.DEFAULT_PROMPT_SUGGESTIONS,
|
||||
}
|
||||
|
|
|
@ -21,15 +21,24 @@ class SetDefaultModelsForm(BaseModel):
|
|||
models: str
|
||||
|
||||
|
||||
class PromptSuggestion(BaseModel):
|
||||
title: List[str]
|
||||
content: str
|
||||
|
||||
|
||||
class SetDefaultSuggestionsForm(BaseModel):
|
||||
suggestions: List[PromptSuggestion]
|
||||
|
||||
|
||||
############################
|
||||
# SetDefaultModels
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/default/models", response_model=str)
|
||||
async def set_global_default_models(request: Request,
|
||||
form_data: SetDefaultModelsForm,
|
||||
user=Depends(get_current_user)):
|
||||
async def set_global_default_models(
|
||||
request: Request, form_data: SetDefaultModelsForm, user=Depends(get_current_user)
|
||||
):
|
||||
if user.role == "admin":
|
||||
request.app.state.DEFAULT_MODELS = form_data.models
|
||||
return request.app.state.DEFAULT_MODELS
|
||||
|
@ -38,3 +47,20 @@ async def set_global_default_models(request: Request,
|
|||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/default/suggestions", response_model=List[PromptSuggestion])
|
||||
async def set_global_default_suggestions(
|
||||
request: Request,
|
||||
form_data: SetDefaultSuggestionsForm,
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
if user.role == "admin":
|
||||
data = form_data.model_dump()
|
||||
request.app.state.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"]
|
||||
return request.app.state.DEFAULT_PROMPT_SUGGESTIONS
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
|
|
@ -54,6 +54,34 @@ OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
|
|||
if OPENAI_API_BASE_URL == "":
|
||||
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
|
||||
|
||||
|
||||
####################################
|
||||
# WEBUI
|
||||
####################################
|
||||
|
||||
DEFAULT_MODELS = os.environ.get("DEFAULT_MODELS", None)
|
||||
DEFAULT_PROMPT_SUGGESTIONS = os.environ.get(
|
||||
"DEFAULT_PROMPT_SUGGESTIONS",
|
||||
[
|
||||
{
|
||||
"title": ["Help me study", "vocabulary for a college entrance exam"],
|
||||
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.",
|
||||
},
|
||||
{
|
||||
"title": ["Give me ideas", "for what to do with my kids' art"],
|
||||
"content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.",
|
||||
},
|
||||
{
|
||||
"title": ["Tell me a fun fact", "about the Roman Empire"],
|
||||
"content": "Tell me a random fun fact about the Roman Empire",
|
||||
},
|
||||
{
|
||||
"title": ["Show me a code snippet", "of a website's sticky header"],
|
||||
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
####################################
|
||||
# WEBUI_VERSION
|
||||
####################################
|
||||
|
|
|
@ -29,3 +29,33 @@ export const setDefaultModels = async (token: string, models: string) => {
|
|||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const setDefaultPromptSuggestions = async (token: string, promptSuggestions: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/default/suggestions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
suggestions: promptSuggestions
|
||||
})
|
||||
})
|
||||
.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;
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
}}
|
||||
>
|
||||
<div class="flex flex-col text-left self-center">
|
||||
{#if prompt.title}
|
||||
{#if prompt.title && prompt.title[0] !== ''}
|
||||
<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div>
|
||||
<div class="text-sm text-gray-500">{prompt.title[1]}</div>
|
||||
{:else}
|
||||
|
|
|
@ -34,6 +34,8 @@
|
|||
updateOpenAIUrl
|
||||
} from '$lib/apis/openai';
|
||||
import { resetVectorDB } from '$lib/apis/rag';
|
||||
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
|
||||
import { getBackendConfig } from '$lib/apis';
|
||||
|
||||
export let show = false;
|
||||
|
||||
|
@ -99,6 +101,9 @@
|
|||
let OPENAI_API_KEY = '';
|
||||
let OPENAI_API_BASE_URL = '';
|
||||
|
||||
// Interface
|
||||
let promptSuggestions = [];
|
||||
|
||||
// Addons
|
||||
let titleAutoGenerate = true;
|
||||
let speechAutoSend = false;
|
||||
|
@ -191,6 +196,11 @@
|
|||
await models.set(await getModels());
|
||||
};
|
||||
|
||||
const updateInterfaceHandler = async () => {
|
||||
promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
|
||||
await config.set(await getBackendConfig());
|
||||
};
|
||||
|
||||
const toggleTheme = async () => {
|
||||
if (theme === 'dark') {
|
||||
theme = 'light';
|
||||
|
@ -577,6 +587,7 @@
|
|||
API_BASE_URL = await getOllamaAPIUrl(localStorage.token);
|
||||
OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token);
|
||||
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
|
||||
promptSuggestions = $config?.default_prompt_suggestions;
|
||||
}
|
||||
|
||||
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||
|
@ -745,6 +756,32 @@
|
|||
</div>
|
||||
<div class=" self-center">External</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'interface'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'interface';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<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="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">Interface</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
|
@ -797,34 +834,6 @@
|
|||
<div class=" self-center">Chats</div>
|
||||
</button>
|
||||
|
||||
{#if !$config || ($config && !$config.auth)}
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'auth'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'auth';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12.516 2.17a.75.75 0 00-1.032 0 11.209 11.209 0 01-7.877 3.08.75.75 0 00-.722.515A12.74 12.74 0 002.25 9.75c0 5.942 4.064 10.933 9.563 12.348a.749.749 0 00.374 0c5.499-1.415 9.563-6.406 9.563-12.348 0-1.39-.223-2.73-.635-3.985a.75.75 0 00-.722-.516l-.143.001c-2.996 0-5.717-1.17-7.734-3.08zm3.094 8.016a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">Authentication</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'account'
|
||||
|
@ -877,7 +886,7 @@
|
|||
<div class=" self-center">About</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 md:min-h-[340px]">
|
||||
<div class="flex-1 md:min-h-[380px]">
|
||||
{#if selectedTab === 'general'}
|
||||
<div class="flex flex-col space-y-3">
|
||||
<div>
|
||||
|
@ -1048,7 +1057,7 @@
|
|||
</div>
|
||||
{:else if selectedTab === 'advanced'}
|
||||
<div class="flex flex-col h-full justify-between text-sm">
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-72">
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||
<div class=" text-sm font-medium">Parameters</div>
|
||||
|
||||
<Advanced bind:options />
|
||||
|
@ -1483,6 +1492,103 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{:else if selectedTab === 'interface'}
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
updateInterfaceHandler();
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||
<div class="flex w-full justify-between mb-2">
|
||||
<div class=" self-center text-sm font-semibold">Default Prompt Suggestions</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
|
||||
promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-1">
|
||||
{#each promptSuggestions as prompt, promptIdx}
|
||||
<div class=" flex border dark:border-gray-600 rounded-lg">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex border-b dark:border-gray-600 w-full">
|
||||
<input
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
|
||||
placeholder="Title (e.g. Tell me a fun fact)"
|
||||
bind:value={prompt.title[0]}
|
||||
/>
|
||||
|
||||
<input
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
|
||||
placeholder="Subtitle (e.g. about the Roman Empire)"
|
||||
bind:value={prompt.title[1]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
|
||||
placeholder="Prompt (e.g. Tell me a fun fact about the Roman Empire)"
|
||||
bind:value={prompt.content}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-2"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
promptSuggestions.splice(promptIdx, 1);
|
||||
promptSuggestions = promptSuggestions;
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if promptSuggestions.length > 0}
|
||||
<div class="text-xs text-left w-full mt-2">
|
||||
Adjusting these settings will apply changes universally to all users.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
} else if (size === 'sm') {
|
||||
return 'w-[30rem]';
|
||||
} else {
|
||||
return 'w-[40rem]';
|
||||
return 'w-[42rem]';
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -765,24 +765,7 @@
|
|||
bind:files
|
||||
bind:prompt
|
||||
bind:autoScroll
|
||||
suggestionPrompts={selectedModelfile?.suggestionPrompts ?? [
|
||||
{
|
||||
title: ['Help me study', 'vocabulary for a college entrance exam'],
|
||||
content: `Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.`
|
||||
},
|
||||
{
|
||||
title: ['Give me ideas', `for what to do with my kids' art`],
|
||||
content: `What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.`
|
||||
},
|
||||
{
|
||||
title: ['Tell me a fun fact', 'about the Roman Empire'],
|
||||
content: 'Tell me a random fun fact about the Roman Empire'
|
||||
},
|
||||
{
|
||||
title: ['Show me a code snippet', `of a website's sticky header`],
|
||||
content: `Show me a code snippet of a website's sticky header in CSS and JavaScript.`
|
||||
}
|
||||
]}
|
||||
suggestionPrompts={selectedModelfile?.suggestionPrompts ?? $config.default_prompt_suggestions}
|
||||
{messages}
|
||||
{submitPrompt}
|
||||
{stopResponse}
|
||||
|
|
|
@ -794,24 +794,7 @@
|
|||
bind:files
|
||||
bind:prompt
|
||||
bind:autoScroll
|
||||
suggestionPrompts={selectedModelfile?.suggestionPrompts ?? [
|
||||
{
|
||||
title: ['Help me study', 'vocabulary for a college entrance exam'],
|
||||
content: `Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.`
|
||||
},
|
||||
{
|
||||
title: ['Give me ideas', `for what to do with my kids' art`],
|
||||
content: `What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.`
|
||||
},
|
||||
{
|
||||
title: ['Tell me a fun fact', 'about the Roman Empire'],
|
||||
content: 'Tell me a random fun fact about the Roman Empire'
|
||||
},
|
||||
{
|
||||
title: ['Show me a code snippet', `of a website's sticky header`],
|
||||
content: `Show me a code snippet of a website's sticky header in CSS and JavaScript.`
|
||||
}
|
||||
]}
|
||||
suggestionPrompts={selectedModelfile?.suggestionPrompts ?? $config.default_prompt_suggestions}
|
||||
{messages}
|
||||
{submitPrompt}
|
||||
{stopResponse}
|
||||
|
|
Loading…
Reference in a new issue