Skip to content

Commit d9d1d1e

Browse files
committed
Merge eimi-windows-clean
1 parent 80bef21 commit d9d1d1e

17 files changed

Lines changed: 1786 additions & 382 deletions

app/src/App.svelte

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
<script>
22
import SessionPage from './SessionPage.svelte'
3+
import SessionList from './SessionList.svelte'
34
import Settings from './Settings.svelte'
45
import SearchPage from './SearchPage.svelte'
5-
import WindowManager from './lib/WindowManager.svelte'
6-
import { genSessionId, openEmptySession } from './utils.js'
6+
import { genSessionId } from './utils.js'
77
import { themeStore } from './themeStore.js'
88
import Theme from './Theme.svelte'
99
import Welcome from './Welcome.svelte'
1010
11+
window.svStateSnapshot = (obj) => ($state.snapshot(obj))
12+
1113
let hash = $state(window.location.hash.slice(1))
14+
let sessionsOpen = $state(false)
1215
1316
window.addEventListener('popstate', () => {
1417
window.scrollTo(0, 0)
@@ -42,13 +45,25 @@ let {page, props} = $derived.by(() => {
4245
return {page, props}
4346
})
4447
48+
let isSessionPage = $derived(page === 'session' && !!props.sessionId)
49+
let sessionLeftInset = $derived(sessionsOpen ? 'calc(24ch + 8px)' : '8px')
50+
51+
$effect(() => {
52+
if (!isSessionPage) sessionsOpen = false
53+
})
54+
4555
</script>
4656

4757
<Theme/>
4858

4959
<div class="header">
5060
<!-- svelte-ignore a11y_invalid_attribute -->
5161
<div class="home"><a href="#" class="no-vs">Eimi</a></div>
62+
{#if isSessionPage}
63+
<button onclick={() => sessionsOpen = !sessionsOpen} aria-pressed={sessionsOpen}>
64+
sessions
65+
</button>
66+
{/if}
5267
<div class='ml-auto'></div>
5368
<div class="settings">
5469
<a
@@ -72,8 +87,13 @@ let {page, props} = $derived.by(() => {
7287
</div>
7388

7489
<div class="page">
75-
{#if page === "session" && props.sessionId}
76-
<SessionPage {...props}/>
90+
{#if isSessionPage}
91+
{#if sessionsOpen}
92+
<aside class="sidebar">
93+
<SessionList sessionId={props.sessionId} />
94+
</aside>
95+
{/if}
96+
<SessionPage {...props} leftInset={sessionLeftInset} />
7797
{/if}
7898
{#if page === "search"}
7999
<SearchPage {...props}/>
@@ -84,7 +104,6 @@ let {page, props} = $derived.by(() => {
84104
</div>
85105

86106
<Welcome/>
87-
<WindowManager/>
88107

89108
<style>
90109
:global(html, body) {
@@ -116,6 +135,17 @@ let {page, props} = $derived.by(() => {
116135
width: 100%;
117136
overflow-x: hidden;
118137
}
138+
139+
.sidebar {
140+
width: calc(24ch + 8px);
141+
height: calc(100vh - 32px);
142+
position: fixed;
143+
left: 0;
144+
top: 32px;
145+
overflow: hidden;
146+
z-index: 5;
147+
}
148+
119149
.settings {
120150
right: 16px;
121151
}

app/src/Session.svelte

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ import { SessionState, USER, ASSISTANT, SYSTEM } from './SessionState.svelte.js'
1515
let props = $props()
1616
const sessionId = props.sessionId // https://github.com/sveltejs/svelte/issues/15697
1717
const autoReply = props.autoReply
18+
const windowState = props.state
1819
if (!sessionId) { throw new Error('sessionId is required') }
1920
2021
const sessionState = new SessionState({ sessionId, autoReply })
2122
const sessionScripts = sessionState.sessionScripts
2223
let sessionLoaded = $derived(sessionState.sessionLoaded)
2324
let sessionData = $derived(sessionState.sessionData)
2425
let messages = $derived(sessionState.messages)
26+
let layoutLoaded = $state(false)
2527
let messageStats = $derived.by(() => {
2628
const cnt = messages.length
2729
let roots = 0
@@ -37,6 +39,8 @@ let messageStats = $derived.by(() => {
3739
3840
onMount(async () => {
3941
await sessionState.loadSession()
42+
windowState?.loadSessionLayout(sessionState.sessionData?.windowLayout)
43+
layoutLoaded = true
4044
const savedScrollY = sessionStorage.getItem(`scroll-${sessionId}`)
4145
if (savedScrollY) {
4246
await tick()
@@ -68,13 +72,19 @@ onMount(() => {
6872
})
6973
7074
onDestroy(async () => {
75+
if (sessionState.sessionData) sessionState.sessionData.windowLayout = windowState?.getSessionLayout()
7176
await sessionState.destroy()
7277
})
7378
7479
$effect(() => {
7580
if (sessionData) document.title = `${sessionData.title} - Eimi LLM UI`
7681
})
7782
83+
$effect(() => {
84+
if (!layoutLoaded || !sessionLoaded || !sessionData || !windowState) return
85+
sessionData.windowLayout = windowState.getSessionLayout()
86+
})
87+
7888
function getMessageFromEvent(event) {
7989
const el = event.target.closest('.message')
8090
if (!el) { return }
@@ -229,27 +239,6 @@ function createMessage(values, index = 0) {
229239
}
230240
</script>
231241
232-
{#if sessionLoaded}
233-
<SessionFaviconChanger {messages} />
234-
<SessionHotkeys {messages} {genResponse} {onCreateMessage} {getMessageFromEvent} {deleteMessage} {moveMessage}/>
235-
<Parameters
236-
{sessionId}
237-
bind:parameters={sessionData.parameters}
238-
scripts={sessionScripts.scripts}
239-
/>
240-
241-
<div>
242-
<input class="title-input" value={sessionData.title} oninput={onTitleUpdate} />
243-
<button onclick={onDup} title="make a copy of this session">dup</button>
244-
<button onclick={onDelete} title="delete session (hold shift)">delete</button>
245-
</div>
246-
<div class="meta-gray">
247-
{new Date(sessionData.createdAt).toLocaleString()}
248-
| messages: {messageStats.cnt}
249-
| roots: {messageStats.roots}
250-
| leafs: {messageStats.leafs}
251-
</div>
252-
253242
{#snippet renderMessage(c, i)}
254243
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
255244
<div class="message-content" class:generating="{c.generating}" tabindex="0">
@@ -375,6 +364,29 @@ function createMessage(values, index = 0) {
375364
</div>
376365
{/snippet}
377366
367+
<div class="session-view">
368+
{#if sessionLoaded}
369+
<SessionFaviconChanger {messages} />
370+
<SessionHotkeys {messages} {genResponse} {onCreateMessage} {getMessageFromEvent} {deleteMessage} {moveMessage}/>
371+
<Parameters
372+
state={windowState}
373+
{sessionId}
374+
bind:parameters={sessionData.parameters}
375+
scripts={sessionScripts.scripts}
376+
/>
377+
378+
<div>
379+
<input class="title-input" value={sessionData.title} oninput={onTitleUpdate} />
380+
<button onclick={onDup} title="make a copy of this session">dup</button>
381+
<button onclick={onDelete} title="delete session (hold shift)">delete</button>
382+
</div>
383+
<div class="meta-gray">
384+
{new Date(sessionData.createdAt).toLocaleString()}
385+
| messages: {messageStats.cnt}
386+
| roots: {messageStats.roots}
387+
| leafs: {messageStats.leafs}
388+
</div>
389+
378390
<div id="messages" class="messages">
379391
{#each messages as c, i (c.id)}
380392
<div id={`m_${c.id}`} class="message message-pad" data-id="{c.id}" data-index="{i}" class:linear="{c.linear}" style="--depth: {c.ddepth}">
@@ -396,8 +408,14 @@ function createMessage(values, index = 0) {
396408
<div id="messages" class="messages"></div>
397409
<div id="scroll-restoration" style="height: 50000px;"></div>
398410
{/if}
411+
</div>
399412
400413
<style>
414+
.session-view {
415+
display: flex;
416+
flex-direction: column;
417+
align-items: center;
418+
}
401419
.title-input {
402420
width: 40ch;
403421
}
@@ -410,6 +428,10 @@ function createMessage(values, index = 0) {
410428
color: var(--brand-hover-color);
411429
}
412430
431+
.messages {
432+
max-width: fit-content; /* tmp! */
433+
}
434+
413435
.message {
414436
position: relative;
415437
padding-left: calc(var(--depth) * 8px);

app/src/SessionList.svelte

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<script>
2+
import { db } from './db.js'
3+
import { escapeRegExp, subSessionList } from './utils.js'
4+
import VirtualList from './VirtualList.svelte'
5+
6+
let { sessionId } = $props()
7+
8+
let sessions = $state([])
9+
let searchQuery = $state('')
10+
11+
async function loadSessionsList() {
12+
const tx = (await db).transaction('sessionMeta', 'readonly')
13+
const allMeta = await tx.store.getAll()
14+
allMeta.sort((a, b) => b.id.localeCompare(a.id))
15+
sessions = allMeta
16+
}
17+
18+
$effect(() => {
19+
$subSessionList
20+
loadSessionsList()
21+
})
22+
23+
let filteredSessions = $derived.by(() => {
24+
if (searchQuery === '') return sessions
25+
let searchRe = new RegExp(escapeRegExp(searchQuery), 'i')
26+
return sessions.filter(s => searchRe.test(s.title))
27+
})
28+
</script>
29+
30+
<div class="sessions-panel">
31+
<input
32+
type="search"
33+
placeholder="Search..."
34+
value={searchQuery}
35+
oninput={(event) => searchQuery = event.currentTarget.value}
36+
/>
37+
38+
{#if filteredSessions.length > 0}
39+
<VirtualList
40+
class="sessions-list"
41+
items={filteredSessions}
42+
overscan={4}
43+
getKey={(s) => s.id}
44+
>
45+
{#snippet row({ item })}
46+
<a href={`#${item.id}`} class:session-active={item.id === sessionId}>{item.title}</a>
47+
{/snippet}
48+
</VirtualList>
49+
{:else}
50+
<div class="sessions-list sessions-empty">
51+
<span>Sessions will be here.</span>
52+
</div>
53+
{/if}
54+
</div>
55+
56+
<style>
57+
.sessions-panel {
58+
height: 100%;
59+
min-height: 0;
60+
display: flex;
61+
flex: 1;
62+
flex-direction: column;
63+
overflow: hidden;
64+
font-size: 0.94em;
65+
padding: 5px 0;
66+
background-color: var(--panel-bg-color);
67+
border-radius: 5px;
68+
margin: 0 0 0 8px;
69+
box-shadow: 0 1px 6px #00000047;
70+
margin-right: 4px;
71+
}
72+
73+
.sessions-empty span {
74+
filter: opacity(30%);
75+
margin: 10px;
76+
}
77+
78+
:global(.sessions-list) {
79+
min-height: 0;
80+
max-width: 100%;
81+
flex: 1;
82+
overflow-y: auto;
83+
}
84+
85+
:global(.sessions-list a) {
86+
min-height: 1.2em;
87+
padding: 0 5px;
88+
margin: 0 5px;
89+
color: var(--text-color);
90+
display: block;
91+
overflow: hidden;
92+
text-overflow: ellipsis;
93+
white-space: nowrap;
94+
border-radius: 2px;
95+
}
96+
97+
:global(.sessions-list a:hover) {
98+
background: #0000000f;
99+
}
100+
101+
:global(.sessions-list .session-active) {
102+
color: #fff;
103+
background: var(--brand-color) !important;
104+
}
105+
</style>

0 commit comments

Comments
 (0)