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
4aab4609
Commit
4aab4609
authored
Jun 21, 2024
by
Jun Siang Cheah
Browse files
Merge remote-tracking branch 'upstream/dev' into feat/oauth
parents
4ff17acc
a2ea6b1b
Changes
133
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
974 additions
and
250 deletions
+974
-250
scripts/prepare-pyodide.js
scripts/prepare-pyodide.js
+57
-11
src/app.html
src/app.html
+6
-0
src/lib/apis/auths/index.ts
src/lib/apis/auths/index.ts
+4
-1
src/lib/apis/files/index.ts
src/lib/apis/files/index.ts
+183
-0
src/lib/apis/functions/index.ts
src/lib/apis/functions/index.ts
+193
-0
src/lib/apis/rag/index.ts
src/lib/apis/rag/index.ts
+31
-0
src/lib/apis/users/index.ts
src/lib/apis/users/index.ts
+70
-0
src/lib/components/admin/AddUserModal.svelte
src/lib/components/admin/AddUserModal.svelte
+6
-6
src/lib/components/admin/Settings/Documents.svelte
src/lib/components/admin/Settings/Documents.svelte
+3
-2
src/lib/components/admin/UserChatsModal.svelte
src/lib/components/admin/UserChatsModal.svelte
+59
-5
src/lib/components/chat/Chat.svelte
src/lib/components/chat/Chat.svelte
+71
-41
src/lib/components/chat/MessageInput.svelte
src/lib/components/chat/MessageInput.svelte
+81
-66
src/lib/components/chat/MessageInput/CallOverlay.svelte
src/lib/components/chat/MessageInput/CallOverlay.svelte
+120
-65
src/lib/components/chat/MessageInput/Documents.svelte
src/lib/components/chat/MessageInput/Documents.svelte
+6
-6
src/lib/components/chat/MessageInput/Models.svelte
src/lib/components/chat/MessageInput/Models.svelte
+8
-6
src/lib/components/chat/MessageInput/PromptCommands.svelte
src/lib/components/chat/MessageInput/PromptCommands.svelte
+9
-7
src/lib/components/chat/MessageInput/Suggestions.svelte
src/lib/components/chat/MessageInput/Suggestions.svelte
+1
-1
src/lib/components/chat/Messages.svelte
src/lib/components/chat/Messages.svelte
+37
-24
src/lib/components/chat/Messages/CodeBlock.svelte
src/lib/components/chat/Messages/CodeBlock.svelte
+11
-1
src/lib/components/chat/Messages/Placeholder.svelte
src/lib/components/chat/Messages/Placeholder.svelte
+18
-8
No files found.
scripts/prepare-pyodide.js
View file @
4aab4609
const
packages
=
[
'
micropip
'
,
'
packaging
'
,
'
requests
'
,
'
beautifulsoup4
'
,
'
numpy
'
,
...
...
@@ -11,20 +13,64 @@ const packages = [
];
import
{
loadPyodide
}
from
'
pyodide
'
;
import
{
writeFile
,
copyFile
,
readdir
}
from
'
fs/promises
'
;
import
{
writeFile
,
readFile
,
copyFile
,
readdir
,
rmdir
}
from
'
fs/promises
'
;
async
function
downloadPackages
()
{
console
.
log
(
'
Setting up pyodide + micropip
'
);
const
pyodide
=
await
loadPyodide
({
packageCacheDir
:
'
static/pyodide
'
});
await
pyodide
.
loadPackage
(
'
micropip
'
);
const
micropip
=
pyodide
.
pyimport
(
'
micropip
'
);
console
.
log
(
'
Downloading Pyodide packages:
'
,
packages
);
await
micropip
.
install
(
packages
);
console
.
log
(
'
Pyodide packages downloaded, freezing into lock file
'
);
const
lockFile
=
await
micropip
.
freeze
();
await
writeFile
(
'
static/pyodide/pyodide-lock.json
'
,
lockFile
);
let
pyodide
;
try
{
pyodide
=
await
loadPyodide
({
packageCacheDir
:
'
static/pyodide
'
});
}
catch
(
err
)
{
console
.
error
(
'
Failed to load Pyodide:
'
,
err
);
return
;
}
const
packageJson
=
JSON
.
parse
(
await
readFile
(
'
package.json
'
));
const
pyodideVersion
=
packageJson
.
dependencies
.
pyodide
.
replace
(
'
^
'
,
''
);
try
{
const
pyodidePackageJson
=
JSON
.
parse
(
await
readFile
(
'
static/pyodide/package.json
'
));
const
pyodidePackageVersion
=
pyodidePackageJson
.
version
.
replace
(
'
^
'
,
''
);
if
(
pyodideVersion
!==
pyodidePackageVersion
)
{
console
.
log
(
'
Pyodide version mismatch, removing static/pyodide directory
'
);
await
rmdir
(
'
static/pyodide
'
,
{
recursive
:
true
});
}
}
catch
(
e
)
{
console
.
log
(
'
Pyodide package not found, proceeding with download.
'
);
}
try
{
console
.
log
(
'
Loading micropip package
'
);
await
pyodide
.
loadPackage
(
'
micropip
'
);
const
micropip
=
pyodide
.
pyimport
(
'
micropip
'
);
console
.
log
(
'
Downloading Pyodide packages:
'
,
packages
);
try
{
for
(
const
pkg
of
packages
)
{
console
.
log
(
`Installing package:
${
pkg
}
`
);
await
micropip
.
install
(
pkg
);
}
}
catch
(
err
)
{
console
.
error
(
'
Package installation failed:
'
,
err
);
return
;
}
console
.
log
(
'
Pyodide packages downloaded, freezing into lock file
'
);
try
{
const
lockFile
=
await
micropip
.
freeze
();
await
writeFile
(
'
static/pyodide/pyodide-lock.json
'
,
lockFile
);
}
catch
(
err
)
{
console
.
error
(
'
Failed to write lock file:
'
,
err
);
}
}
catch
(
err
)
{
console
.
error
(
'
Failed to load or install micropip:
'
,
err
);
}
}
async
function
copyPyodide
()
{
...
...
src/app.html
View file @
4aab4609
...
...
@@ -13,6 +13,12 @@
href=
"/opensearch.xml"
/>
<script>
function
resizeIframe
(
obj
)
{
obj
.
style
.
height
=
obj
.
contentWindow
.
document
.
documentElement
.
scrollHeight
+
'
px
'
;
}
</script>
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
(()
=>
{
...
...
src/lib/apis/auths/index.ts
View file @
4aab4609
...
...
@@ -90,7 +90,8 @@ export const getSessionUser = async (token: string) => {
headers
:
{
'
Content-Type
'
:
'
application/json
'
,
Authorization
:
`Bearer
${
token
}
`
}
},
credentials
:
'
include
'
})
.
then
(
async
(
res
)
=>
{
if
(
!
res
.
ok
)
throw
await
res
.
json
();
...
...
@@ -117,6 +118,7 @@ export const userSignIn = async (email: string, password: string) => {
headers
:
{
'
Content-Type
'
:
'
application/json
'
},
credentials
:
'
include
'
,
body
:
JSON
.
stringify
({
email
:
email
,
password
:
password
...
...
@@ -153,6 +155,7 @@ export const userSignUp = async (
headers
:
{
'
Content-Type
'
:
'
application/json
'
},
credentials
:
'
include
'
,
body
:
JSON
.
stringify
({
name
:
name
,
email
:
email
,
...
...
src/lib/apis/files/index.ts
0 → 100644
View file @
4aab4609
import
{
WEBUI_API_BASE_URL
}
from
'
$lib/constants
'
;
export
const
uploadFile
=
async
(
token
:
string
,
file
:
File
)
=>
{
const
data
=
new
FormData
();
data
.
append
(
'
file
'
,
file
);
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/files/`
,
{
method
:
'
POST
'
,
headers
:
{
Accept
:
'
application/json
'
,
authorization
:
`Bearer
${
token
}
`
},
body
:
data
})
.
then
(
async
(
res
)
=>
{
if
(
!
res
.
ok
)
throw
await
res
.
json
();
return
res
.
json
();
})
.
catch
((
err
)
=>
{
error
=
err
.
detail
;
console
.
log
(
err
);
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
;
};
export
const
getFiles
=
async
(
token
:
string
=
''
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/files/`
,
{
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
getFileById
=
async
(
token
:
string
,
id
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/files/
${
id
}
`
,
{
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
getFileContentById
=
async
(
id
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/files/
${
id
}
/content`
,
{
method
:
'
GET
'
,
headers
:
{
Accept
:
'
application/json
'
},
credentials
:
'
include
'
})
.
then
(
async
(
res
)
=>
{
if
(
!
res
.
ok
)
throw
await
res
.
json
();
return
await
res
.
blob
();
})
.
catch
((
err
)
=>
{
error
=
err
.
detail
;
console
.
log
(
err
);
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
;
};
export
const
deleteFileById
=
async
(
token
:
string
,
id
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/files/
${
id
}
`
,
{
method
:
'
DELETE
'
,
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
deleteAllFiles
=
async
(
token
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/files/all`
,
{
method
:
'
DELETE
'
,
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
;
};
src/lib/apis/functions/index.ts
0 → 100644
View file @
4aab4609
import
{
WEBUI_API_BASE_URL
}
from
'
$lib/constants
'
;
export
const
createNewFunction
=
async
(
token
:
string
,
func
:
object
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/functions/create`
,
{
method
:
'
POST
'
,
headers
:
{
Accept
:
'
application/json
'
,
'
Content-Type
'
:
'
application/json
'
,
authorization
:
`Bearer
${
token
}
`
},
body
:
JSON
.
stringify
({
...
func
})
})
.
then
(
async
(
res
)
=>
{
if
(
!
res
.
ok
)
throw
await
res
.
json
();
return
res
.
json
();
})
.
catch
((
err
)
=>
{
error
=
err
.
detail
;
console
.
log
(
err
);
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
;
};
export
const
getFunctions
=
async
(
token
:
string
=
''
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/functions/`
,
{
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
exportFunctions
=
async
(
token
:
string
=
''
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/functions/export`
,
{
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
getFunctionById
=
async
(
token
:
string
,
id
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/functions/id/
${
id
}
`
,
{
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
updateFunctionById
=
async
(
token
:
string
,
id
:
string
,
func
:
object
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/functions/id/
${
id
}
/update`
,
{
method
:
'
POST
'
,
headers
:
{
Accept
:
'
application/json
'
,
'
Content-Type
'
:
'
application/json
'
,
authorization
:
`Bearer
${
token
}
`
},
body
:
JSON
.
stringify
({
...
func
})
})
.
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
deleteFunctionById
=
async
(
token
:
string
,
id
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/functions/id/
${
id
}
/delete`
,
{
method
:
'
DELETE
'
,
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
;
};
src/lib/apis/rag/index.ts
View file @
4aab4609
...
...
@@ -164,6 +164,37 @@ export const updateQuerySettings = async (token: string, settings: QuerySettings
return
res
;
};
export
const
processDocToVectorDB
=
async
(
token
:
string
,
file_id
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
RAG_API_BASE_URL
}
/process/doc`
,
{
method
:
'
POST
'
,
headers
:
{
Accept
:
'
application/json
'
,
'
Content-Type
'
:
'
application/json
'
,
authorization
:
`Bearer
${
token
}
`
},
body
:
JSON
.
stringify
({
file_id
:
file_id
})
})
.
then
(
async
(
res
)
=>
{
if
(
!
res
.
ok
)
throw
await
res
.
json
();
return
res
.
json
();
})
.
catch
((
err
)
=>
{
error
=
err
.
detail
;
console
.
log
(
err
);
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
;
};
export
const
uploadDocToVectorDB
=
async
(
token
:
string
,
collection_name
:
string
,
file
:
File
)
=>
{
const
data
=
new
FormData
();
data
.
append
(
'
file
'
,
file
);
...
...
src/lib/apis/users/index.ts
View file @
4aab4609
import
{
WEBUI_API_BASE_URL
}
from
'
$lib/constants
'
;
import
{
getUserPosition
}
from
'
$lib/utils
'
;
export
const
getUserPermissions
=
async
(
token
:
string
)
=>
{
let
error
=
null
;
...
...
@@ -198,6 +199,75 @@ export const getUserById = async (token: string, userId: string) => {
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
)
=>
{
let
error
=
null
;
...
...
src/lib/components/admin/AddUserModal.svelte
View file @
4aab4609
...
...
@@ -153,7 +153,7 @@
type="button"
on:click={() => {
tab = '';
}}>Form</button
}}>
{$i18n.t('
Form
')}
</button
>
<button
...
...
@@ -161,7 +161,7 @@
type="button"
on:click={() => {
tab = 'import';
}}>CSV Import</button
}}>
{$i18n.t('
CSV Import
')}
</button
>
</div>
<div class="px-1">
...
...
@@ -176,9 +176,9 @@
placeholder={$i18n.t('Enter Your Role')}
required
>
<option value="pending"> pending </option>
<option value="user"> user </option>
<option value="admin"> admin </option>
<option value="pending">
{$i18n.t('
pending
')}
</option>
<option value="user">
{$i18n.t('
user
')}
</option>
<option value="admin">
{$i18n.t('
admin
')}
</option>
</select>
</div>
</div>
...
...
@@ -262,7 +262,7 @@
class="underline dark:text-gray-200"
href="{WEBUI_BASE_URL}/static/user-import.csv"
>
Click here to download user import template file.
{$i18n.t('
Click here to download user import template file.
')}
</a>
</div>
</div>
...
...
src/lib/components/admin/Settings/Documents.svelte
View file @
4aab4609
<script lang="ts">
import { getDocs } from '$lib/apis/documents';
import { deleteAllFiles, deleteFileById } from '$lib/apis/files';
import {
getQuerySettings,
scanDocs,
...
...
@@ -217,8 +218,8 @@
<ResetUploadDirConfirmDialog
bind:show={showResetUploadDirConfirm}
on:confirm={() => {
const res =
resetUploadDir
(localStorage.token).catch((error) => {
on:confirm={
async
() => {
const res =
await deleteAllFiles
(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
...
...
src/lib/components/admin/UserChatsModal.svelte
View file @
4aab4609
...
...
@@ -31,6 +31,17 @@
}
})();
}
let sortKey = 'updated_at'; // default sort key
let sortOrder = 'desc'; // default sort order
function setSortKey(key) {
if (sortKey === key) {
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
} else {
sortKey = key;
sortOrder = 'asc';
}
}
</script>
<Modal size="lg" bind:show>
...
...
@@ -69,18 +80,56 @@
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800"
>
<tr>
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
<th scope="col" class="px-3 py-2 hidden md:flex"> {$i18n.t('Created at')} </th>
<th
scope="col"
class="px-3 py-2 cursor-pointer select-none"
on:click={() => setSortKey('title')}
>
{$i18n.t('Title')}
{#if sortKey === 'title'}
{sortOrder === 'asc' ? '▲' : '▼'}
{:else}
<span class="invisible">▲</span>
{/if}
</th>
<th
scope="col"
class="px-3 py-2 cursor-pointer select-none"
on:click={() => setSortKey('created_at')}
>
{$i18n.t('Created at')}
{#if sortKey === 'created_at'}
{sortOrder === 'asc' ? '▲' : '▼'}
{:else}
<span class="invisible">▲</span>
{/if}
</th>
<th
scope="col"
class="px-3 py-2 hidden md:flex cursor-pointer select-none"
on:click={() => setSortKey('updated_at')}
>
{$i18n.t('Updated at')}
{#if sortKey === 'updated_at'}
{sortOrder === 'asc' ? '▲' : '▼'}
{:else}
<span class="invisible">▲</span>
{/if}
</th>
<th scope="col" class="px-3 py-2 text-right" />
</tr>
</thead>
<tbody>
{#each chats as chat, idx}
{#each chats.sort((a, b) => {
if (a[sortKey] < b[sortKey]) return sortOrder === 'asc' ? -1 : 1;
if (a[sortKey] > b[sortKey]) return sortOrder === 'asc' ? 1 : -1;
return 0;
}) as chat, idx}
<tr
class="bg-transparent {idx !== chats.length - 1 &&
'border-b'} dark:bg-gray-900 dark:border-gray-850 text-xs"
>
<td class="px-3 py-1
w-2/3
">
<td class="px-3 py-1">
<a href="/s/{chat.id}" target="_blank">
<div class=" underline line-clamp-1">
{chat.title}
...
...
@@ -88,11 +137,16 @@
</a>
</td>
<td class=" px-3 py-1
hidden md:flex
h-[2.5rem]">
<td class=" px-3 py-1 h-[2.5rem]">
<div class="my-auto">
{dayjs(chat.created_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
</div>
</td>
<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
<div class="my-auto">
{dayjs(chat.updated_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
</div>
</td>
<td class="px-3 py-1 text-right">
<div class="flex justify-end w-full">
...
...
src/lib/components/chat/Chat.svelte
View file @
4aab4609
...
...
@@ -31,6 +31,7 @@
convertMessagesToHistory,
copyToClipboard,
extractSentencesForAudio,
getUserPosition,
promptTemplate,
splitStream
} from '$lib/utils';
...
...
@@ -50,7 +51,7 @@
import { runWebSearch } from '$lib/apis/rag';
import { createOpenAITextStream } from '$lib/apis/streaming';
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 Banner from '../common/Banner.svelte';
...
...
@@ -272,11 +273,14 @@
id: m.id,
role: m.role,
content: m.content,
info: m.info ? m.info : undefined,
timestamp: m.timestamp
})),
chat_id: $chatId
}).catch((error) => {
console.error(error);
toast.error(error);
messages.at(-1).error = { content: error };
return null;
});
...
...
@@ -321,9 +325,16 @@
} else if (messages.length != 0 && messages.at(-1).done != true) {
// Response not done
console.log('wait');
} else if (messages.length != 0 && messages.at(-1).error) {
// Error in response
toast.error(
$i18n.t(
`Oops! There was an error in the previous response. Please try again or contact admin.`
)
);
} else if (
files.length > 0 &&
files.filter((file) => file.
upload_
status
=
==
false
).length > 0
files.filter((file) => file.
type !== 'image' && file.
status
!
==
'processed'
).length > 0
) {
// Upload not done
toast.error(
...
...
@@ -533,7 +544,13 @@
$settings.system || (responseMessage?.userContext ?? null)
? {
role: 'system',
content: `${promptTemplate($settings?.system ?? '', $user.name)}${
content: `${promptTemplate(
$settings?.system ?? '',
$user.name,
$settings?.userLocation
? await getAndUpdateUserLocation(localStorage.token)
: undefined
)}${
responseMessage?.userContext ?? null
? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
: ''
...
...
@@ -578,23 +595,18 @@
}
});
let docs = [];
let files = [];
if (model?.info?.meta?.knowledge ?? false) {
doc
s = model.info.meta.knowledge;
file
s = model.info.meta.knowledge;
}
docs = [
...docs,
...messages
.filter((message) => message?.files ?? null)
.map((message) =>
message.files.filter((item) =>
['doc', 'collection', 'web_search_results'].includes(item.type)
)
)
.flat(1)
const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
files = [
...files,
...(lastUserMessage?.files?.filter((item) =>
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
) ?? [])
].filter(
// Remove duplicates
(item, index, array) =>
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
);
...
...
@@ -626,8 +638,8 @@
format: $settings.requestFormat ?? undefined,
keep_alive: $settings.keepAlive ?? undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
docs: doc
s.length > 0 ?
doc
s : undefined,
citations:
doc
s.length > 0,
files: file
s.length > 0 ?
file
s : undefined,
citations:
file
s.length > 0
? true : undefined
,
chat_id: $chatId
});
...
...
@@ -823,23 +835,18 @@
let _response = null;
const responseMessage = history.messages[responseMessageId];
let docs = [];
let files = [];
if (model?.info?.meta?.knowledge ?? false) {
doc
s = model.info.meta.knowledge;
file
s = model.info.meta.knowledge;
}
docs = [
...docs,
...messages
.filter((message) => message?.files ?? null)
.map((message) =>
message.files.filter((item) =>
['doc', 'collection', 'web_search_results'].includes(item.type)
)
)
.flat(1)
const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
files = [
...files,
...(lastUserMessage?.files?.filter((item) =>
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
) ?? [])
].filter(
// Remove duplicates
(item, index, array) =>
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
);
...
...
@@ -871,7 +878,13 @@
$settings.system || (responseMessage?.userContext ?? null)
? {
role: 'system',
content: `${promptTemplate($settings?.system ?? '', $user.name)}${
content: `${promptTemplate(
$settings?.system ?? '',
$user.name,
$settings?.userLocation
? await getAndUpdateUserLocation(localStorage.token)
: undefined
)}${
responseMessage?.userContext ?? null
? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
: ''
...
...
@@ -923,11 +936,12 @@
frequency_penalty: $settings?.params?.frequency_penalty ?? undefined,
max_tokens: $settings?.params?.max_tokens ?? undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
docs: docs.length > 0 ? docs : undefined,
citations: docs.length > 0,
files: files.length > 0 ? files : undefined,
citations: files.length > 0 ? true : undefined,
chat_id: $chatId
},
`${
OPENAI_AP
I_BASE_URL}`
`${
WEBU
I_BASE_URL}
/api
`
);
// Wait until history/message have been updated
...
...
@@ -1309,6 +1323,19 @@
? 'md:max-w-[calc(100%-260px)]'
: ''} w-full max-w-full flex flex-col"
>
{#if $settings?.backgroundImageUrl ?? null}
<div
class="absolute {$showSidebar
? 'md:max-w-[calc(100%-260px)] md:translate-x-[260px]'
: ''} top-0 left-0 w-full h-full bg-cover bg-center bg-no-repeat"
style="background-image: url({$settings.backgroundImageUrl}) "
/>
<div
class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-white to-white/85 dark:from-gray-900 dark:to-[#171717]/90 z-0"
/>
{/if}
<Navbar
{title}
bind:selectedModels
...
...
@@ -1320,7 +1347,9 @@
{#if $banners.length > 0 && messages.length === 0 && !$chatId && selectedModels.length <= 1}
<div
class="absolute top-[4.25rem] w-full {$showSidebar ? 'md:max-w-[calc(100%-260px)]' : ''}"
class="absolute top-[4.25rem] w-full {$showSidebar
? 'md:max-w-[calc(100%-260px)]'
: ''} z-20"
>
<div class=" flex flex-col gap-1 w-full">
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
...
...
@@ -1345,9 +1374,9 @@
</div>
{/if}
<div class="flex flex-col flex-auto">
<div class="flex flex-col flex-auto
z-10
">
<div
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full"
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full
z-10
"
id="messages-container"
bind:this={messagesContainerElement}
on:scroll={(e) => {
...
...
@@ -1386,6 +1415,7 @@
}
return a;
}, [])}
transparentBackground={$settings?.backgroundImageUrl ?? false}
{selectedModels}
{messages}
{submitPrompt}
...
...
src/lib/components/chat/MessageInput.svelte
View file @
4aab4609
...
...
@@ -15,11 +15,19 @@
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
import {
processDocToVectorDB,
uploadDocToVectorDB,
uploadWebToVectorDB,
uploadYoutubeTranscriptionToVectorDB
} from '$lib/apis/rag';
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS, WEBUI_BASE_URL } from '$lib/constants';
import { uploadFile } from '$lib/apis/files';
import {
SUPPORTED_FILE_TYPE,
SUPPORTED_FILE_EXTENSIONS,
WEBUI_BASE_URL,
WEBUI_API_BASE_URL
} from '$lib/constants';
import Prompts from './MessageInput/PromptCommands.svelte';
import Suggestions from './MessageInput/Suggestions.svelte';
...
...
@@ -35,6 +43,8 @@
const i18n = getContext('i18n');
export let transparentBackground = false;
export let submitPrompt: Function;
export let stopResponse: Function;
...
...
@@ -84,44 +94,75 @@
element.scrollTop = element.scrollHeight;
};
const upload
Doc
= async (file) => {
const upload
FileHandler
= async (file) => {
console.log(file);
// Check if the file is an audio file and transcribe/convert it to text file
if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(error);
return null;
});
const doc = {
type: 'doc',
name: file.name,
collection_name: '',
upload_status: false,
error: ''
};
try {
files = [...files, doc];
if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(error);
return null;
});
if (res) {
console.log(res);
const blob = new Blob([res.text], { type: 'text/plain' });
file = blobToFile(blob, `${file.name}.txt`);
}
}
if (res) {
console.log(res);
const blob = new Blob([res.text], { type: 'text/plain' });
file = blobToFile(blob, `${file.name}.txt`);
}
// Upload the file to the server
const uploadedFile = await uploadFile(localStorage.token, file).catch((error) => {
toast.error(error);
return null;
});
if (uploadedFile) {
const fileItem = {
type: 'file',
file: uploadedFile,
id: uploadedFile.id,
url: `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`,
name: file.name,
collection_name: '',
status: 'uploaded',
error: ''
};
files = [...files, fileItem];
// TODO: Check if tools & functions have files support to skip this step to delegate file processing
// Default Upload to VectorDB
if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
processFileItem(fileItem);
} else {
toast.error(
$i18n.t(`Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.`, {
file_type: file['type']
})
);
processFileItem(fileItem);
}
}
};
const res = await uploadDocToVectorDB(localStorage.token, '', file);
const processFileItem = async (fileItem) => {
try {
const res = await processDocToVectorDB(localStorage.token, fileItem.id);
if (res) {
doc.upload_status = true
;
doc
.collection_name = res.collection_name;
fileItem.status = 'processed'
;
fileItem
.collection_name = res.collection_name;
files = files;
}
} catch (e) {
// Remove the failed doc from the files array
files = files.filter((f) => f.
name
!== file
.name
);
//
files = files.filter((f) => f.
id
!== file
Item.id
);
toast.error(e);
fileItem.status = 'processed';
files = files;
}
};
...
...
@@ -132,7 +173,7 @@
type: 'doc',
name: url,
collection_name: '',
upload_
status: false,
status: false,
url: url,
error: ''
};
...
...
@@ -142,7 +183,7 @@
const res = await uploadWebToVectorDB(localStorage.token, '', url);
if (res) {
doc.
upload_
status =
true
;
doc.status =
'processed'
;
doc.collection_name = res.collection_name;
files = files;
}
...
...
@@ -160,7 +201,7 @@
type: 'doc',
name: url,
collection_name: '',
upload_
status: false,
status: false,
url: url,
error: ''
};
...
...
@@ -170,7 +211,7 @@
const res = await uploadYoutubeTranscriptionToVectorDB(localStorage.token, url);
if (res) {
doc.
upload_
status =
true
;
doc.status =
'processed'
;
doc.collection_name = res.collection_name;
files = files;
}
...
...
@@ -228,19 +269,8 @@
];
};
reader.readAsDataURL(file);
} else if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
uploadDoc(file);
} else {
toast.error(
$i18n.t(
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
{ file_type: file['type'] }
)
);
uploadDoc(file);
uploadFileHandler(file);
}
});
} else {
...
...
@@ -336,9 +366,9 @@
files = [
...files,
{
type: e?.detail?.type ?? '
doc
',
type: e?.detail?.type ?? '
file
',
...e.detail,
upload_
status:
true
status:
'processed'
}
];
}}
...
...
@@ -391,7 +421,7 @@
</div>
</div>
<div class="bg-white dark:bg-gray-900">
<div class="
{transparentBackground ? 'bg-transparent' : '
bg-white dark:bg-gray-900
'}
">
<div class="max-w-6xl px-2.5 md:px-6 mx-auto inset-x-0">
<div class=" pb-2">
<input
...
...
@@ -407,8 +437,6 @@
if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
if (visionCapableModels.length === 0) {
toast.error($i18n.t('Selected model(s) do not support image inputs'));
inputFiles = null;
filesInputElement.value = '';
return;
}
let reader = new FileReader();
...
...
@@ -420,30 +448,17 @@
url: `${event.target.result}`
}
];
inputFiles = null;
filesInputElement.value = '';
};
reader.readAsDataURL(file);
} else if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
uploadDoc(file);
filesInputElement.value = '';
} else {
toast.error(
$i18n.t(
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
{ file_type: file['type'] }
)
);
uploadDoc(file);
filesInputElement.value = '';
uploadFileHandler(file);
}
});
} else {
toast.error($i18n.t(`File not found.`));
}
filesInputElement.value = '';
}}
/>
...
...
@@ -517,12 +532,12 @@
</Tooltip>
{/if}
</div>
{:else if
file.type === 'doc'
}
{:else if
['doc', 'file'].includes(file.type)
}
<div
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
{#if file.
upload_status
}
{#if file.
status === 'processed'
}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
...
...
src/lib/components/chat/MessageInput/CallOverlay.svelte
View file @
4aab4609
<
script
lang
=
"ts"
>
import { config, settings, showCallOverlay } from '$lib/stores';
import
{
config
,
models
,
settings
,
showCallOverlay
}
from
'$lib/stores'
;
import
{
onMount
,
tick
,
getContext
}
from
'svelte'
;
import
{
...
...
@@ -28,9 +28,12 @@
export
let
chatId
;
export
let
modelId
;
let
model
=
null
;
let
loading
=
false
;
let
confirmed
=
false
;
let
interrupted
=
false
;
let
assistantSpeaking
=
false
;
let
emoji
=
null
;
...
...
@@ -268,6 +271,15 @@
return
;
}
if
(
assistantSpeaking
)
{
//
Mute
the
audio
if
the
assistant
is
speaking
analyser
.
maxDecibels
=
0
;
analyser
.
minDecibels
=
-
1
;
}
else
{
analyser
.
minDecibels
=
MIN_DECIBELS
;
analyser
.
maxDecibels
=
-
30
;
}
analyser
.
getByteTimeDomainData
(
timeDomainData
);
analyser
.
getByteFrequencyData
(
domainData
);
...
...
@@ -379,6 +391,7 @@
};
const
stopAllAudio
=
async
()
=>
{
assistantSpeaking
=
false
;
interrupted
=
true
;
if
(
chatStreaming
)
{
...
...
@@ -485,6 +498,7 @@
}
}
else
if
(
finishedMessages
[
id
]
&&
messages
[
id
]
&&
messages
[
id
].
length
===
0
)
{
//
If
the
message
is
finished
and
there
are
no
more
messages
to
process
,
break
the
loop
assistantSpeaking
=
false
;
break
;
}
else
{
//
No
messages
to
process
,
sleep
for
a
bit
...
...
@@ -495,6 +509,8 @@
};
onMount
(
async
()
=>
{
model
=
$
models
.
find
((
m
)
=>
m
.
id
===
modelId
);
startRecording
();
const
chatStartHandler
=
async
(
e
)
=>
{
...
...
@@ -511,6 +527,7 @@
}
audioAbortController
=
new
AbortController
();
assistantSpeaking
=
true
;
//
Start
monitoring
and
playing
audio
for
the
message
ID
monitorAndPlayAudio
(
id
,
audioAbortController
.
signal
);
}
...
...
@@ -545,9 +562,9 @@
const
chatFinishHandler
=
async
(
e
)
=>
{
const
{
id
,
content
}
=
e
.
detail
;
//
"content"
here
is
the
entire
message
from
the
assistant
finishedMessages
[
id
]
=
true
;
chatStreaming
=
false
;
finishedMessages[id] = true;
};
eventTarget
.
addEventListener
(
'chat:start'
,
chatStartHandler
);
...
...
@@ -577,7 +594,15 @@
>
<
div
class
=
"max-w-lg w-full h-screen max-h-[100dvh] flex flex-col justify-between p-3 md:p-6"
>
{#
if
camera
}
<div class="flex justify-center items-center w-full h-20 min-h-20">
<
button
type
=
"button"
class
=
"flex justify-center items-center w-full h-20 min-h-20"
on
:
click
={()
=>
{
if
(
assistantSpeaking
)
{
stopAllAudio
();
}
}}
>
{#
if
emoji
}
<
div
class
=
" transition-all rounded-full"
...
...
@@ -591,7 +616,7 @@
>
{
emoji
}
</
div
>
{:else if loading}
{:
else
if
loading
||
assistantSpeaking
}
<
svg
class
=
"size-12 text-gray-900 dark:text-gray-400"
viewBox
=
"0 0 24 24"
...
...
@@ -636,76 +661,97 @@
? ' size-16'
: rmsLevel * 100 > 1
? 'size-14'
: 'size-12'} transition-all bg-black dark:bg-white rounded-full"
: 'size-12'} transition-all rounded-full {(model?.info?.meta
?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
? ' bg-cover bg-center bg-no-repeat'
: 'bg-black dark:bg-white'} bg-black dark:bg-white"
style
={(
model
?.
info
?.
meta
?.
profile_image_url
??
'/favicon.png'
)
!== '/favicon.png'
?
`
background
-
image
:
url
(
'${model?.info?.meta?.profile_image_url}'
);`
:
''
}
/>
{/
if
}
<
!-- navbar -->
</
div
>
</
button
>
{/
if
}
<
div
class
=
"flex justify-center items-center flex-1 h-full w-full max-h-full"
>
{#
if
!camera}
{#if emoji}
<div
class=" transition-all rounded-full"
style="font-size:{rmsLevel * 100 > 4
? '13'
: rmsLevel * 100 > 2
? '12'
: rmsLevel * 100 > 1
? '11.5'
: '11'}rem;width:100%;text-align:center;"
>
{emoji}
</div>
{:else if loading}
<svg
class="size-44 text-gray-900 dark:text-gray-400"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
<
button
type
=
"button"
on
:
click
={()
=>
{
if
(
assistantSpeaking
)
{
stopAllAudio
();
}
}}
>
{#
if
emoji
}
<
div
class
=
" transition-all rounded-full"
style
=
"font-size:{rmsLevel * 100 > 4
? '13'
: rmsLevel * 100 > 2
? '12'
: rmsLevel * 100 > 1
? '11.5'
: '11'}rem;width:100%;text-align:center;"
>
{
emoji
}
</
div
>
{:
else
if
loading
||
assistantSpeaking
}
<
svg
class
=
"size-44 text-gray-900 dark:text-gray-400"
viewBox
=
"0 0 24 24"
fill
=
"currentColor"
xmlns
=
"http://www.w3.org/2000/svg"
><
style
>
.
spinner_qM83
{
animation
:
spinner_8HQG
1.05
s
infinite
;
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
.
spinner_oXPr
{
animation
-
delay
:
0.1
s
;
}
100%
{
transform: translate(0)
;
.
spinner_ZTLf
{
animation
-
delay
:
0.2
s
;
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="3"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
>
{:else}
<div
class=" {rmsLevel * 100 > 4
? ' size-52'
: rmsLevel * 100 > 2
? 'size-48'
: rmsLevel * 100 > 1
? 'size-[11.5rem]'
: 'size-44'} transition-all bg-black dark:bg-white rounded-full"
/>
{/if}
@
keyframes
spinner_8HQG
{
0
%,
57.14
%
{
animation
-
timing
-
function
:
cubic
-
bezier
(
0.33
,
0.66
,
0.66
,
1
);
transform
:
translate
(
0
);
}
28.57
%
{
animation
-
timing
-
function
:
cubic
-
bezier
(
0.33
,
0
,
0.66
,
0.33
);
transform
:
translateY
(-
6
px
);
}
100
%
{
transform
:
translate
(
0
);
}
}
</
style
><
circle
class
=
"spinner_qM83"
cx
=
"4"
cy
=
"12"
r
=
"3"
/><
circle
class
=
"spinner_qM83 spinner_oXPr"
cx
=
"12"
cy
=
"12"
r
=
"3"
/><
circle
class
=
"spinner_qM83 spinner_ZTLf"
cx
=
"20"
cy
=
"12"
r
=
"3"
/></
svg
>
{:
else
}
<
div
class
=
" {rmsLevel * 100 > 4
? ' size-52'
: rmsLevel * 100 > 2
? 'size-48'
: rmsLevel * 100 > 1
? 'size-[11.5rem]'
: 'size-44'} transition-all rounded-full {(model?.info?.meta
?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
? ' bg-cover bg-center bg-no-repeat'
: 'bg-black dark:bg-white'} "
style
={(
model
?.
info
?.
meta
?.
profile_image_url
??
'/favicon.png'
)
!== '/favicon.png'
?
`
background
-
image
:
url
(
'${model?.info?.meta?.profile_image_url}'
);`
:
''
}
/>
{/
if
}
</
button
>
{:
else
}
<
div
class
=
"relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full"
...
...
@@ -805,10 +851,19 @@
</
div
>
<
div
>
<button type="button">
<
button
type
=
"button"
on
:
click
={()
=>
{
if
(
assistantSpeaking
)
{
stopAllAudio
();
}
}}
>
<
div
class
=
" line-clamp-1 text-sm font-medium"
>
{#
if
loading
}
{$
i18n
.
t
(
'Thinking...'
)}
{:
else
if
assistantSpeaking
}
{$
i18n
.
t
(
'Tap to interrupt'
)}
{:
else
}
{$
i18n
.
t
(
'Listening...'
)}
{/
if
}
...
...
src/lib/components/chat/MessageInput/Documents.svelte
View file @
4aab4609
...
...
@@ -101,20 +101,20 @@
</script>
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full
px-2
">
<div class=" bg-gray-
10
0 dark:bg-gray-
70
0 w-10 rounded-l-
x
l text-center">
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0
z-10
">
<div class="flex w-full
dark:border dark:border-gray-850 rounded-lg
">
<div class=" bg-gray-
5
0 dark:bg-gray-
85
0 w-10 rounded-l-l
g
text-center">
<div class=" text-lg font-semibold mt-2">#</div>
</div>
<div
class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-
85
0 dark:text-gray-100"
class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-
90
0 dark:text-gray-100"
>
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5
scrollbar-hidden
">
{#each filteredItems as doc, docIdx}
<button
class=" px-3 py-1.5 rounded-xl w-full text-left {docIdx === selectedIdx
? ' bg-gray-
10
0 dark:bg-gray-
60
0 dark:text-gray-100 selected-command-option-button'
? ' bg-gray-
5
0 dark:bg-gray-
85
0 dark:text-gray-100 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
...
...
src/lib/components/chat/MessageInput/Models.svelte
View file @
4aab4609
...
...
@@ -133,18 +133,20 @@
{#if prompt.charAt(0) === '@'}
{#if filteredModels.length > 0}
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full
px-2
">
<div class=" bg-gray-
10
0 dark:bg-gray-
70
0 w-10 rounded-l-
x
l text-center">
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0
z-10
">
<div class="flex w-full
dark:border dark:border-gray-850 rounded-lg
">
<div class=" bg-gray-
5
0 dark:bg-gray-
85
0 w-10 rounded-l-l
g
text-center">
<div class=" text-lg font-semibold mt-2">@</div>
</div>
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-850">
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
<div
class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
>
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
{#each filteredModels as model, modelIdx}
<button
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
? ' bg-gray-
10
0 dark:bg-gray-
600
selected-command-option-button'
? '
bg-gray-
5
0 dark:bg-gray-
850
selected-command-option-button'
: ''}"
type="button"
on:click={() => {
...
...
src/lib/components/chat/MessageInput/PromptCommands.svelte
View file @
4aab4609
...
...
@@ -88,18 +88,20 @@
</script>
{#if filteredPromptCommands.length > 0}
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full
px-2
">
<div class=" bg-gray-
10
0 dark:bg-gray-
70
0 w-10 rounded-l-
x
l text-center">
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0
z-10
">
<div class="flex w-full
dark:border dark:border-gray-850 rounded-lg
">
<div class="
bg-gray-
5
0 dark:bg-gray-
85
0 w-10 rounded-l-l
g
text-center">
<div class=" text-lg font-semibold mt-2">/</div>
</div>
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-850">
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
<div
class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
>
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
{#each filteredPromptCommands as command, commandIdx}
<button
class=" px-3 py-1.5 rounded-xl w-full text-left {commandIdx === selectedCommandIdx
? ' bg-gray-
10
0 dark:bg-gray-
60
0 selected-command-option-button'
? '
bg-gray-
5
0 dark:bg-gray-
85
0 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
...
...
@@ -122,7 +124,7 @@
</div>
<div
class=" px-2 pb-1 text-xs text-gray-600 dark:text-gray-100 bg-white dark:bg-gray-
85
0 rounded-br-xl flex items-center space-x-1"
class=" px-2 pb-1 text-xs text-gray-600 dark:text-gray-100 bg-white dark:bg-gray-
90
0 rounded-br-xl flex items-center space-x-1"
>
<div>
<svg
...
...
src/lib/components/chat/MessageInput/Suggestions.svelte
View file @
4aab4609
...
...
@@ -62,7 +62,7 @@
<div class="text-sm text-gray-600 font-normal line-clamp-2">{prompt.title[1]}</div>
{:else}
<div
class="
self-center
text-sm font-medium dark:text-gray-300 dark:group-hover:text-gray-100 transition line-clamp-2"
class=" text-sm font-medium dark:text-gray-300 dark:group-hover:text-gray-100 transition line-clamp-2"
>
{prompt.content}
</div>
...
...
src/lib/components/chat/Messages.svelte
View file @
4aab4609
...
...
@@ -202,38 +202,51 @@
}, 100);
};
const
messageDelet
eHandler = async (messageId) => {
const
deleteMessag
eHandler = async (messageId) => {
const messageToDelete = history.messages[messageId];
const messageParentId = messageToDelete.parentId;
const messageChildrenIds = messageToDelete.childrenIds ?? [];
const hasSibling = messageChildrenIds.some(
const parentMessageId = messageToDelete.parentId;
const childMessageIds = messageToDelete.childrenIds ?? [];
const hasDescendantMessages = childMessageIds.some(
(childId) => history.messages[childId]?.childrenIds?.length > 0
);
messageChildrenIds.forEach((childId) => {
const child = history.messages[childId];
if (child && child.childrenIds) {
if (child.childrenIds.length === 0 && !hasSibling) {
// if last prompt/response pair
history.messages[messageParentId].childrenIds = [];
history.currentId = messageParentId;
history.currentId = parentMessageId;
await tick();
// Remove the message itself from the parent message's children array
history.messages[parentMessageId].childrenIds = history.messages[
parentMessageId
].childrenIds.filter((id) => id !== messageId);
await tick();
childMessageIds.forEach((childId) => {
const childMessage = history.messages[childId];
if (childMessage && childMessage.childrenIds) {
if (childMessage.childrenIds.length === 0 && !hasDescendantMessages) {
// If there are no other responses/prompts
history.messages[parentMessageId].childrenIds = [];
} else {
child.childrenIds.forEach((grandChildId) => {
child
Message
.childrenIds.forEach((grandChildId) => {
if (history.messages[grandChildId]) {
history.messages[grandChildId].parentId =
messageParent
Id;
history.messages[
messageParent
Id].childrenIds.push(grandChildId);
history.messages[grandChildId].parentId =
parentMessage
Id;
history.messages[
parentMessage
Id].childrenIds.push(grandChildId);
}
});
}
}
// remove response
history.messages[messageParentId].childrenIds = history.messages[
messageParentId
// Remove child message id from the parent message's children array
history.messages[parentMessageId].childrenIds = history.messages[
parentMessageId
].childrenIds.filter((id) => id !== childId);
});
// remove prompt
history.messages[messageParentId].childrenIds = history.messages[
messageParentId
].childrenIds.filter((id) => id !== messageId);
await tick();
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
...
...
@@ -292,7 +305,7 @@
>
{#if message.role === 'user'}
<UserMessage
on:delete={() =>
messageDelet
eHandler(message.id)}
on:delete={() =>
deleteMessag
eHandler(message.id)}
{user}
{readOnly}
{message}
...
...
@@ -308,7 +321,7 @@
copyToClipboard={copyToClipboardWithToast}
/>
{:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1}
{#key message.id}
{#key message.id
&& history.currentId
}
<ResponseMessage
{message}
siblings={history.messages[message.parentId]?.childrenIds ?? []}
...
...
@@ -372,7 +385,7 @@
{/each}
{#if bottomPadding}
<div class=" pb-
20
" />
<div class=" pb-
6
" />
{/if}
{/key}
</div>
...
...
src/lib/components/chat/Messages/CodeBlock.svelte
View file @
4aab4609
...
...
@@ -203,8 +203,18 @@ __builtins__.input = input`);
};
};
let debounceTimeout;
$: if (code) {
highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code;
// Function to perform the code highlighting
const highlightCode = () => {
highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code;
};
// Clear the previous timeout if it exists
clearTimeout(debounceTimeout);
// Set a new timeout to debounce the code highlighting
debounceTimeout = setTimeout(highlightCode, 10);
}
</script>
...
...
src/lib/components/chat/Messages/Placeholder.svelte
View file @
4aab4609
...
...
@@ -9,6 +9,7 @@
import Suggestions from '../MessageInput/Suggestions.svelte';
import { sanitizeResponseContent } from '$lib/utils';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
...
...
@@ -41,14 +42,23 @@
selectedModelIdx = modelIdx;
}}
>
<img
crossorigin="anonymous"
src={model?.info?.meta?.profile_image_url ??
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
alt="logo"
draggable="false"
/>
<Tooltip
content={marked.parse(
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description ?? '')
)}
placement="right"
>
<img
crossorigin="anonymous"
src={model?.info?.meta?.profile_image_url ??
($i18n.language === 'dg-DG'
? `/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`)}
class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
alt="logo"
draggable="false"
/>
</Tooltip>
</button>
{/each}
</div>
...
...
Prev
1
2
3
4
5
6
7
Next
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