Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
open-webui
Commits
4b6b33b0
Commit
4b6b33b0
authored
Jun 16, 2024
by
Timothy J. Baek
Browse files
feat: user_location
parent
8e62c361
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
275 additions
and
19 deletions
+275
-19
backend/apps/webui/internal/migrations/013_add_user_info.py
backend/apps/webui/internal/migrations/013_add_user_info.py
+48
-0
backend/apps/webui/models/users.py
backend/apps/webui/models/users.py
+2
-0
backend/apps/webui/routers/users.py
backend/apps/webui/routers/users.py
+46
-0
backend/main.py
backend/main.py
+13
-3
backend/utils/task.py
backend/utils/task.py
+6
-6
src/lib/apis/users/index.ts
src/lib/apis/users/index.ts
+70
-0
src/lib/components/chat/Chat.svelte
src/lib/components/chat/Chat.svelte
+16
-3
src/lib/components/chat/Settings/Interface.svelte
src/lib/components/chat/Settings/Interface.svelte
+47
-3
src/lib/utils/index.ts
src/lib/utils/index.ts
+27
-4
No files found.
backend/apps/webui/internal/migrations/013_add_user_info.py
0 → 100644
View file @
4b6b33b0
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from
contextlib
import
suppress
import
peewee
as
pw
from
peewee_migrate
import
Migrator
with
suppress
(
ImportError
):
import
playhouse.postgres_ext
as
pw_pext
def
migrate
(
migrator
:
Migrator
,
database
:
pw
.
Database
,
*
,
fake
=
False
):
"""Write your migrations here."""
# Adding fields info to the 'user' table
migrator
.
add_fields
(
"user"
,
info
=
pw
.
TextField
(
null
=
True
))
def
rollback
(
migrator
:
Migrator
,
database
:
pw
.
Database
,
*
,
fake
=
False
):
"""Write your rollback migrations here."""
# Remove the settings field
migrator
.
remove_fields
(
"user"
,
"info"
)
backend/apps/webui/models/users.py
View file @
4b6b33b0
...
@@ -26,6 +26,7 @@ class User(Model):
...
@@ -26,6 +26,7 @@ class User(Model):
api_key
=
CharField
(
null
=
True
,
unique
=
True
)
api_key
=
CharField
(
null
=
True
,
unique
=
True
)
settings
=
JSONField
(
null
=
True
)
settings
=
JSONField
(
null
=
True
)
info
=
JSONField
(
null
=
True
)
class
Meta
:
class
Meta
:
database
=
DB
database
=
DB
...
@@ -50,6 +51,7 @@ class UserModel(BaseModel):
...
@@ -50,6 +51,7 @@ class UserModel(BaseModel):
api_key
:
Optional
[
str
]
=
None
api_key
:
Optional
[
str
]
=
None
settings
:
Optional
[
UserSettings
]
=
None
settings
:
Optional
[
UserSettings
]
=
None
info
:
Optional
[
dict
]
=
None
####################
####################
...
...
backend/apps/webui/routers/users.py
View file @
4b6b33b0
...
@@ -115,6 +115,52 @@ async def update_user_settings_by_session_user(
...
@@ -115,6 +115,52 @@ async def update_user_settings_by_session_user(
)
)
############################
# GetUserInfoBySessionUser
############################
@
router
.
get
(
"/user/info"
,
response_model
=
Optional
[
dict
])
async
def
get_user_info_by_session_user
(
user
=
Depends
(
get_verified_user
)):
user
=
Users
.
get_user_by_id
(
user
.
id
)
if
user
:
return
user
.
info
else
:
raise
HTTPException
(
status_code
=
status
.
HTTP_400_BAD_REQUEST
,
detail
=
ERROR_MESSAGES
.
USER_NOT_FOUND
,
)
############################
# UpdateUserInfoBySessionUser
############################
@
router
.
post
(
"/user/info/update"
,
response_model
=
Optional
[
dict
])
async
def
update_user_settings_by_session_user
(
form_data
:
dict
,
user
=
Depends
(
get_verified_user
)
):
user
=
Users
.
get_user_by_id
(
user
.
id
)
if
user
:
if
user
.
info
is
None
:
user
.
info
=
{}
user
=
Users
.
update_user_by_id
(
user
.
id
,
{
"info"
:
{
**
user
.
info
,
**
form_data
}})
if
user
:
return
user
.
info
else
:
raise
HTTPException
(
status_code
=
status
.
HTTP_400_BAD_REQUEST
,
detail
=
ERROR_MESSAGES
.
USER_NOT_FOUND
,
)
else
:
raise
HTTPException
(
status_code
=
status
.
HTTP_400_BAD_REQUEST
,
detail
=
ERROR_MESSAGES
.
USER_NOT_FOUND
,
)
############################
############################
# GetUserById
# GetUserById
############################
############################
...
...
backend/main.py
View file @
4b6b33b0
...
@@ -764,7 +764,12 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
...
@@ -764,7 +764,12 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
template
=
app
.
state
.
config
.
TITLE_GENERATION_PROMPT_TEMPLATE
template
=
app
.
state
.
config
.
TITLE_GENERATION_PROMPT_TEMPLATE
content
=
title_generation_template
(
content
=
title_generation_template
(
template
,
form_data
[
"prompt"
],
user
.
model_dump
()
template
,
form_data
[
"prompt"
],
{
"name"
:
user
.
name
,
"location"
:
user
.
info
.
get
(
"location"
)
if
user
.
info
else
None
,
},
)
)
payload
=
{
payload
=
{
...
@@ -830,7 +835,7 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
...
@@ -830,7 +835,7 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
template
=
app
.
state
.
config
.
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
template
=
app
.
state
.
config
.
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
content
=
search_query_generation_template
(
content
=
search_query_generation_template
(
template
,
form_data
[
"prompt"
],
user
.
model_dump
()
template
,
form_data
[
"prompt"
],
{
"name"
:
user
.
name
}
)
)
payload
=
{
payload
=
{
...
@@ -893,7 +898,12 @@ Message: """{{prompt}}"""
...
@@ -893,7 +898,12 @@ Message: """{{prompt}}"""
'''
'''
content
=
title_generation_template
(
content
=
title_generation_template
(
template
,
form_data
[
"prompt"
],
user
.
model_dump
()
template
,
form_data
[
"prompt"
],
{
"name"
:
user
.
name
,
"location"
:
user
.
info
.
get
(
"location"
)
if
user
.
info
else
None
,
},
)
)
payload
=
{
payload
=
{
...
...
backend/utils/task.py
View file @
4b6b33b0
...
@@ -6,7 +6,7 @@ from typing import Optional
...
@@ -6,7 +6,7 @@ from typing import Optional
def
prompt_template
(
def
prompt_template
(
template
:
str
,
user_name
:
str
=
None
,
current
_location
:
str
=
None
template
:
str
,
user_name
:
str
=
None
,
user
_location
:
str
=
None
)
->
str
:
)
->
str
:
# Get the current date
# Get the current date
current_date
=
datetime
.
now
()
current_date
=
datetime
.
now
()
...
@@ -25,9 +25,9 @@ def prompt_template(
...
@@ -25,9 +25,9 @@ def prompt_template(
# Replace {{USER_NAME}} in the template with the user's name
# Replace {{USER_NAME}} in the template with the user's name
template
=
template
.
replace
(
"{{USER_NAME}}"
,
user_name
)
template
=
template
.
replace
(
"{{USER_NAME}}"
,
user_name
)
if
current
_location
:
if
user
_location
:
# Replace {{
CURRENT
_LOCATION}} in the template with the current location
# Replace {{
USER
_LOCATION}} in the template with the current location
template
=
template
.
replace
(
"{{
CURRENT
_LOCATION}}"
,
current
_location
)
template
=
template
.
replace
(
"{{
USER
_LOCATION}}"
,
user
_location
)
return
template
return
template
...
@@ -65,7 +65,7 @@ def title_generation_template(
...
@@ -65,7 +65,7 @@ def title_generation_template(
template
=
prompt_template
(
template
=
prompt_template
(
template
,
template
,
**
(
**
(
{
"user_name"
:
user
.
get
(
"name"
),
"
current
_location"
:
user
.
get
(
"location"
)}
{
"user_name"
:
user
.
get
(
"name"
),
"
user
_location"
:
user
.
get
(
"location"
)}
if
user
if
user
else
{}
else
{}
),
),
...
@@ -108,7 +108,7 @@ def search_query_generation_template(
...
@@ -108,7 +108,7 @@ def search_query_generation_template(
template
=
prompt_template
(
template
=
prompt_template
(
template
,
template
,
**
(
**
(
{
"user_name"
:
user
.
get
(
"name"
),
"
current
_location"
:
user
.
get
(
"location"
)}
{
"user_name"
:
user
.
get
(
"name"
),
"
user
_location"
:
user
.
get
(
"location"
)}
if
user
if
user
else
{}
else
{}
),
),
...
...
src/lib/apis/users/index.ts
View file @
4b6b33b0
import
{
WEBUI_API_BASE_URL
}
from
'
$lib/constants
'
;
import
{
WEBUI_API_BASE_URL
}
from
'
$lib/constants
'
;
import
{
getUserPosition
}
from
'
$lib/utils
'
;
export
const
getUserPermissions
=
async
(
token
:
string
)
=>
{
export
const
getUserPermissions
=
async
(
token
:
string
)
=>
{
let
error
=
null
;
let
error
=
null
;
...
@@ -198,6 +199,75 @@ export const getUserById = async (token: string, userId: string) => {
...
@@ -198,6 +199,75 @@ export const getUserById = async (token: string, userId: string) => {
return
res
;
return
res
;
};
};
export
const
getUserInfo
=
async
(
token
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/users/user/info`
,
{
method
:
'
GET
'
,
headers
:
{
'
Content-Type
'
:
'
application/json
'
,
Authorization
:
`Bearer
${
token
}
`
}
})
.
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
;
};
export
const
updateUserInfo
=
async
(
token
:
string
,
info
:
object
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/users/user/info/update`
,
{
method
:
'
POST
'
,
headers
:
{
'
Content-Type
'
:
'
application/json
'
,
Authorization
:
`Bearer
${
token
}
`
},
body
:
JSON
.
stringify
({
...
info
})
})
.
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
;
};
export
const
getAndUpdateUserLocation
=
async
(
token
:
string
)
=>
{
const
location
=
await
getUserPosition
().
catch
((
err
)
=>
{
throw
err
;
});
if
(
location
)
{
await
updateUserInfo
(
token
,
{
location
:
location
});
return
location
;
}
else
{
throw
new
Error
(
'
Failed to get user location
'
);
}
};
export
const
deleteUserById
=
async
(
token
:
string
,
userId
:
string
)
=>
{
export
const
deleteUserById
=
async
(
token
:
string
,
userId
:
string
)
=>
{
let
error
=
null
;
let
error
=
null
;
...
...
src/lib/components/chat/Chat.svelte
View file @
4b6b33b0
...
@@ -31,6 +31,7 @@
...
@@ -31,6 +31,7 @@
convertMessagesToHistory,
convertMessagesToHistory,
copyToClipboard,
copyToClipboard,
extractSentencesForAudio,
extractSentencesForAudio,
getUserPosition,
promptTemplate,
promptTemplate,
splitStream
splitStream
} from '$lib/utils';
} from '$lib/utils';
...
@@ -50,7 +51,7 @@
...
@@ -50,7 +51,7 @@
import { runWebSearch } from '$lib/apis/rag';
import { runWebSearch } from '$lib/apis/rag';
import { createOpenAITextStream } from '$lib/apis/streaming';
import { createOpenAITextStream } from '$lib/apis/streaming';
import { queryMemory } from '$lib/apis/memories';
import { queryMemory } from '$lib/apis/memories';
import { getUserSettings } from '$lib/apis/users';
import {
getAndUpdateUserLocation,
getUserSettings } from '$lib/apis/users';
import { chatCompleted, generateTitle, generateSearchQuery } from '$lib/apis';
import { chatCompleted, generateTitle, generateSearchQuery } from '$lib/apis';
import Banner from '../common/Banner.svelte';
import Banner from '../common/Banner.svelte';
...
@@ -533,7 +534,13 @@
...
@@ -533,7 +534,13 @@
$settings.system || (responseMessage?.userContext ?? null)
$settings.system || (responseMessage?.userContext ?? null)
? {
? {
role: 'system',
role: 'system',
content: `${promptTemplate($settings?.system ?? '', $user.name)}${
content: `${promptTemplate(
$settings?.system ?? '',
$user.name,
$settings?.userLocation
? await getAndUpdateUserLocation(localStorage.token)
: undefined
)}${
responseMessage?.userContext ?? null
responseMessage?.userContext ?? null
? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
: ''
: ''
...
@@ -871,7 +878,13 @@
...
@@ -871,7 +878,13 @@
$settings.system || (responseMessage?.userContext ?? null)
$settings.system || (responseMessage?.userContext ?? null)
? {
? {
role: 'system',
role: 'system',
content: `${promptTemplate($settings?.system ?? '', $user.name)}${
content: `${promptTemplate(
$settings?.system ?? '',
$user.name,
$settings?.userLocation
? await getAndUpdateUserLocation(localStorage.token)
: undefined
)}${
responseMessage?.userContext ?? null
responseMessage?.userContext ?? null
? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
: ''
: ''
...
...
src/lib/components/chat/Settings/Interface.svelte
View file @
4b6b33b0
...
@@ -5,6 +5,8 @@
...
@@ -5,6 +5,8 @@
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { toast } from 'svelte-sonner';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { updateUserInfo } from '$lib/apis/users';
import { getUserPosition } from '$lib/utils';
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
const i18n = getContext('i18n');
...
@@ -16,6 +18,7 @@
...
@@ -16,6 +18,7 @@
let responseAutoCopy = false;
let responseAutoCopy = false;
let widescreenMode = false;
let widescreenMode = false;
let splitLargeChunks = false;
let splitLargeChunks = false;
let userLocation = false;
// Interface
// Interface
let defaultModelId = '';
let defaultModelId = '';
...
@@ -51,6 +54,26 @@
...
@@ -51,6 +54,26 @@
saveSettings({ showEmojiInCall: showEmojiInCall });
saveSettings({ showEmojiInCall: showEmojiInCall });
};
};
const toggleUserLocation = async () => {
userLocation = !userLocation;
if (userLocation) {
const position = await getUserPosition().catch((error) => {
toast.error(error.message);
return null;
});
if (position) {
await updateUserInfo(localStorage.token, { location: position });
toast.success('User location successfully retrieved.');
} else {
userLocation = false;
}
}
saveSettings({ userLocation });
};
const toggleTitleAutoGenerate = async () => {
const toggleTitleAutoGenerate = async () => {
titleAutoGenerate = !titleAutoGenerate;
titleAutoGenerate = !titleAutoGenerate;
saveSettings({
saveSettings({
...
@@ -106,6 +129,7 @@
...
@@ -106,6 +129,7 @@
widescreenMode = $settings.widescreenMode ?? false;
widescreenMode = $settings.widescreenMode ?? false;
splitLargeChunks = $settings.splitLargeChunks ?? false;
splitLargeChunks = $settings.splitLargeChunks ?? false;
chatDirection = $settings.chatDirection ?? 'LTR';
chatDirection = $settings.chatDirection ?? 'LTR';
userLocation = $settings.userLocation ?? false;
defaultModelId = ($settings?.models ?? ['']).at(0);
defaultModelId = ($settings?.models ?? ['']).at(0);
});
});
...
@@ -142,6 +166,26 @@
...
@@ -142,6 +166,26 @@
</div>
</div>
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Widescreen Mode')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
togglewidescreenMode();
}}
type="button"
>
{#if widescreenMode === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
</div>
<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('Title Auto-Generation')}</div>
<div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div>
...
@@ -186,16 +230,16 @@
...
@@ -186,16 +230,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('
Widescreen Mode
')}</div>
<div class=" self-center text-xs font-medium">{$i18n.t('
Allow User Location
')}</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={() => {
toggle
widescreenMode
();
toggle
UserLocation
();
}}
}}
type="button"
type="button"
>
>
{#if
widescreenMode
=== true}
{#if
userLocation
=== 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>
...
...
src/lib/utils/index.ts
View file @
4b6b33b0
...
@@ -302,6 +302,29 @@ export const getImportOrigin = (_chats) => {
...
@@ -302,6 +302,29 @@ export const getImportOrigin = (_chats) => {
return
'
webui
'
;
return
'
webui
'
;
};
};
export
const
getUserPosition
=
async
(
raw
=
false
)
=>
{
// Get the user's location using the Geolocation API
const
position
=
await
new
Promise
((
resolve
,
reject
)
=>
{
navigator
.
geolocation
.
getCurrentPosition
(
resolve
,
reject
);
}).
catch
((
error
)
=>
{
console
.
error
(
'
Error getting user location:
'
,
error
);
throw
error
;
});
if
(
!
position
)
{
return
'
Location not available
'
;
}
// Extract the latitude and longitude from the position
const
{
latitude
,
longitude
}
=
position
.
coords
;
if
(
raw
)
{
return
{
latitude
,
longitude
};
}
else
{
return
`
${
latitude
.
toFixed
(
3
)}
,
${
longitude
.
toFixed
(
3
)}
(lat, long)`
;
}
};
const
convertOpenAIMessages
=
(
convo
)
=>
{
const
convertOpenAIMessages
=
(
convo
)
=>
{
// Parse OpenAI chat messages and create chat dictionary for creating new chats
// Parse OpenAI chat messages and create chat dictionary for creating new chats
const
mapping
=
convo
[
'
mapping
'
];
const
mapping
=
convo
[
'
mapping
'
];
...
@@ -474,7 +497,7 @@ export const blobToFile = (blob, fileName) => {
...
@@ -474,7 +497,7 @@ export const blobToFile = (blob, fileName) => {
export
const
promptTemplate
=
(
export
const
promptTemplate
=
(
template
:
string
,
template
:
string
,
user_name
?:
string
,
user_name
?:
string
,
current
_location
?:
string
user
_location
?:
string
):
string
=>
{
):
string
=>
{
// Get the current date
// Get the current date
const
currentDate
=
new
Date
();
const
currentDate
=
new
Date
();
...
@@ -509,9 +532,9 @@ export const promptTemplate = (
...
@@ -509,9 +532,9 @@ export const promptTemplate = (
template
=
template
.
replace
(
'
{{USER_NAME}}
'
,
user_name
);
template
=
template
.
replace
(
'
{{USER_NAME}}
'
,
user_name
);
}
}
if
(
current
_location
)
{
if
(
user
_location
)
{
// Replace {{
CURRENT
_LOCATION}} in the template with the current location
// Replace {{
USER
_LOCATION}} in the template with the current location
template
=
template
.
replace
(
'
{{
CURRENT
_LOCATION}}
'
,
current
_location
);
template
=
template
.
replace
(
'
{{
USER
_LOCATION}}
'
,
user
_location
);
}
}
return
template
;
return
template
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment