feat: add chat page layout with message list and bubble components
- Create /chat route with scrollable message list - MessageBubble component with distinct user/assistant styles - MessageList with auto-scroll-to-bottom on new messages - Empty state display when no messages - ChatMessage type definition in src/lib/types.ts - Add browser globals to ESLint config for Svelte files Closes #5 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import js from '@eslint/js';
|
|||||||
import svelte from 'eslint-plugin-svelte';
|
import svelte from 'eslint-plugin-svelte';
|
||||||
import prettier from 'eslint-config-prettier';
|
import prettier from 'eslint-config-prettier';
|
||||||
import ts from 'typescript-eslint';
|
import ts from 'typescript-eslint';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
export default ts.config(
|
export default ts.config(
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
@@ -12,6 +13,9 @@ export default ts.config(
|
|||||||
{
|
{
|
||||||
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser
|
||||||
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: ts.parser
|
parser: ts.parser
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,4 @@
|
|||||||
| #2 | Proto codegen pipeline for TypeScript gRPC-Web stubs | COMPLETED | [issue-002.md](issue-002.md) |
|
| #2 | Proto codegen pipeline for TypeScript gRPC-Web stubs | COMPLETED | [issue-002.md](issue-002.md) |
|
||||||
| #3 | Configure Caddy for gRPC-Web support | COMPLETED | [issue-003.md](issue-003.md) |
|
| #3 | Configure Caddy for gRPC-Web support | COMPLETED | [issue-003.md](issue-003.md) |
|
||||||
| #4 | gRPC-Web client service layer | COMPLETED | [issue-004.md](issue-004.md) |
|
| #4 | gRPC-Web client service layer | COMPLETED | [issue-004.md](issue-004.md) |
|
||||||
|
| #5 | Chat page layout and message list component | COMPLETED | [issue-005.md](issue-005.md) |
|
||||||
|
|||||||
25
implementation-plans/issue-005.md
Normal file
25
implementation-plans/issue-005.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
# Issue #5: Chat page layout and message list component
|
||||||
|
|
||||||
|
**Status:** COMPLETED
|
||||||
|
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/5
|
||||||
|
**Branch:** `feature/issue-5-chat-layout`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Build the `/chat` route with scrollable message list, user/assistant message bubbles, auto-scroll, and empty state. Uses Svelte 5 runes.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] `/chat` route created
|
||||||
|
- [x] Scrollable message list component
|
||||||
|
- [x] Distinct user and assistant message bubble styles
|
||||||
|
- [x] Auto-scroll to bottom on new messages
|
||||||
|
- [x] Svelte 5 runes used for reactive message state (`$state`, `$derived`)
|
||||||
|
- [x] Empty state shown when no messages
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
- Added `globals` package to ESLint config for browser globals in Svelte files (fixes `Element`/`HTMLDivElement` not defined errors).
|
||||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -21,6 +21,7 @@
|
|||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
"eslint-plugin-svelte": "^3.0.0",
|
"eslint-plugin-svelte": "^3.0.0",
|
||||||
|
"globals": "^17.4.0",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"prettier-plugin-svelte": "^3.0.0",
|
"prettier-plugin-svelte": "^3.0.0",
|
||||||
"svelte": "^5.51.0",
|
"svelte": "^5.51.0",
|
||||||
@@ -803,6 +804,19 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
||||||
|
"version": "14.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||||
|
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.39.4",
|
"version": "9.39.4",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
|
||||||
@@ -2695,9 +2709,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/globals": {
|
"node_modules/globals": {
|
||||||
"version": "14.0.0",
|
"version": "17.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz",
|
||||||
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
"integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
"eslint-plugin-svelte": "^3.0.0",
|
"eslint-plugin-svelte": "^3.0.0",
|
||||||
|
"globals": "^17.4.0",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"prettier-plugin-svelte": "^3.0.0",
|
"prettier-plugin-svelte": "^3.0.0",
|
||||||
"svelte": "^5.51.0",
|
"svelte": "^5.51.0",
|
||||||
|
|||||||
20
src/lib/components/MessageBubble.svelte
Normal file
20
src/lib/components/MessageBubble.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ChatMessage } from '$lib/types';
|
||||||
|
|
||||||
|
let { message }: { message: ChatMessage } = $props();
|
||||||
|
|
||||||
|
const isUser = $derived(message.role === 'user');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex {isUser ? 'justify-end' : 'justify-start'} mb-3">
|
||||||
|
<div
|
||||||
|
class="max-w-[75%] rounded-2xl px-4 py-2.5 {isUser
|
||||||
|
? 'bg-blue-600 text-white rounded-br-md'
|
||||||
|
: 'bg-gray-200 text-gray-900 rounded-bl-md'}"
|
||||||
|
>
|
||||||
|
<p class="text-sm whitespace-pre-wrap">{message.content}</p>
|
||||||
|
<time class="mt-1 block text-xs {isUser ? 'text-blue-200' : 'text-gray-500'}">
|
||||||
|
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
37
src/lib/components/MessageList.svelte
Normal file
37
src/lib/components/MessageList.svelte
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { tick } from 'svelte';
|
||||||
|
import type { ChatMessage } from '$lib/types';
|
||||||
|
import MessageBubble from './MessageBubble.svelte';
|
||||||
|
|
||||||
|
let { messages }: { messages: ChatMessage[] } = $props();
|
||||||
|
|
||||||
|
let container: Element | undefined = $state();
|
||||||
|
|
||||||
|
async function scrollToBottom() {
|
||||||
|
await tick();
|
||||||
|
if (container) {
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (messages.length > 0) {
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={container} class="flex-1 overflow-y-auto p-4">
|
||||||
|
{#if messages.length === 0}
|
||||||
|
<div class="flex h-full items-center justify-center">
|
||||||
|
<div class="text-center text-gray-400">
|
||||||
|
<p class="text-lg font-medium">No messages yet</p>
|
||||||
|
<p class="mt-1 text-sm">Send a message to start a conversation</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{#each messages as message (message.id)}
|
||||||
|
<MessageBubble {message} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
6
src/lib/types.ts
Normal file
6
src/lib/types.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface ChatMessage {
|
||||||
|
id: string;
|
||||||
|
role: 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
14
src/routes/chat/+page.svelte
Normal file
14
src/routes/chat/+page.svelte
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ChatMessage } from '$lib/types';
|
||||||
|
import MessageList from '$lib/components/MessageList.svelte';
|
||||||
|
|
||||||
|
let messages: ChatMessage[] = $state([]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex h-screen flex-col">
|
||||||
|
<header class="border-b border-gray-200 bg-white px-4 py-3">
|
||||||
|
<h1 class="text-lg font-semibold text-gray-900">Chat</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<MessageList {messages} />
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user