Merge pull request #548 from ollama-webui/custom-interface

feat: custom interface support
This commit is contained in:
Timothy Jaeryang Baek 2024-01-22 21:55:31 -08:00 committed by GitHub
commit e2ec19b472
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 231 additions and 73 deletions

View File

@ -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,
}

View File

@ -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,
)

View File

@ -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
####################################

View File

@ -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;
};

View File

@ -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}

View File

@ -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"

View File

@ -13,7 +13,7 @@
} else if (size === 'sm') {
return 'w-[30rem]';
} else {
return 'w-[40rem]';
return 'w-[42rem]';
}
};

View File

@ -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}

View File

@ -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}