Repository: fmaclen/hollama
Pull request #32 (2024-04-21 23:17:50)

feat: flip orientation of prompt field & completions
![localhost_5173_4jrb1w](https://github.com/fmaclen/hollama/assets/1434675/ffe0abb6-9fa6-4eb6-aa82-2b0454d9bd49)
![localhost_5173_](https://github.com/fmaclen/hollama/assets/1434675/2d4c0ab0-82a9-45e0-8704-8b405cfc77c9)


cloudflare-pages[bot]: ## Deploying hollama with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>acccc1e</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://748554fd.hollama.pages.dev'>https://748554fd.hollama.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://new-session.hollama.pages.dev'>https://new-session.hollama.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/?to=/:account/pages/view/hollama/748554fd-e24c-4dc4-969e-41b17a753247)


Patch:
diff --git a/README.md b/README.md
index 8c4119b..f52f71e 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
 
 A minimal web-UI for talking to [Ollama](https://github.com/jmorganca/ollama/) servers.
 
-> ![localhost_5173_5ggycv](https://github.com/fmaclen/hollama/assets/1434675/e75b5f6b-5bb7-4912-9583-36be5d6ac54b)
-> ![localhost_5173_iavqp6 (1)](https://github.com/fmaclen/hollama/assets/1434675/8ad2e4d0-94e7-4d9e-ac62-69a05b00a77f)
+> ![localhost_5173_4jrb1w](https://github.com/fmaclen/hollama/assets/1434675/ffe0abb6-9fa6-4eb6-aa82-2b0454d9bd49)
+> ![localhost_5173_](https://github.com/fmaclen/hollama/assets/1434675/2d4c0ab0-82a9-45e0-8704-8b405cfc77c9)
 
 
 ### Features
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 1b7407e..0abb27c 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -35,6 +35,7 @@
 				<Settings class="h-4 w-4" />
 			</Button>
 			<Button
+				data-testid="new-session"
 				class="w-full"
 				variant="outline"
 				href={`/${newSessionId}`}
diff --git a/src/routes/[id]/+page.svelte b/src/routes/[id]/+page.svelte
index 5b0252e..990fca8 100644
--- a/src/routes/[id]/+page.svelte
+++ b/src/routes/[id]/+page.svelte
@@ -7,11 +7,12 @@
 	import Textarea from '$lib/components/ui/textarea/textarea.svelte';
 	import { Trash2 } from 'lucide-svelte';
 	import Article from './Article.svelte';
-	
+
 	import { sessionsStore } from '$lib/store';
 	import { ollamaGenerate, type OllamaCompletionResponse } from '$lib/ollama';
 	import { saveSession, type Message, type Session, loadSession } from '$lib/sessions';
 	import type { PageData } from './$types';
+	import { slide } from 'svelte/transition';
 
 	export let data: PageData;
 
@@ -63,7 +64,7 @@
 	async function handleSubmit() {
 		if (!prompt) return;
 
-		let message: Message = { role: 'user', content: prompt };
+		const message: Message = { role: 'user', content: prompt };
 		prompt = '';
 		completion = '';
 		session.messages = [...session.messages, message];
@@ -114,7 +115,7 @@
 <div class="flex h-screen w-full flex-col">
 	<div class="flex items-center justify-between px-6 py-4">
 		<div class="space-y-1">
-			<p data-testid="session-id" class="text-foreground text-sm font-bold leading-none">
+			<p data-testid="session-id" class="text-sm font-bold leading-none text-foreground">
 				Session <a class={_a} href={`/${session.id}`}>#{session.id}</a><br />
 			</p>
 			<p data-testid="model-name" class="text-sm text-muted-foreground">{session.model}</p>
@@ -127,23 +128,20 @@
 	<Separator />
 
 	<Resizable.PaneGroup direction="horizontal">
-		<Resizable.Pane defaultSize={40} minSize={30}>
-			<div class="flex h-full flex-col gap-y-6 bg-accent p-6">
-				<Textarea
-					placeholder="Prompt"
-					class="h-full resize-none"
-					bind:value={prompt}
-					on:keydown={handleKeyDown}
-				/>
-				<Button on:click={handleSubmit} disabled={!prompt}>Send</Button>
-			</div>
-		</Resizable.Pane>
-		<Resizable.Handle />
-		<Resizable.Pane defaultSize={40} minSize={30}>
+		<Resizable.Pane defaultSize={60} minSize={30}>
 			<div
-				class="flex h-full flex-col gap-y-4 overflow-y-auto bg-accent p-6 text-current"
+				class="flex h-full flex-col gap-y-4 overflow-y-auto bg-accent pt-6 pb-12 px-6 text-current"
 				bind:this={messageWindow}
 			>
+				{#if session.messages.length === 0}
+					<div
+						class="align-center flex h-full w-full items-center justify-center text-muted-foreground"
+						transition:slide
+					>
+						Write a prompt to start a new session
+					</div>
+				{/if}
+
 				{#each session.messages as message, i}
 					<Article {message} />
 				{/each}
@@ -153,5 +151,17 @@
 				{/if}
 			</div>
 		</Resizable.Pane>
+		<Resizable.Handle />
+		<Resizable.Pane defaultSize={40} minSize={30}>
+			<div class="flex h-full flex-col gap-y-6 bg-accent p-6">
+				<Textarea
+					placeholder="Prompt"
+					class="h-full resize-none"
+					bind:value={prompt}
+					on:keydown={handleKeyDown}
+				/>
+				<Button on:click={handleSubmit} disabled={!prompt}>Send</Button>
+			</div>
+		</Resizable.Pane>
 	</Resizable.PaneGroup>
 </div>
diff --git a/src/routes/[id]/Article.svelte b/src/routes/[id]/Article.svelte
index 53ec2fe..7b7cc09 100644
--- a/src/routes/[id]/Article.svelte
+++ b/src/routes/[id]/Article.svelte
@@ -1,4 +1,5 @@
 <script lang="ts">
+	import { slide } from 'svelte/transition';
 	import MarkdownIt from 'markdown-it';
 	import hljs from 'highlight.js';
 	import 'highlight.js/styles/github.css';
@@ -22,19 +23,20 @@
 	});
 
 	export let message: Message;
+	const isUserRole = message.role === 'user';
 
 	function copyMessage() {
 		navigator.clipboard.writeText(message.content);
 	}
 </script>
 
-<article class="mx-auto flex w-full max-w-[70ch] flex-col gap-y-3">
+<article class="mx-auto flex w-full max-w-[70ch] flex-col gap-y-3" in:slide={{duration: isUserRole ? 400 : 0 }}>
 	<nav class="grid grid-cols-[max-content_auto_max-content] items-center">
 		<p
 			data-testid="session-role"
 			class="mr-3 text-center text-xs font-bold uppercase leading-7 text-muted-foreground"
 		>
-			{message.role === 'user' ? 'You' : message.role}
+			{isUserRole ? 'You' : message.role}
 		</p>
 		<Separator />
 		<Button
@@ -48,7 +50,7 @@
 		</Button>
 	</nav>
 
-	<div id="markdown" class="text-md mx-auto w-full overflow-x-auto px-[3ch]">
+	<div id="markdown" class="text-md mx-auto w-full overflow-x-auto px-[4ch]">
 		{#if message.content}
 			{@html md.render(message.content)}
 		{/if}


Test patch:
diff --git a/tests/session.test.ts b/tests/session.test.ts
index abe4f81..334febd 100644
--- a/tests/session.test.ts
+++ b/tests/session.test.ts
@@ -8,15 +8,17 @@ test.beforeEach(async ({ page }) => {
 test('creates new session and chats', async ({ page }) => {
   const sessionIdLocator = page.getByTestId('session-id');
   const modelNameLocator = page.getByTestId('model-name');
-  const newSessionButton = page.getByText('New session');
+  const newSessionButton = page.getByTestId('new-session');
   const promptTextarea = page.getByPlaceholder('Prompt');
   const sendButton = page.getByText('Send');
   const articleLocator = page.locator('article');
+  const newPromptHelp = page.getByText('Write a prompt to start a new session');
 
   await page.goto('/');
   await chooseModelFromSettings(page, 'gemma:7b');
   await expect(sessionIdLocator).not.toBeVisible();
   await expect(modelNameLocator).not.toBeVisible();
+  await expect(newPromptHelp).not.toBeVisible();
 
   await newSessionButton.click();
   await expect(sessionIdLocator).toBeVisible();
@@ -27,6 +29,7 @@ test('creates new session and chats', async ({ page }) => {
   await expect(promptTextarea).toHaveValue('');
   await expect(sendButton).toBeVisible();
   await expect(sendButton).toBeDisabled();
+  await expect(newPromptHelp).toBeVisible();
 
   await promptTextarea.fill('Who would win in a fight between Emma Watson and Jessica Alba?');
   await expect(sendButton).toBeEnabled();
@@ -35,6 +38,7 @@ test('creates new session and chats', async ({ page }) => {
   await mockCompletionResponse(page, MOCK_SESSION_1_RESPONSE_1);
   await sendButton.click();
   await expect(page.locator('article', { hasText: 'I am unable to provide subjective or speculative information, including fight outcomes between individuals.' })).toBeVisible();
+  await expect(newPromptHelp).not.toBeVisible();
 
   await promptTextarea.fill("I understand, it's okay");
   await expect(page.locator('article', { hasText: 'No problem! If you have any other questions or would like to discuss something else, feel free to ask' })).not.toBeVisible();