feat: add message input with send and keyboard shortcuts #26
@@ -7,3 +7,4 @@
|
|||||||
| #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) |
|
| #5 | Chat page layout and message list component | COMPLETED | [issue-005.md](issue-005.md) |
|
||||||
|
| #6 | Message input with send and keyboard shortcuts | COMPLETED | [issue-006.md](issue-006.md) |
|
||||||
|
|||||||
17
implementation-plans/issue-006.md
Normal file
17
implementation-plans/issue-006.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
# Issue #6: Message input with send and keyboard shortcuts
|
||||||
|
|
||||||
|
**Status:** COMPLETED
|
||||||
|
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/6
|
||||||
|
**Branch:** `feature/issue-6-message-input`
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] Text input/textarea component for composing messages
|
||||||
|
- [x] Send button triggers message submission
|
||||||
|
- [x] Enter key sends message
|
||||||
|
- [x] Shift+Enter inserts newline
|
||||||
|
- [x] Input and send button disabled while a response is streaming
|
||||||
|
- [x] Input auto-focuses on page load and after send
|
||||||
63
src/lib/components/MessageInput.svelte
Normal file
63
src/lib/components/MessageInput.svelte
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
onSend,
|
||||||
|
disabled = false
|
||||||
|
}: { onSend: (message: string) => void; disabled?: boolean } = $props();
|
||||||
|
|
||||||
|
let input = $state('');
|
||||||
|
let textarea: HTMLTextAreaElement | undefined = $state();
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
const trimmed = input.trim();
|
||||||
|
if (!trimmed || disabled) return;
|
||||||
|
onSend(trimmed);
|
||||||
|
input = '';
|
||||||
|
resizeTextarea();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeTextarea() {
|
||||||
|
if (!textarea) return;
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!disabled && textarea) {
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-200 bg-white p-4">
|
||||||
|
<form onsubmit={handleSubmit} class="flex items-end gap-2">
|
||||||
|
<textarea
|
||||||
|
bind:this={textarea}
|
||||||
|
bind:value={input}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
oninput={resizeTextarea}
|
||||||
|
{disabled}
|
||||||
|
placeholder="Type a message..."
|
||||||
|
rows="1"
|
||||||
|
class="flex-1 resize-none rounded-xl border border-gray-300 px-4 py-2.5 text-sm
|
||||||
|
focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none
|
||||||
|
disabled:bg-gray-100 disabled:text-gray-400"
|
||||||
|
></textarea>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={handleSubmit}
|
||||||
|
disabled={disabled || !input.trim()}
|
||||||
|
class="rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-medium text-white
|
||||||
|
hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none
|
||||||
|
disabled:bg-gray-300 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -1,8 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ChatMessage } from '$lib/types';
|
import type { ChatMessage } from '$lib/types';
|
||||||
import MessageList from '$lib/components/MessageList.svelte';
|
import MessageList from '$lib/components/MessageList.svelte';
|
||||||
|
import MessageInput from '$lib/components/MessageInput.svelte';
|
||||||
|
|
||||||
let messages: ChatMessage[] = $state([]);
|
let messages: ChatMessage[] = $state([]);
|
||||||
|
let isStreaming = $state(false);
|
||||||
|
|
||||||
|
function handleSend(content: string) {
|
||||||
|
const userMessage: ChatMessage = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
role: 'user',
|
||||||
|
content,
|
||||||
|
timestamp: new Date()
|
||||||
|
};
|
||||||
|
messages.push(userMessage);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-screen flex-col">
|
<div class="flex h-screen flex-col">
|
||||||
@@ -11,4 +23,5 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<MessageList {messages} />
|
<MessageList {messages} />
|
||||||
|
<MessageInput onSend={handleSend} disabled={isStreaming} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user