mirror of
https://github.com/morgan9e/chatgpt-web
synced 2026-04-14 00:14:04 +09:00
fff
This commit is contained in:
@@ -3,16 +3,19 @@
|
|||||||
import { wrap } from 'svelte-spa-router/wrap'
|
import { wrap } from 'svelte-spa-router/wrap'
|
||||||
|
|
||||||
import Navbar from './lib/Navbar.svelte'
|
import Navbar from './lib/Navbar.svelte'
|
||||||
import Sidebar from './lib/Sidebar.svelte'
|
import Sidebar, { sidebarCollapsed } from './lib/Sidebar.svelte'
|
||||||
import Home from './lib/Home.svelte'
|
import Home from './lib/Home.svelte'
|
||||||
import Chat from './lib/Chat.svelte'
|
import Chat from './lib/Chat.svelte'
|
||||||
import NewChat from './lib/NewChat.svelte'
|
import NewChat from './lib/NewChat.svelte'
|
||||||
import { chatsStorage } from './lib/Storage.svelte'
|
import { chatsStorage } from './lib/Storage.svelte'
|
||||||
import { Modals, closeModal } from 'svelte-modals'
|
import { Modals, closeModal } from 'svelte-modals'
|
||||||
import { dispatchModalEsc, checkModalEsc } from './lib/Util.svelte'
|
import { dispatchModalEsc, checkModalEsc, migrateChatData } from './lib/Util.svelte'
|
||||||
import { set as setOpenAI } from './lib/providers/openai/util.svelte'
|
import { set as setOpenAI } from './lib/providers/openai/util.svelte'
|
||||||
import { hasActiveModels } from './lib/Models.svelte'
|
import { hasActiveModels } from './lib/Models.svelte'
|
||||||
|
|
||||||
|
// Run migration on app startup to convert old numeric chat IDs to UUIDs
|
||||||
|
migrateChatData()
|
||||||
|
|
||||||
// Check if the API key is passed in as a "key" query parameter - if so, save it
|
// Check if the API key is passed in as a "key" query parameter - if so, save it
|
||||||
// Example: https://niek.github.io/chatgpt-web/#/?key=sk-...
|
// Example: https://niek.github.io/chatgpt-web/#/?key=sk-...
|
||||||
const urlParams: URLSearchParams = new URLSearchParams($querystring)
|
const urlParams: URLSearchParams = new URLSearchParams($querystring)
|
||||||
@@ -34,7 +37,7 @@
|
|||||||
'/chat/:chatId': wrap({
|
'/chat/:chatId': wrap({
|
||||||
component: Chat,
|
component: Chat,
|
||||||
conditions: (detail) => {
|
conditions: (detail) => {
|
||||||
return $chatsStorage.find((chat) => chat.id === parseInt(detail?.params?.chatId as string)) !== undefined
|
return $chatsStorage.find((chat) => chat.id === detail?.params?.chatId as string) !== undefined
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -51,10 +54,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div class="side-bar-column">
|
<div class="side-bar-column" class:collapsed={$sidebarCollapsed}>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
<div class="main-content-column" id="content">
|
<div class="main-content-column" class:collapsed={$sidebarCollapsed} id="content">
|
||||||
{#key $location}
|
{#key $location}
|
||||||
<Router {routes} on:conditionsFailed={() => replace('/')}/>
|
<Router {routes} on:conditionsFailed={() => replace('/')}/>
|
||||||
{/key}
|
{/key}
|
||||||
|
|||||||
469
src/app.scss
469
src/app.scss
@@ -96,7 +96,9 @@ html {
|
|||||||
/* Sizes */
|
/* Sizes */
|
||||||
--sidebarTop: 0px;
|
--sidebarTop: 0px;
|
||||||
--sidebarWidth: max(300px, 20%);
|
--sidebarWidth: max(300px, 20%);
|
||||||
|
--sidebarCollapsedWidth: 60px;
|
||||||
--mainContentWidth: calc(100% - var(--sidebarWidth));
|
--mainContentWidth: calc(100% - var(--sidebarWidth));
|
||||||
|
--mainContentWidthCollapsed: calc(100% - var(--sidebarCollapsedWidth));
|
||||||
|
|
||||||
--sectionPaddingTop: 0px;
|
--sectionPaddingTop: 0px;
|
||||||
|
|
||||||
@@ -579,13 +581,17 @@ $modal-background-background-color-dark: rgba($dark, 0.86) !default; // remove t
|
|||||||
width: var(--mainContentWidth);
|
width: var(--mainContentWidth);
|
||||||
padding-top: var(--sectionPaddingTop);
|
padding-top: var(--sectionPaddingTop);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content-column.collapsed {
|
||||||
|
width: var(--mainContentWidthCollapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
aside.menu.main-menu {
|
aside.menu.main-menu {
|
||||||
z-index:50;
|
z-index:50;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: var(--sidebarWidth);
|
width: var(--sidebarWidth);
|
||||||
padding-right: 20px;
|
|
||||||
top: var(--sidebarTop);
|
top: var(--sidebarTop);
|
||||||
bottom:0px;
|
bottom:0px;
|
||||||
}
|
}
|
||||||
@@ -681,6 +687,12 @@ aside.menu.main-menu .menu-expanse {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
width: var(--mainContentWidth);
|
width: var(--mainContentWidth);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content-column.collapsed ~ .pin-footer,
|
||||||
|
.main-content-column.collapsed .pin-footer {
|
||||||
|
width: var(--mainContentWidthCollapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-input-container {
|
.prompt-input-container {
|
||||||
@@ -688,6 +700,7 @@ aside.menu.main-menu .menu-expanse {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
width: var(--mainContentWidth);
|
width: var(--mainContentWidth);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
|
||||||
padding:
|
padding:
|
||||||
var(--chatInputPaddingTop)
|
var(--chatInputPaddingTop)
|
||||||
@@ -704,6 +717,11 @@ aside.menu.main-menu .menu-expanse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-content-column.collapsed ~ .prompt-input-container,
|
||||||
|
.main-content-column.collapsed .prompt-input-container {
|
||||||
|
width: var(--mainContentWidthCollapsed);
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 900px) {
|
@media only screen and (max-width: 900px) {
|
||||||
.prompt-input-container {
|
.prompt-input-container {
|
||||||
.control.send .button {
|
.control.send .button {
|
||||||
@@ -744,6 +762,10 @@ aside.menu.main-menu .menu-expanse {
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar .uncollapse-menu .button {
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
.main-menu .menu-nav-bar .chat-option-menu {
|
.main-menu .menu-nav-bar .chat-option-menu {
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
}
|
}
|
||||||
@@ -938,3 +960,448 @@ aside.menu.main-menu .menu-expanse {
|
|||||||
.modal.chat-settings .field-body {
|
.modal.chat-settings .field-body {
|
||||||
max-width: calc(100% - 40px);
|
max-width: calc(100% - 40px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== MODERN SIDEBAR STYLING ===== */
|
||||||
|
|
||||||
|
/* Base Sidebar */
|
||||||
|
.modern-sidebar {
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
width: var(--sidebarCollapsedWidth) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
background-color: var(--BgColorSidebarDark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Section */
|
||||||
|
.modern-sidebar .sidebar-header {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.header-content-collapsed {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-title {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container-collapsed {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-button {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-option-menu-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-option-menu-container-collapsed {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Chat option menu when collapsed */
|
||||||
|
.modern-sidebar .chat-option-menu-container-collapsed .dropdown .dropdown-menu {
|
||||||
|
top: 100% !important;
|
||||||
|
bottom: auto !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: auto !important;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Chat List */
|
||||||
|
.modern-sidebar .chat-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-menu-item {
|
||||||
|
display: block;
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 0;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background-color: rgba(0, 102, 204, 0.2);
|
||||||
|
color: #ffffff;
|
||||||
|
border-left: 3px solid #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
.chat-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button,
|
||||||
|
.edit-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-button {
|
||||||
|
right: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.delete-button,
|
||||||
|
.edit-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.modern-sidebar .sidebar-footer {
|
||||||
|
padding: 16px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.footer-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-controls-collapsed {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chat-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chat-section-collapsed {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chat-button {
|
||||||
|
width: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #0066cc;
|
||||||
|
border: none;
|
||||||
|
color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 8px 12px;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #0052a3;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chat-button-collapsed {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #0066cc;
|
||||||
|
border: none;
|
||||||
|
color: #ffffff;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #0052a3;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button-collapsed {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown Positioning */
|
||||||
|
.modern-sidebar .dropdown {
|
||||||
|
.dropdown-menu {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background-color: var(--BgColorSidebarDark);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-up .dropdown-menu {
|
||||||
|
bottom: 100%;
|
||||||
|
top: auto;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-right .dropdown-menu {
|
||||||
|
right: 0;
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix dropdown positioning in header to prevent off-screen */
|
||||||
|
.modern-sidebar .sidebar-header .dropdown .dropdown-menu {
|
||||||
|
top: calc(100% + 8px) !important;
|
||||||
|
bottom: auto !important;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure chat option menu dropdown is visible */
|
||||||
|
.modern-sidebar .chat-option-menu-container .dropdown .dropdown-menu {
|
||||||
|
top: 100% !important;
|
||||||
|
bottom: auto !important;
|
||||||
|
left: auto !important;
|
||||||
|
right: 0 !important;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer dropdown positioning */
|
||||||
|
.modern-sidebar .sidebar-footer .dropdown .dropdown-menu {
|
||||||
|
bottom: 100% !important;
|
||||||
|
top: auto !important;
|
||||||
|
left: 50% !important;
|
||||||
|
right: auto !important;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout Updates */
|
||||||
|
.side-bar-column.collapsed {
|
||||||
|
width: var(--sidebarCollapsedWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Override */
|
||||||
|
aside.menu.main-menu.modern-sidebar {
|
||||||
|
background: var(--BgColorSidebarDark);
|
||||||
|
box-shadow: none;
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
.menu-expanse {
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapsed Mode Adjustments */
|
||||||
|
.modern-sidebar.collapsed {
|
||||||
|
.chat-list {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
import { getModelDetail } from './Models.svelte'
|
import { getModelDetail } from './Models.svelte'
|
||||||
|
|
||||||
export let params = { chatId: '' }
|
export let params = { chatId: '' }
|
||||||
const chatId: number = parseInt(params.chatId)
|
const chatId: string = params.chatId
|
||||||
|
|
||||||
let chatRequest = new ChatRequest()
|
let chatRequest = new ChatRequest()
|
||||||
let input: HTMLTextAreaElement
|
let input: HTMLTextAreaElement
|
||||||
@@ -53,8 +53,8 @@
|
|||||||
|
|
||||||
// Optimize chat lookup to avoid expensive find() on every chats update
|
// Optimize chat lookup to avoid expensive find() on every chats update
|
||||||
let chat: Chat
|
let chat: Chat
|
||||||
let chatSettings: ChatSettings
|
let chatSettings: any
|
||||||
let showSettingsModal
|
let showSettingsModal: any
|
||||||
|
|
||||||
// Only update chat when chatId changes or when the specific chat is updated
|
// Only update chat when chatId changes or when the specific chat is updated
|
||||||
$: {
|
$: {
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
|
|
||||||
$: afterChatLoad($currentChatId)
|
$: afterChatLoad($currentChatId)
|
||||||
|
|
||||||
setCurrentChat(0)
|
setCurrentChat('')
|
||||||
// Make sure chat object is ready to go
|
// Make sure chat object is ready to go
|
||||||
updateChatSettings(chatId)
|
updateChatSettings(chatId)
|
||||||
|
|
||||||
@@ -303,7 +303,7 @@
|
|||||||
|
|
||||||
const userMessagesCount = chat.messages.filter(message => message.role === 'user').length
|
const userMessagesCount = chat.messages.filter(message => message.role === 'user').length
|
||||||
const assiMessagesCount = chat.messages.filter(message => message.role === 'assistant').length
|
const assiMessagesCount = chat.messages.filter(message => message.role === 'assistant').length
|
||||||
if (userMessagesCount == 3 && chat.name.startsWith('Chat ')) {
|
if (userMessagesCount == 3 && chat.name.startsWith('New Chat')) {
|
||||||
suggestName()
|
suggestName()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,7 +392,7 @@
|
|||||||
<div class="level-left">
|
<div class="level-left">
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
<p class="subtitle is-5">
|
<p class="subtitle is-5">
|
||||||
<span>{chat.name || `Chat ${chat.id}`}</span>
|
<span>{chat.name || `New Chat`}</span>
|
||||||
<a href={'#'} class="greyscale ml-2 is-hidden has-text-weight-bold editbutton" title="Rename chat" on:click|preventDefault={promptRename}><Fa icon={faPenToSquare} /></a>
|
<a href={'#'} class="greyscale ml-2 is-hidden has-text-weight-bold editbutton" title="Rename chat" on:click|preventDefault={promptRename}><Fa icon={faPenToSquare} /></a>
|
||||||
<a href={'#'} class="greyscale ml-2 is-hidden has-text-weight-bold editbutton" title="Suggest a chat name" on:click|preventDefault={suggestName}><Fa icon={faLightbulb} /></a>
|
<a href={'#'} class="greyscale ml-2 is-hidden has-text-weight-bold editbutton" title="Suggest a chat name" on:click|preventDefault={suggestName}><Fa icon={faLightbulb} /></a>
|
||||||
<!-- <a href={'#'} class="greyscale ml-2 is-hidden has-text-weight-bold editbutton" title="Copy this chat" on:click|preventDefault={() => { copyChat(chatId) }}><Fa icon={faClone} /></a> -->
|
<!-- <a href={'#'} class="greyscale ml-2 is-hidden has-text-weight-bold editbutton" title="Copy this chat" on:click|preventDefault={() => { copyChat(chatId) }}><Fa icon={faClone} /></a> -->
|
||||||
@@ -410,7 +410,7 @@
|
|||||||
|
|
||||||
<Messages messages={$currentChatMessages} chatId={chatId} chat={chat} />
|
<Messages messages={$currentChatMessages} chatId={chatId} chat={chat} />
|
||||||
|
|
||||||
{#if chatRequest.updating === true || $currentChatId === 0}
|
{#if chatRequest.updating === true || !$currentChatId}
|
||||||
<article class="message is-success assistant-message">
|
<article class="message is-success assistant-message">
|
||||||
<div class="message-body content">
|
<div class="message-body content">
|
||||||
<span class="is-loading" ></span>
|
<span class="is-loading" ></span>
|
||||||
@@ -419,7 +419,7 @@
|
|||||||
</article>
|
</article>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $currentChatId !== 0 && ($currentChatMessages.length === 0 || ($currentChatMessages.length === 1 && $currentChatMessages[0].role === 'system'))}
|
{#if $currentChatId && ($currentChatMessages.length === 0 || ($currentChatMessages.length === 1 && $currentChatMessages[0].role === 'system'))}
|
||||||
<Prompts bind:input />
|
<Prompts bind:input />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
<a class="is-pulled-right is-hidden px-1 py-0 has-text-weight-bold edit-button" href={'$'} on:click|preventDefault={() => edit()}><Fa icon={faPencil} /></a>
|
<a class="is-pulled-right is-hidden px-1 py-0 has-text-weight-bold edit-button" href={'$'} on:click|preventDefault={() => edit()}><Fa icon={faPencil} /></a>
|
||||||
<a class="is-pulled-right is-hidden px-1 py-0 has-text-weight-bold delete-button" href={'$'} on:click|preventDefault={() => delChat()}><Fa icon={faTrash} /></a>
|
<a class="is-pulled-right is-hidden px-1 py-0 has-text-weight-bold delete-button" href={'$'} on:click|preventDefault={() => delChat()}><Fa icon={faTrash} /></a>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="chat-item-name"><Fa class="mr-2 chat-icon" size="xs" icon="{faMessage}"/>{chat.name || `Chat ${chat.id}`}</span>
|
<span class="chat-item-name"><Fa class="mr-2 chat-icon" size="xs" icon="{faMessage}"/>{chat.name || `New Chat`}</span>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
import { startNewChatWithWarning, startNewChatFromChatId, errorNotice, encodeHTMLEntities } from './Util.svelte'
|
import { startNewChatWithWarning, startNewChatFromChatId, errorNotice, encodeHTMLEntities } from './Util.svelte'
|
||||||
import type { ChatSettings } from './Types.svelte'
|
import type { ChatSettings } from './Types.svelte'
|
||||||
import { hasActiveModels } from './Models.svelte'
|
import { hasActiveModels } from './Models.svelte'
|
||||||
|
import { sidebarCollapsed } from './Sidebar.svelte'
|
||||||
|
|
||||||
export let chatId
|
export let chatId
|
||||||
export const show = (showHide:boolean = true) => {
|
export const show = (showHide:boolean = true) => {
|
||||||
@@ -228,12 +229,19 @@
|
|||||||
|
|
||||||
<div class="dropdown {style}" class:is-active={showChatMenu} use:clickOutside={() => { showChatMenu = false }}>
|
<div class="dropdown {style}" class:is-active={showChatMenu} use:clickOutside={() => { showChatMenu = false }}>
|
||||||
<div class="dropdown-trigger">
|
<div class="dropdown-trigger">
|
||||||
|
{#if !sidebarCollapsed }
|
||||||
<button class="button is-ghost default-text" aria-haspopup="true"
|
<button class="button is-ghost default-text" aria-haspopup="true"
|
||||||
aria-controls="dropdown-menu3"
|
aria-controls="dropdown-menu3"
|
||||||
on:click|preventDefault|stopPropagation={() => { showChatMenu = !showChatMenu }}
|
on:click|preventDefault|stopPropagation={() => { showChatMenu = !showChatMenu }}
|
||||||
>
|
>
|
||||||
<span class="icon "><Fa icon={faEllipsis}/></span>
|
<span class="icon "><Fa icon={faEllipsis}/></span>
|
||||||
</button>
|
</button>
|
||||||
|
{:else}
|
||||||
|
<div class="icon" aria-controls="dropdown-menu3"
|
||||||
|
on:click|preventDefault|stopPropagation={() => { showChatMenu = !showChatMenu }}
|
||||||
|
>
|
||||||
|
<Fa icon={faEllipsis}/></div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-menu" id="dropdown-menu3" role="menu">
|
<div class="dropdown-menu" id="dropdown-menu3" role="menu">
|
||||||
<div class="dropdown-content">
|
<div class="dropdown-content">
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
import { getChatModelOptions, getImageModelOptions } from './Models.svelte'
|
import { getChatModelOptions, getImageModelOptions } from './Models.svelte'
|
||||||
import { faClipboard } from '@fortawesome/free-regular-svg-icons'
|
import { faClipboard } from '@fortawesome/free-regular-svg-icons'
|
||||||
|
|
||||||
export let chatId:number
|
export let chatId:string
|
||||||
export const show = () => { showSettings() }
|
export const show = () => { showSettings() }
|
||||||
|
|
||||||
let showSettingsModal = 0
|
let showSettingsModal = 0
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
import renderMathInElement from 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/auto-render.mjs'
|
import renderMathInElement from 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/auto-render.mjs'
|
||||||
|
|
||||||
export let message:Message
|
export let message:Message
|
||||||
export let chatId:number
|
export let chatId:string
|
||||||
export let chat:Chat
|
export let chat:Chat
|
||||||
|
|
||||||
$: chatSettings = chat.settings
|
$: chatSettings = chat.settings
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ onMount(() => {
|
|||||||
// console.log('started', apiKey, $lastChatId, getChat($lastChatId))
|
// console.log('started', apiKey, $lastChatId, getChat($lastChatId))
|
||||||
if (hasActiveModels() && getChat($lastChatId)) {
|
if (hasActiveModels() && getChat($lastChatId)) {
|
||||||
const chatId = $lastChatId
|
const chatId = $lastChatId
|
||||||
$lastChatId = 0
|
$lastChatId = ''
|
||||||
replace(`/chat/${chatId}`)
|
replace(`/chat/${chatId}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$lastChatId = 0
|
$lastChatId = ''
|
||||||
})
|
})
|
||||||
|
|
||||||
afterUpdate(() => {
|
afterUpdate(() => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import EditMessage from './EditMessage.svelte'
|
import EditMessage from './EditMessage.svelte'
|
||||||
|
|
||||||
export let messages : Message[]
|
export let messages : Message[]
|
||||||
export let chatId: number
|
export let chatId: string
|
||||||
export let chat: Chat
|
export let chat: Chat
|
||||||
|
|
||||||
$: chatSettings = chat.settings
|
$: chatSettings = chat.settings
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { params } from 'svelte-spa-router'
|
import { params } from 'svelte-spa-router'
|
||||||
import { pinMainMenu } from './Storage.svelte'
|
import { pinMainMenu } from './Storage.svelte'
|
||||||
import logo from '../assets/logo.svg'
|
|
||||||
import ChatOptionMenu from './ChatOptionMenu.svelte'
|
import ChatOptionMenu from './ChatOptionMenu.svelte'
|
||||||
|
import logo from '../assets/logo.svg'
|
||||||
import Fa from 'svelte-fa/src/fa.svelte'
|
import Fa from 'svelte-fa/src/fa.svelte'
|
||||||
import { faBars, faXmark } from '@fortawesome/free-solid-svg-icons/index'
|
import { faBars, faXmark } from '@fortawesome/free-solid-svg-icons/index'
|
||||||
|
$: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefined
|
||||||
$: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefined
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="navbar is-fixed-top" aria-label="main navigation">
|
<nav class="navbar is-fixed-top" aria-label="main navigation">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<div class="navbar-item">
|
<div class="navbar-item uncollapse-menu">
|
||||||
|
|
||||||
{#if $pinMainMenu}
|
{#if $pinMainMenu}
|
||||||
<button class="button" on:click|stopPropagation={() => { $pinMainMenu = false }}>
|
<button class="button" on:click|stopPropagation={() => { $pinMainMenu = false }}>
|
||||||
@@ -27,10 +26,6 @@ $: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefin
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<a class="navbar-item" href={'#/'}>
|
|
||||||
<img src={logo} alt="ChatGPT-web" width="24" height="24" />
|
|
||||||
<p class="ml-2 is-size-6 has-text-weight-bold">ChatGPT-web</p>
|
|
||||||
</a>
|
|
||||||
<div class="chat-option-menu navbar-item is-pulled-right">
|
<div class="chat-option-menu navbar-item is-pulled-right">
|
||||||
<ChatOptionMenu bind:chatId={activeChatId} />
|
<ChatOptionMenu bind:chatId={activeChatId} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -83,19 +83,19 @@ export const cleanContent = (settings: ChatSettings, content: string|undefined):
|
|||||||
return (content || '').replace(/::NOTE::[\s\S]*?::NOTE::\s*/g, '')
|
return (content || '').replace(/::NOTE::[\s\S]*?::NOTE::\s*/g, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const prepareProfilePrompt = (chatId:number) => {
|
export const prepareProfilePrompt = (chatId:string) => {
|
||||||
const settings = getChatSettings(chatId)
|
const settings = getChatSettings(chatId)
|
||||||
return mergeProfileFields(settings, settings.systemPrompt).trim()
|
return mergeProfileFields(settings, settings.systemPrompt).trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const prepareSummaryPrompt = (chatId:number, maxTokens:number) => {
|
export const prepareSummaryPrompt = (chatId:string, maxTokens:number) => {
|
||||||
const settings = getChatSettings(chatId)
|
const settings = getChatSettings(chatId)
|
||||||
const currentSummaryPrompt = settings.summaryPrompt
|
const currentSummaryPrompt = settings.summaryPrompt
|
||||||
// ~.75 words per token. We'll use 0.70 for a little extra margin.
|
// ~.75 words per token. We'll use 0.70 for a little extra margin.
|
||||||
return mergeProfileFields(settings, currentSummaryPrompt, Math.floor(maxTokens * 0.70)).trim()
|
return mergeProfileFields(settings, currentSummaryPrompt, Math.floor(maxTokens * 0.70)).trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setSystemPrompt = (chatId: number) => {
|
export const setSystemPrompt = (chatId: string) => {
|
||||||
const messages = getMessages(chatId)
|
const messages = getMessages(chatId)
|
||||||
const systemPromptMessage:Message = {
|
const systemPromptMessage:Message = {
|
||||||
role: 'system',
|
role: 'system',
|
||||||
@@ -108,7 +108,7 @@ export const setSystemPrompt = (chatId: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restart currently loaded profile
|
// Restart currently loaded profile
|
||||||
export const restartProfile = (chatId:number, noApply:boolean = false) => {
|
export const restartProfile = (chatId:string, noApply:boolean = false) => {
|
||||||
const settings = getChatSettings(chatId)
|
const settings = getChatSettings(chatId)
|
||||||
if (!settings.profile && !noApply) return applyProfile(chatId, '', true)
|
if (!settings.profile && !noApply) return applyProfile(chatId, '', true)
|
||||||
// Clear current messages
|
// Clear current messages
|
||||||
@@ -135,7 +135,7 @@ export const newNameForProfile = (name:string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply currently selected profile
|
// Apply currently selected profile
|
||||||
export const applyProfile = (chatId:number, key:string = '', resetChat:boolean = false) => {
|
export const applyProfile = (chatId:string, key:string = '', resetChat:boolean = false) => {
|
||||||
resetChatSettings(chatId, resetChat) // Fully reset
|
resetChatSettings(chatId, resetChat) // Fully reset
|
||||||
if (!resetChat) return
|
if (!resetChat) return
|
||||||
return restartProfile(chatId, true)
|
return restartProfile(chatId, true)
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const getExcludeFromProfile = () => {
|
|||||||
return excludeFromProfile
|
return excludeFromProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideModelSetting = (chatId: number, setting: ChatSetting) => {
|
const hideModelSetting = (chatId: string, setting: ChatSetting) => {
|
||||||
return getModelDetail(getChatSettings(chatId).model).hideSetting(chatId, setting)
|
return getModelDetail(getChatSettings(chatId).model).hideSetting(chatId, setting)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,9 +142,9 @@ const excludeFromProfile = {
|
|||||||
|
|
||||||
export const chatSortOptions = {
|
export const chatSortOptions = {
|
||||||
name: { text: 'Name', icon: faArrowDownAZ, value: '', sortFn: (a, b) => { return a.name < b.name ? -1 : a.name > b.name ? 1 : 0 } },
|
name: { text: 'Name', icon: faArrowDownAZ, value: '', sortFn: (a, b) => { return a.name < b.name ? -1 : a.name > b.name ? 1 : 0 } },
|
||||||
created: { text: 'Created', icon: faArrowDown91, value: '', sortFn: (a, b) => { return ((b.created || 0) - (a.created || 0)) || (b.id - a.id) } },
|
created: { text: 'Created', icon: faArrowDown91, value: '', sortFn: (a, b) => { return ((b.created || 0) - (a.created || 0)) || a.id.localeCompare(b.id) } },
|
||||||
lastUse: { text: 'Last Use', icon: faArrowDown91, value: '', sortFn: (a, b) => { return ((b.lastUse || 0) - (a.lastUse || 0)) || (b.id - a.id) } },
|
lastUse: { text: 'Last Use', icon: faArrowDown91, value: '', sortFn: (a, b) => { return ((b.lastUse || 0) - (a.lastUse || 0)) || a.id.localeCompare(b.id) } },
|
||||||
lastAccess: { text: 'Last View', icon: faArrowDown91, value: '', sortFn: (a, b) => { return ((b.lastAccess || 0) - (a.lastAccess || 0)) || (b.id - a.id) } }
|
lastAccess: { text: 'Last View', icon: faArrowDown91, value: '', sortFn: (a, b) => { return ((b.lastAccess || 0) - (a.lastAccess || 0)) || a.id.localeCompare(b.id) } }
|
||||||
} as Record<string, ChatSortOption>
|
} as Record<string, ChatSortOption>
|
||||||
|
|
||||||
Object.entries(chatSortOptions).forEach(([k, o]) => { o.value = k })
|
Object.entries(chatSortOptions).forEach(([k, o]) => { o.value = k })
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
|
<script context="module" lang="ts">
|
||||||
|
import { writable } from 'svelte/store'
|
||||||
|
|
||||||
|
// Export sidebar collapse state so other components can react to it
|
||||||
|
export const sidebarCollapsed = writable(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { params } from 'svelte-spa-router'
|
import { params } from 'svelte-spa-router'
|
||||||
import ChatMenuItem from './ChatMenuItem.svelte'
|
import ChatMenuItem from './ChatMenuItem.svelte'
|
||||||
import { chatsStorage, pinMainMenu, checkStateChange, getChatSortOption, setChatSortOption } from './Storage.svelte'
|
import { chatsStorage, pinMainMenu, checkStateChange, getChatSortOption, setChatSortOption } from './Storage.svelte'
|
||||||
import Fa from 'svelte-fa/src/fa.svelte'
|
import Fa from 'svelte-fa/src/fa.svelte'
|
||||||
import { faSquarePlus, faKey, faDownload, faRotate, faUpload } from '@fortawesome/free-solid-svg-icons/index'
|
import { faSquarePlus, faKey, faBars, faAngleLeft, faAngleRight } from '@fortawesome/free-solid-svg-icons/index'
|
||||||
import ChatOptionMenu from './ChatOptionMenu.svelte'
|
import ChatOptionMenu from './ChatOptionMenu.svelte'
|
||||||
import logo from '../assets/logo.svg'
|
import logo from '../assets/logo.svg'
|
||||||
import { clickOutside } from 'svelte-use-click-outside'
|
import { clickOutside } from 'svelte-use-click-outside'
|
||||||
@@ -17,7 +24,7 @@
|
|||||||
let lastSortOption: any = null
|
let lastSortOption: any = null
|
||||||
let lastChatsLength = 0
|
let lastChatsLength = 0
|
||||||
|
|
||||||
$: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefined
|
$: activeChatId = $params && $params.chatId ? $params.chatId : undefined
|
||||||
|
|
||||||
let sortOption = getChatSortOption()
|
let sortOption = getChatSortOption()
|
||||||
let hasModels = hasActiveModels()
|
let hasModels = hasActiveModels()
|
||||||
@@ -43,6 +50,17 @@
|
|||||||
$: onStateChange($checkStateChange)
|
$: onStateChange($checkStateChange)
|
||||||
|
|
||||||
let showSortMenu = false
|
let showSortMenu = false
|
||||||
|
let isCollapsed = false
|
||||||
|
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
isCollapsed = !isCollapsed
|
||||||
|
sidebarCollapsed.set(isCollapsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to the store to keep local state in sync
|
||||||
|
sidebarCollapsed.subscribe(value => {
|
||||||
|
isCollapsed = value
|
||||||
|
})
|
||||||
|
|
||||||
async function uploadLocalStorage (uid = 19492) {
|
async function uploadLocalStorage (uid = 19492) {
|
||||||
try {
|
try {
|
||||||
@@ -166,20 +184,52 @@
|
|||||||
// setInterval(syncLocalStorage, 10000);
|
// setInterval(syncLocalStorage, 10000);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside class="menu main-menu" class:pinned={$pinMainMenu} use:clickOutside={() => { $pinMainMenu = false }}>
|
<aside class="menu main-menu modern-sidebar" class:collapsed={isCollapsed} class:pinned={$pinMainMenu} use:clickOutside={() => { $pinMainMenu = false }}>
|
||||||
<div style="font-size:8px;position:fixed;top:1px;right:2px;">&&&BUILDVER&&&</div>
|
<div class="sidebar-content">
|
||||||
<div class="menu-expanse">
|
<!-- Header with logo and collapse button -->
|
||||||
<div class="navbar-brand menu-nav-bar">
|
<div class="sidebar-header">
|
||||||
<a class="navbar-item gpt-logo" href={'#/'}>
|
{#if !isCollapsed}
|
||||||
|
|
||||||
|
<div class="header-content">
|
||||||
|
<a class="logo-container" href={'#/'}>
|
||||||
<img src={logo} alt="ChatGPT-web" width="24" height="24" />
|
<img src={logo} alt="ChatGPT-web" width="24" height="24" />
|
||||||
</a>
|
</a>
|
||||||
<div class="chat-option-menu navbar-item is-pulled-right">
|
<div class="collapse-section">
|
||||||
|
<div class="chat-option-menu-container">
|
||||||
|
<ChatOptionMenu bind:chatId={activeChatId} />
|
||||||
|
</div>
|
||||||
|
<button class="collapse-button" on:click={toggleSidebar} title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}>
|
||||||
|
<Fa icon={isCollapsed ? faAngleRight : faAngleLeft} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{:else}
|
||||||
|
|
||||||
|
<div class="header-content-collapsed">
|
||||||
|
<a class="logo-container-collapsed" href={'#/'}>
|
||||||
|
<img src={logo} alt="ChatGPT-web" width="24" height="24" />
|
||||||
|
</a>
|
||||||
|
<div class="collapse-section">
|
||||||
|
<button class="collapse-button" on:click={toggleSidebar} title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}>
|
||||||
|
<Fa icon={isCollapsed ? faAngleRight : faAngleLeft} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="chat-option-menu-container-collapsed">
|
||||||
<ChatOptionMenu bind:chatId={activeChatId} />
|
<ChatOptionMenu bind:chatId={activeChatId} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="menu-list menu-expansion-list">
|
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chat list -->
|
||||||
|
<div class="chat-list" class:collapsed={isCollapsed}>
|
||||||
|
{#if !isCollapsed}
|
||||||
{#if sortedChats.length === 0}
|
{#if sortedChats.length === 0}
|
||||||
<li><a href={'#'} class="is-disabled">No chats yet...</a></li>
|
<div class="empty-state">
|
||||||
|
<span>No chats yet...</span>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#key $checkStateChange}
|
{#key $checkStateChange}
|
||||||
{#each sortedChats as chat, i}
|
{#each sortedChats as chat, i}
|
||||||
@@ -189,17 +239,38 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
{/if}
|
||||||
<!-- <p class="menu-label">Actions</p> -->
|
</div>
|
||||||
<div class="level is-mobile bottom-buttons p-1">
|
|
||||||
<div class="level-left">
|
<!-- Bottom controls -->
|
||||||
<div class="dropdown is-left is-up" class:is-active={showSortMenu} use:clickOutside={() => { showSortMenu = false }}>
|
<div class="sidebar-footer" class:collapsed={isCollapsed}>
|
||||||
|
{#if !isCollapsed}
|
||||||
|
<div class="footer-controls">
|
||||||
|
<div class="new-chat-section">
|
||||||
|
{#if hasModels}
|
||||||
|
<button
|
||||||
|
class="new-chat-button"
|
||||||
|
on:click={() => { $pinMainMenu = false; startNewChatWithWarning(activeChatId) }}
|
||||||
|
title="Start new chat">
|
||||||
|
<Fa icon={faSquarePlus} />
|
||||||
|
<span>New Chat</span>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<a href={'#/'} class="new-chat-button api-settings" title="Set up API key">
|
||||||
|
<Fa icon={faKey} />
|
||||||
|
<span>API Settings</span>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="sort-controls">
|
||||||
|
<div class="dropdown is-up" class:is-active={showSortMenu} use:clickOutside={() => { showSortMenu = false }}>
|
||||||
<div class="dropdown-trigger">
|
<div class="dropdown-trigger">
|
||||||
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu3" on:click|preventDefault|stopPropagation={() => { showSortMenu = !showSortMenu }}>
|
<button class="control-button" on:click|preventDefault|stopPropagation={() => { showSortMenu = !showSortMenu }} title="Sort chats">
|
||||||
<span class="icon"><Fa icon={sortOption.icon}/></span>
|
<Fa icon={sortOption.icon}/>
|
||||||
|
<span>Sort</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-menu" id="dropdown-menu3" role="menu">
|
<div class="dropdown-menu" role="menu">
|
||||||
<div class="dropdown-content">
|
<div class="dropdown-content">
|
||||||
{#each Object.values(chatSortOptions) as opt}
|
{#each Object.values(chatSortOptions) as opt}
|
||||||
<a href={'#'} class="dropdown-item" class:is-active={sortOption === opt} on:click|preventDefault={() => { showSortMenu = false; setChatSortOption(opt.value) }}>
|
<a href={'#'} class="dropdown-item" class:is-active={sortOption === opt} on:click|preventDefault={() => { showSortMenu = false; setChatSortOption(opt.value) }}>
|
||||||
@@ -210,30 +281,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="is-left is-up ml-2">
|
|
||||||
<button class="button" aria-haspopup="true" on:click|preventDefault|stopPropagation={() => { loadLocalStorage() }}>
|
|
||||||
<span class="icon"><Fa icon={faUpload}/></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="is-left is-up ml-2">
|
|
||||||
<button class="button" aria-haspopup="true" on:click|preventDefault|stopPropagation={() => { dumpLocalStorage() }}>
|
|
||||||
<span class="icon"><Fa icon={faDownload}/></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="level-right">
|
|
||||||
{#if !hasModels}
|
|
||||||
<div class="level-item">
|
|
||||||
<a href={'#/'} class="panel-block" class:is-disabled={!hasModels}
|
|
||||||
><span class="greyscale mr-1"><Fa icon={faKey} /></span> API Setting</a
|
|
||||||
></div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="level-item">
|
<div class="footer-controls-collapsed">
|
||||||
<button on:click={() => { $pinMainMenu = false; startNewChatWithWarning(activeChatId) }} class="panel-block button" title="Start new chat with default profile" class:is-disabled={!hasModels}
|
<div class="new-chat-section-collapsed">
|
||||||
><span class="greyscale"><Fa icon={faSquarePlus} /></span></button>
|
{#if hasModels}
|
||||||
|
<button
|
||||||
|
class="new-chat-button-collapsed"
|
||||||
|
on:click={() => { $pinMainMenu = false; startNewChatWithWarning(activeChatId) }}
|
||||||
|
title="Start new chat">
|
||||||
|
<Fa icon={faSquarePlus} />
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<a href={'#/'} class="new-chat-button-collapsed api-settings" title="Set up API key">
|
||||||
|
<Fa icon={faKey} />
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="dropdown is-up is-right" class:is-active={showSortMenu} use:clickOutside={() => { showSortMenu = false }}>
|
||||||
|
<div class="dropdown-trigger">
|
||||||
|
<button class="control-button-collapsed" on:click|preventDefault|stopPropagation={() => { showSortMenu = !showSortMenu }} title="Sort chats">
|
||||||
|
<Fa icon={sortOption.icon}/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu" role="menu">
|
||||||
|
<div class="dropdown-content">
|
||||||
|
{#each Object.values(chatSortOptions) as opt}
|
||||||
|
<a href={'#'} class="dropdown-item" class:is-active={sortOption === opt} on:click|preventDefault={() => { showSortMenu = false; setChatSortOption(opt.value) }}>
|
||||||
|
<span class="menu-icon"><Fa icon={opt.icon}/></span>
|
||||||
|
{opt.text}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { getChatSettingObjectByKey, getGlobalSettingObjectByKey, getChatDefaults, getExcludeFromProfile, chatSortOptions, globalDefaults } from './Settings.svelte'
|
import { getChatSettingObjectByKey, getGlobalSettingObjectByKey, getChatDefaults, getExcludeFromProfile, chatSortOptions, globalDefaults } from './Settings.svelte'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { getProfile, getProfiles, isStaticProfile, newNameForProfile, restartProfile } from './Profiles.svelte'
|
import { getProfile, getProfiles, isStaticProfile, newNameForProfile, restartProfile } from './Profiles.svelte'
|
||||||
import { errorNotice } from './Util.svelte'
|
import { errorNotice, generateShortId } from './Util.svelte'
|
||||||
import { clearAllImages, deleteImage, setImage } from './ImageStore.svelte'
|
import { clearAllImages, deleteImage, setImage } from './ImageStore.svelte'
|
||||||
|
|
||||||
// TODO: move chatsStorage to indexedDB with localStorage as a fallback for private browsing.
|
// TODO: move chatsStorage to indexedDB with localStorage as a fallback for private browsing.
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
export let continueMessage = writable('') //
|
export let continueMessage = writable('') //
|
||||||
export let currentChatMessages = writable([] as Message[])
|
export let currentChatMessages = writable([] as Message[])
|
||||||
export let started = writable(false)
|
export let started = writable(false)
|
||||||
export let currentChatId = writable(0)
|
export let currentChatId = writable('')
|
||||||
export let lastChatId = persisted('lastChatId', 0)
|
export let lastChatId = persisted('lastChatId', '')
|
||||||
|
|
||||||
const chatDefaults = getChatDefaults()
|
const chatDefaults = getChatDefaults()
|
||||||
|
|
||||||
@@ -31,25 +31,22 @@
|
|||||||
return get(apiKeyStorage)
|
return get(apiKeyStorage)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const newChatID = (): number => {
|
export const newChatID = (): string => {
|
||||||
const chats = get(chatsStorage)
|
return generateShortId()
|
||||||
const chatId = chats.reduce((maxId, chat) => Math.max(maxId, chat.id), 0) + 1
|
|
||||||
return chatId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addChat = (profile:ChatSettings|undefined = undefined): number => {
|
export const addChat = (profile:ChatSettings|undefined = undefined): string => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
|
|
||||||
// Find the max chatId
|
// Generate new short UUID
|
||||||
const chatId = newChatID()
|
const chatId = newChatID()
|
||||||
|
|
||||||
profile = JSON.parse(JSON.stringify(profile || getProfile(''))) as ChatSettings
|
profile = JSON.parse(JSON.stringify(profile || getProfile(''))) as ChatSettings
|
||||||
const nameMap = chats.reduce((a, chat) => { a[chat.name] = chat; return a }, {})
|
|
||||||
|
|
||||||
// Add a new chat
|
// Add a new chat
|
||||||
chats.push({
|
chats.push({
|
||||||
id: chatId,
|
id: chatId,
|
||||||
name: newName(`Chat ${chatId}`, nameMap),
|
name: `New Chat`,
|
||||||
settings: profile,
|
settings: profile,
|
||||||
messages: [],
|
messages: [],
|
||||||
usage: {} as Record<Model, Usage>,
|
usage: {} as Record<Model, Usage>,
|
||||||
@@ -65,7 +62,7 @@
|
|||||||
return chatId
|
return chatId
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addChatFromJSON = async (json: string): Promise<number> => {
|
export const addChatFromJSON = async (json: string): Promise<string> => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
|
|
||||||
// Find the max chatId
|
// Find the max chatId
|
||||||
@@ -74,13 +71,13 @@
|
|||||||
let chat: Chat
|
let chat: Chat
|
||||||
try {
|
try {
|
||||||
chat = JSON.parse(json) as Chat
|
chat = JSON.parse(json) as Chat
|
||||||
if (!chat.settings || !chat.messages || isNaN(chat.id)) {
|
if (!chat.settings || !chat.messages || !chat.id) {
|
||||||
errorNotice('Not valid Chat JSON')
|
errorNotice('Not valid Chat JSON')
|
||||||
return 0
|
return ''
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorNotice("Can't parse file JSON")
|
errorNotice("Can't parse file JSON")
|
||||||
return 0
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
chat.id = chatId
|
chat.id = chatId
|
||||||
@@ -99,7 +96,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make sure a chat's settings are set with current values or defaults
|
// Make sure a chat's settings are set with current values or defaults
|
||||||
export const updateChatSettings = (chatId:number) => {
|
export const updateChatSettings = (chatId:string) => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
if (!chat.settings) {
|
if (!chat.settings) {
|
||||||
@@ -152,7 +149,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reset all setting to current profile defaults
|
// Reset all setting to current profile defaults
|
||||||
export const resetChatSettings = (chatId, resetAll:boolean = false) => {
|
export const resetChatSettings = (chatId: string, resetAll:boolean = false) => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
const profile = getProfile(chat.settings.profile)
|
const profile = getProfile(chat.settings.profile)
|
||||||
@@ -179,17 +176,17 @@
|
|||||||
chatsStorage.set(chats)
|
chatsStorage.set(chats)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getChat = (chatId: number):Chat => {
|
export const getChat = (chatId: string):Chat => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
return chats.find((chat) => chat.id === chatId) as Chat
|
return chats.find((chat) => chat.id === chatId) as Chat
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getChatSettings = (chatId: number):ChatSettings => {
|
export const getChatSettings = (chatId: string):ChatSettings => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
return (chats.find((chat) => chat.id === chatId) as Chat).settings
|
return (chats.find((chat) => chat.id === chatId) as Chat).settings
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateRunningTotal = (chatId: number, usage: Usage, model:Model) => {
|
export const updateRunningTotal = (chatId: string, usage: Usage, model:Model) => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
let total:Usage = chat.usage[model]
|
let total:Usage = chat.usage[model]
|
||||||
@@ -207,7 +204,7 @@
|
|||||||
chatsStorage.set(chats)
|
chatsStorage.set(chats)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const subtractRunningTotal = (chatId: number, usage: Usage, model:Model) => {
|
export const subtractRunningTotal = (chatId: string, usage: Usage, model:Model) => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
let total:Usage = chat.usage[model]
|
let total:Usage = chat.usage[model]
|
||||||
@@ -225,17 +222,17 @@
|
|||||||
chatsStorage.set(chats)
|
chatsStorage.set(chats)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMessages = (chatId: number): Message[] => {
|
export const getMessages = (chatId: string): Message[] => {
|
||||||
if (get(currentChatId) === chatId) return get(currentChatMessages)
|
if (get(currentChatId) === chatId) return get(currentChatMessages)
|
||||||
return getChat(chatId).messages
|
return getChat(chatId).messages
|
||||||
}
|
}
|
||||||
|
|
||||||
let setChatTimer: any
|
let setChatTimer: any
|
||||||
export const setCurrentChat = (chatId: number) => {
|
export const setCurrentChat = (chatId: string) => {
|
||||||
clearTimeout(setChatTimer)
|
clearTimeout(setChatTimer)
|
||||||
if (!chatId) {
|
if (!chatId) {
|
||||||
currentChatId.set(0)
|
currentChatId.set('')
|
||||||
lastChatId.set(0)
|
lastChatId.set('')
|
||||||
currentChatMessages.set([])
|
currentChatMessages.set([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -246,8 +243,8 @@
|
|||||||
}, 10)
|
}, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
const signalChangeTimers = new Map<number, any>()
|
const signalChangeTimers = new Map<string, any>()
|
||||||
const setChatLastUse = (chatId: number, time: number) => {
|
const setChatLastUse = (chatId: string, time: number) => {
|
||||||
const existingTimer = signalChangeTimers.get(chatId)
|
const existingTimer = signalChangeTimers.get(chatId)
|
||||||
if (existingTimer) {
|
if (existingTimer) {
|
||||||
clearTimeout(existingTimer)
|
clearTimeout(existingTimer)
|
||||||
@@ -260,8 +257,8 @@
|
|||||||
signalChangeTimers.set(chatId, timer)
|
signalChangeTimers.set(chatId, timer)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setMessagesTimers = new Map<number, any>()
|
const setMessagesTimers = new Map<string, any>()
|
||||||
export const setMessages = (chatId: number, messages: Message[]) => {
|
export const setMessages = (chatId: string, messages: Message[]) => {
|
||||||
if (get(currentChatId) === chatId) {
|
if (get(currentChatId) === chatId) {
|
||||||
// update current message cache right away
|
// update current message cache right away
|
||||||
currentChatMessages.set(messages)
|
currentChatMessages.set(messages)
|
||||||
@@ -289,7 +286,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateMessages = (chatId: number) => {
|
export const updateMessages = (chatId: string) => {
|
||||||
setMessages(chatId, getMessages(chatId))
|
setMessages(chatId, getMessages(chatId))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,11 +308,11 @@
|
|||||||
setMessagesTimers.clear()
|
setMessagesTimers.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addError = (chatId: number, error: string) => {
|
export const addError = (chatId: string, error: string) => {
|
||||||
addMessage(chatId, { content: error } as Message)
|
addMessage(chatId, { content: error } as Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addMessage = (chatId: number, message: Message) => {
|
export const addMessage = (chatId: string, message: Message) => {
|
||||||
const messages = getMessages(chatId)
|
const messages = getMessages(chatId)
|
||||||
if (!message.uuid) message.uuid = uuidv4()
|
if (!message.uuid) message.uuid = uuidv4()
|
||||||
if (!message.created) message.created = Date.now()
|
if (!message.created) message.created = Date.now()
|
||||||
@@ -326,11 +323,11 @@
|
|||||||
setMessages(chatId, messages)
|
setMessages(chatId, messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMessage = (chatId: number, uuid:string):Message|undefined => {
|
export const getMessage = (chatId: string, uuid:string):Message|undefined => {
|
||||||
return getMessages(chatId).find((m) => m.uuid === uuid)
|
return getMessages(chatId).find((m) => m.uuid === uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const insertMessages = (chatId: number, insertAfter: Message, newMessages: Message[]) => {
|
export const insertMessages = (chatId: string, insertAfter: Message, newMessages: Message[]) => {
|
||||||
const messages = getMessages(chatId)
|
const messages = getMessages(chatId)
|
||||||
const index = messages.findIndex((m) => m.uuid === insertAfter.uuid)
|
const index = messages.findIndex((m) => m.uuid === insertAfter.uuid)
|
||||||
if (index === undefined || index < 0) {
|
if (index === undefined || index < 0) {
|
||||||
@@ -345,7 +342,7 @@
|
|||||||
setMessages(chatId, messages.filter(m => true))
|
setMessages(chatId, messages.filter(m => true))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteSummaryMessage = (chatId: number, uuid: string) => {
|
export const deleteSummaryMessage = (chatId: string, uuid: string) => {
|
||||||
const message = getMessage(chatId, uuid)
|
const message = getMessage(chatId, uuid)
|
||||||
if (message && message.summarized) throw new Error('Unable to delete summarized message')
|
if (message && message.summarized) throw new Error('Unable to delete summarized message')
|
||||||
if (message && message.summary) { // messages we summarized
|
if (message && message.summary) { // messages we summarized
|
||||||
@@ -361,7 +358,7 @@
|
|||||||
deleteMessage(chatId, uuid)
|
deleteMessage(chatId, uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteMessage = (chatId: number, uuid: string) => {
|
export const deleteMessage = (chatId: string, uuid: string) => {
|
||||||
const messages = getMessages(chatId)
|
const messages = getMessages(chatId)
|
||||||
const index = messages.findIndex((m) => m.uuid === uuid)
|
const index = messages.findIndex((m) => m.uuid === uuid)
|
||||||
const message = getMessage(chatId, uuid)
|
const message = getMessage(chatId, uuid)
|
||||||
@@ -379,13 +376,13 @@
|
|||||||
setMessages(chatId, messages.filter(m => true))
|
setMessages(chatId, messages.filter(m => true))
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearImages = (chatId: number, messages: Message[]) => {
|
const clearImages = (chatId: string, messages: Message[]) => {
|
||||||
messages.forEach(m => {
|
messages.forEach(m => {
|
||||||
if (m.image) deleteImage(chatId, m.image.id)
|
if (m.image) deleteImage(chatId, m.image.id)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const truncateFromMessage = (chatId: number, uuid: string) => {
|
export const truncateFromMessage = (chatId: string, uuid: string) => {
|
||||||
const messages = getMessages(chatId)
|
const messages = getMessages(chatId)
|
||||||
const index = messages.findIndex((m) => m.uuid === uuid)
|
const index = messages.findIndex((m) => m.uuid === uuid)
|
||||||
const message = getMessage(chatId, uuid)
|
const message = getMessage(chatId, uuid)
|
||||||
@@ -398,18 +395,18 @@
|
|||||||
setMessages(chatId, messages.filter(m => true))
|
setMessages(chatId, messages.filter(m => true))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clearMessages = (chatId: number) => {
|
export const clearMessages = (chatId: string) => {
|
||||||
clearImages(chatId, getMessages(chatId))
|
clearImages(chatId, getMessages(chatId))
|
||||||
setMessages(chatId, [])
|
setMessages(chatId, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteChat = (chatId: number) => {
|
export const deleteChat = (chatId: string) => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
clearImages(chatId, getMessages(chatId) || [])
|
clearImages(chatId, getMessages(chatId) || [])
|
||||||
chatsStorage.set(chats.filter((chat) => chat.id !== chatId))
|
chatsStorage.set(chats.filter((chat) => chat.id !== chatId))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateChatImages = async (chatId: number, chat: Chat) => {
|
export const updateChatImages = async (chatId: string, chat: Chat) => {
|
||||||
const messages = chat.messages
|
const messages = chat.messages
|
||||||
for (let i = 0; i < messages.length; i++) {
|
for (let i = 0; i < messages.length; i++) {
|
||||||
const m = messages[i]
|
const m = messages[i]
|
||||||
@@ -417,7 +414,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const copyChat = async (chatId: number) => {
|
export const copyChat = async (chatId: string) => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
const nameMap = chats.reduce((a, chat) => { a[chat.name] = chat; return a }, {})
|
const nameMap = chats.reduce((a, chat) => { a[chat.name] = chat; return a }, {})
|
||||||
@@ -454,7 +451,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setChatSettingValueByKey = (chatId: number, key: keyof ChatSettings, value) => {
|
export const setChatSettingValueByKey = (chatId: string, key: keyof ChatSettings, value) => {
|
||||||
const setting = getChatSettingObjectByKey(key)
|
const setting = getChatSettingObjectByKey(key)
|
||||||
if (setting) return setChatSettingValue(chatId, setting, value)
|
if (setting) return setChatSettingValue(chatId, setting, value)
|
||||||
if (!(key in chatDefaults)) throw new Error('Invalid chat setting: ' + key)
|
if (!(key in chatDefaults)) throw new Error('Invalid chat setting: ' + key)
|
||||||
@@ -469,7 +466,7 @@
|
|||||||
settings[key] = cleanSettingValue(typeof d, value)
|
settings[key] = cleanSettingValue(typeof d, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setChatSettingValue = (chatId: number, setting: ChatSetting, value) => {
|
export const setChatSettingValue = (chatId: string, setting: ChatSetting, value) => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
let settings = chat.settings as any
|
let settings = chat.settings as any
|
||||||
@@ -481,7 +478,7 @@
|
|||||||
chatsStorage.set(chats)
|
chatsStorage.set(chats)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getChatSettingValueNullDefault = (chatId: number, setting: ChatSetting):any => {
|
export const getChatSettingValueNullDefault = (chatId: string, setting: ChatSetting):any => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
let value = chat.settings && chat.settings[setting.key]
|
let value = chat.settings && chat.settings[setting.key]
|
||||||
@@ -515,7 +512,7 @@
|
|||||||
return store.profiles || {}
|
return store.profiles || {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteCustomProfile = (chatId:number, profileId:string) => {
|
export const deleteCustomProfile = (chatId:string, profileId:string) => {
|
||||||
if (isStaticProfile(profileId)) {
|
if (isStaticProfile(profileId)) {
|
||||||
throw new Error('Sorry, you can\'t delete a static profile.')
|
throw new Error('Sorry, you can\'t delete a static profile.')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export type ChatSettings = {
|
|||||||
} & Request;
|
} & Request;
|
||||||
|
|
||||||
export type Chat = {
|
export type Chat = {
|
||||||
id: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
usage: Record<Model, Usage>;
|
usage: Record<Model, Usage>;
|
||||||
|
|||||||
@@ -6,6 +6,17 @@
|
|||||||
import { replace } from 'svelte-spa-router'
|
import { replace } from 'svelte-spa-router'
|
||||||
// import PromptConfirm from './PromptConfirm.svelte'
|
// import PromptConfirm from './PromptConfirm.svelte'
|
||||||
import type { ChatSettings } from './Types.svelte'
|
import type { ChatSettings } from './Types.svelte'
|
||||||
|
|
||||||
|
// Generate a short UUID (8 characters) for chat IDs using hex format (0-9a-f)
|
||||||
|
export const generateShortId = (): string => {
|
||||||
|
const chars = '0123456789abcdef'
|
||||||
|
let result = ''
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// Cache for auto-size elements to avoid expensive DOM queries
|
// Cache for auto-size elements to avoid expensive DOM queries
|
||||||
let cachedAutoSizeElements: HTMLTextAreaElement[] = []
|
let cachedAutoSizeElements: HTMLTextAreaElement[] = []
|
||||||
let lastElementCount = 0
|
let lastElementCount = 0
|
||||||
@@ -137,13 +148,13 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const startNewChatFromChatId = (chatId: number) => {
|
export const startNewChatFromChatId = (chatId: string) => {
|
||||||
const newChatId = addChat(getChat(chatId).settings)
|
const newChatId = addChat(getChat(chatId).settings)
|
||||||
// go to new chat
|
// go to new chat
|
||||||
replace(`/chat/${newChatId}`)
|
replace(`/chat/${newChatId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const startNewChatWithWarning = (activeChatId: number|undefined, profile?: ChatSettings|undefined) => {
|
export const startNewChatWithWarning = (activeChatId: string|undefined, profile?: ChatSettings|undefined) => {
|
||||||
const newChat = () => {
|
const newChat = () => {
|
||||||
const chatId = addChat(profile)
|
const chatId = addChat(profile)
|
||||||
replace(`/chat/${chatId}`)
|
replace(`/chat/${chatId}`)
|
||||||
@@ -164,11 +175,64 @@
|
|||||||
newChat()
|
newChat()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const valueOf = (chatId: number, value: any) => {
|
export const valueOf = (chatId: string, value: any) => {
|
||||||
if (typeof value === 'function') return value(chatId)
|
if (typeof value === 'function') return value(chatId)
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration function to convert old numeric chat IDs to hex UUIDs
|
||||||
|
export const migrateChatData = () => {
|
||||||
|
try {
|
||||||
|
const chatsDataString = localStorage.getItem('chats')
|
||||||
|
if (!chatsDataString) {
|
||||||
|
console.log('No chat data found to migrate')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatsData = JSON.parse(chatsDataString)
|
||||||
|
if (!Array.isArray(chatsData) || chatsData.length === 0) {
|
||||||
|
console.log('No chats to migrate')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let migratedCount = 0
|
||||||
|
const migrationMap = new Map() // old ID -> new ID mapping
|
||||||
|
|
||||||
|
// First pass: identify chats with numeric IDs and create new UUIDs
|
||||||
|
chatsData.forEach(chat => {
|
||||||
|
if (typeof chat.id === 'number') {
|
||||||
|
const newId = generateShortId()
|
||||||
|
migrationMap.set(chat.id, newId)
|
||||||
|
chat.id = newId
|
||||||
|
migratedCount++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (migratedCount === 0) {
|
||||||
|
console.log('No numeric chat IDs found to migrate')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update lastChatId if it was numeric
|
||||||
|
const lastChatIdString = localStorage.getItem('lastChatId')
|
||||||
|
if (lastChatIdString) {
|
||||||
|
const lastChatId = JSON.parse(lastChatIdString)
|
||||||
|
if (typeof lastChatId === 'number' && migrationMap.has(lastChatId)) {
|
||||||
|
localStorage.setItem('lastChatId', JSON.stringify(migrationMap.get(lastChatId)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save migrated data back to localStorage
|
||||||
|
localStorage.setItem('chats', JSON.stringify(chatsData))
|
||||||
|
|
||||||
|
console.log(`Successfully migrated ${migratedCount} chats from numeric IDs to hex UUIDs`)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during chat data migration:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const escapeRegex = (string: string): string => {
|
export const escapeRegex = (string: string): string => {
|
||||||
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
|
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user