Browse Source

Always show message actions for mobile UI + improvements for user message sizing (#16076)

Aleksander Grygier 3 months ago
parent
commit
5d0a40f390

+ 2 - 2
.gitignore

@@ -149,6 +149,6 @@ poetry.toml
 /run-chat.sh
 .ccache/
 
-# Code Workspace
+# IDE
 *.code-workspace
-
+.windsurf/

+ 0 - 7
.windsurf/rules/css-architecture.md

@@ -1,7 +0,0 @@
----
-trigger: manual
----
-
-#### Tailwind & CSS
-
--   We are using Tailwind v4 which uses oklch colors so we now want to refer to the CSS vars directly, without wrapping it with any color function like `hsla/hsl`, `rgba` etc.

+ 0 - 48
.windsurf/rules/sveltekit-architecture.md

@@ -1,48 +0,0 @@
----
-trigger: manual
----
-
-# Coding rules
-
-## Svelte & SvelteKit
-
-### Services vs Stores Separation Pattern
-
-#### `lib/services/` - Pure Business Logic
-
--   **Purpose**: Stateless business logic and external communication
--   **Contains**:
-    -   API calls to external services (ApiService)
-    -   Pure business logic functions (ChatService, etc.)
--   **Rules**:
-    -   NO Svelte runes ($state, $derived, $effect)
-    -   NO reactive state management
-    -   Pure functions and classes only
-    -   Can import types but not stores
-    -   Focus on "how" - implementation details
-
-#### `lib/stores/` - Reactive State Management
-
--   **Purpose**: Svelte-specific reactive state with runes
--   **Contains**:
-    -   Reactive state classes with $state, $derived, $effect
-    -   Database operations (DatabaseStore)
-    -   UI-focused state management
-    -   Store orchestration logic
--   **Rules**:
-    -   USE Svelte runes for reactivity
-    -   Import and use services for business logic
-    -   NO direct database operations
-    -   NO direct API calls (use services)
-    -   Focus on "what" - reactive state for UI
-
-#### Enforcement
-
--   Services should be testable without Svelte
--   Stores should leverage Svelte's reactivity system
--   Clear separation: services handle data, stores handle state
--   Services can be reused across multiple stores
-
-#### Misc
-
--   Always use `let` for $derived state variables

+ 0 - 9
.windsurf/rules/tests.md

@@ -1,9 +0,0 @@
----
-trigger: manual
----
-
-# Automated Tests
-
-## General rules
-
--   NEVER include any test code in the production code - we should always have it in a separate dedicated files

+ 0 - 7
.windsurf/rules/typescript-architecture.md

@@ -1,7 +0,0 @@
----
-trigger: manual
----
-
-## TypeScript
-
--   Add JSDocs for functions

BIN
tools/server/public/index.html.gz


+ 3 - 2
tools/server/webui/package.json

@@ -4,7 +4,7 @@
 	"version": "1.0.0",
 	"type": "module",
 	"scripts": {
-		"dev": "vite dev --host 0.0.0.0 & storybook dev -p 6006 --ci",
+		"dev": "bash scripts/dev.sh",
 		"build": "vite build && ./scripts/post-build.sh",
 		"preview": "vite preview",
 		"prepare": "svelte-kit sync || echo ''",
@@ -20,7 +20,8 @@
 		"test:ui": "vitest --project=ui",
 		"test:unit": "vitest",
 		"storybook": "storybook dev -p 6006",
-		"build-storybook": "storybook build"
+		"build-storybook": "storybook build",
+		"cleanup": "rm -rf .svelte-kit build node_modules test-results"
 	},
 	"devDependencies": {
 		"@chromatic-com/storybook": "^4.0.1",

+ 103 - 0
tools/server/webui/scripts/dev.sh

@@ -0,0 +1,103 @@
+#!/bin/bash
+
+cd ../../../
+
+# Check and install git hooks if missing
+check_and_install_hooks() {
+    local hooks_missing=false
+    
+    # Check for required hooks
+    if [ ! -f ".git/hooks/pre-commit" ] || [ ! -f ".git/hooks/pre-push" ] || [ ! -f ".git/hooks/post-push" ]; then
+        hooks_missing=true
+    fi
+    
+    if [ "$hooks_missing" = true ]; then
+        echo "🔧 Git hooks missing, installing them..."
+        cd tools/server/webui
+        if bash scripts/install-git-hooks.sh; then
+            echo "✅ Git hooks installed successfully"
+        else
+            echo "⚠️  Failed to install git hooks, continuing anyway..."
+        fi
+        cd ../../../
+    else
+        echo "✅ Git hooks already installed"
+    fi
+}
+
+# Install git hooks if needed
+check_and_install_hooks
+
+# Check if llama-server binary already exists
+if [ ! -f "build/bin/llama-server" ]; then
+    echo "Building llama-server..."
+    cmake -B build && cmake --build build --config Release -t llama-server
+else
+    echo "llama-server binary already exists, skipping build."
+fi
+
+# Start llama-server and capture output
+echo "Starting llama-server..."
+mkfifo server_output.pipe
+build/bin/llama-server -hf ggml-org/gpt-oss-20b-GGUF --jinja -c 0 --no-webui > server_output.pipe 2>&1 &
+SERVER_PID=$!
+
+# Function to wait for server to be ready
+wait_for_server() {
+    echo "Waiting for llama-server to be ready..."
+    local max_wait=60
+    local start_time=$(date +%s)
+    
+    # Read server output in background and look for the ready message
+    (
+        while IFS= read -r line; do
+            echo "🔍 Server: $line"
+            if [[ "$line" == *"server is listening on http://127.0.0.1:8080 - starting the main loop"* ]]; then
+                echo "✅ llama-server is ready!"
+                echo "READY" > server_ready.flag
+                break
+            fi
+        done < server_output.pipe
+    ) &
+    
+    # Wait for ready flag or timeout
+    while [ ! -f server_ready.flag ]; do
+        local current_time=$(date +%s)
+        local elapsed=$((current_time - start_time))
+        
+        if [ $elapsed -ge $max_wait ]; then
+            echo "❌ Server failed to start within $max_wait seconds"
+            rm -f server_ready.flag
+            return 1
+        fi
+        
+        sleep 1
+    done
+    
+    rm -f server_ready.flag
+    return 0
+}
+
+# Cleanup function
+cleanup() {
+    echo "🧹 Cleaning up..."
+    kill $SERVER_PID 2>/dev/null
+    rm -f server_output.pipe server_ready.flag
+    exit
+}
+
+# Set up signal handlers
+trap cleanup SIGINT SIGTERM
+
+# Wait for server to be ready
+if wait_for_server; then
+    echo "🚀 Starting development servers..."
+    cd tools/server/webui
+    storybook dev -p 6006 --ci & vite dev --host 0.0.0.0 &
+    
+    # Wait for all background processes
+    wait
+else
+    echo "❌ Failed to start development environment"
+    cleanup
+fi

+ 121 - 54
tools/server/webui/scripts/install-git-hooks.sh

@@ -1,14 +1,14 @@
 #!/bin/bash
 
-# Script to install pre-commit and post-commit hooks for webui
-# Pre-commit: formats, lints, checks, and builds code, stashes unstaged changes
-# Post-commit: automatically unstashes changes
+# Script to install pre-commit and pre-push hooks for webui
+# Pre-commit: formats code and runs checks
+# Pre-push: builds the project, stashes unstaged changes
 
 REPO_ROOT=$(git rev-parse --show-toplevel)
 PRE_COMMIT_HOOK="$REPO_ROOT/.git/hooks/pre-commit"
-POST_COMMIT_HOOK="$REPO_ROOT/.git/hooks/post-commit"
+PRE_PUSH_HOOK="$REPO_ROOT/.git/hooks/pre-push"
 
-echo "Installing pre-commit and post-commit hooks for webui..."
+echo "Installing pre-commit and pre-push hooks for webui..."
 
 # Create the pre-commit hook
 cat > "$PRE_COMMIT_HOOK" << 'EOF'
@@ -16,7 +16,7 @@ cat > "$PRE_COMMIT_HOOK" << 'EOF'
 
 # Check if there are any changes in the webui directory
 if git diff --cached --name-only | grep -q "^tools/server/webui/"; then
-    echo "Formatting webui code..."
+    echo "Formatting and checking webui code..."
     
     # Change to webui directory and run format
     cd tools/server/webui
@@ -27,20 +27,12 @@ if git diff --cached --name-only | grep -q "^tools/server/webui/"; then
         exit 1
     fi
     
-    # Stash any unstaged changes to avoid conflicts during format/build
-    echo "Stashing unstaged changes..."
-    git stash push --keep-index --include-untracked -m "Pre-commit hook: stashed unstaged changes"
-    STASH_CREATED=$?
-    
     # Run the format command
     npm run format
 
     # Check if format command succeeded
     if [ $? -ne 0 ]; then
         echo "Error: npm run format failed"
-        if [ $STASH_CREATED -eq 0 ]; then
-            echo "You can restore your unstaged changes with: git stash pop"
-        fi
         exit 1
     fi
 
@@ -50,9 +42,6 @@ if git diff --cached --name-only | grep -q "^tools/server/webui/"; then
     # Check if lint command succeeded
     if [ $? -ne 0 ]; then
         echo "Error: npm run lint failed"
-        if [ $STASH_CREATED -eq 0 ]; then
-            echo "You can restore your unstaged changes with: git stash pop"
-        fi
         exit 1
     fi
 
@@ -62,73 +51,151 @@ if git diff --cached --name-only | grep -q "^tools/server/webui/"; then
     # Check if check command succeeded
     if [ $? -ne 0 ]; then
         echo "Error: npm run check failed"
-        if [ $STASH_CREATED -eq 0 ]; then
-            echo "You can restore your unstaged changes with: git stash pop"
-        fi
         exit 1
     fi
 
-    # Run the build command
-    npm run build
+    # Go back to repo root
+    cd ../../..
     
-    # Check if build command succeeded
-    if [ $? -ne 0 ]; then
-        echo "Error: npm run build failed"
-        if [ $STASH_CREATED -eq 0 ]; then
-            echo "You can restore your unstaged changes with: git stash pop"
-        fi
+    echo "✅ Webui code formatted and checked successfully"
+fi
+
+exit 0
+EOF
+
+# Create the pre-push hook
+cat > "$PRE_PUSH_HOOK" << 'EOF'
+#!/bin/bash
+
+# Check if there are any webui changes that need building
+WEBUI_CHANGES=$(git diff --name-only @{push}..HEAD | grep "^tools/server/webui/" || true)
+
+if [ -n "$WEBUI_CHANGES" ]; then
+    echo "Webui changes detected, checking if build is up-to-date..."
+    
+    # Change to webui directory
+    cd tools/server/webui
+    
+    # Check if npm is available and package.json exists
+    if [ ! -f "package.json" ]; then
+        echo "Error: package.json not found in tools/server/webui"
         exit 1
     fi
-
-    # Go back to repo root to add build output
-    cd ../../..
     
-    # Add the build output to staging area
-    git add tools/server/public/index.html.gz
+    # Check if build output exists and is newer than source files
+    BUILD_FILE="../public/index.html.gz"
+    NEEDS_BUILD=false
     
-    if [ $STASH_CREATED -eq 0 ]; then
-        echo "✅ Build completed. Your unstaged changes have been stashed."
-        echo "They will be automatically restored after the commit."
-        # Create a marker file to indicate stash was created by pre-commit hook
-        touch .git/WEBUI_STASH_MARKER
+    if [ ! -f "$BUILD_FILE" ]; then
+        echo "Build output not found, building..."
+        NEEDS_BUILD=true
+    else
+        # Check if any source files are newer than the build output
+        if find src -newer "$BUILD_FILE" -type f | head -1 | grep -q .; then
+            echo "Source files are newer than build output, rebuilding..."
+            NEEDS_BUILD=true
+        fi
+    fi
+    
+    if [ "$NEEDS_BUILD" = true ]; then
+        echo "Building webui..."
+        
+        # Stash any unstaged changes to avoid conflicts during build
+        echo "Checking for unstaged changes..."
+        if ! git diff --quiet || ! git diff --cached --quiet --diff-filter=A; then
+            echo "Stashing unstaged changes..."
+            git stash push --include-untracked -m "Pre-push hook: stashed unstaged changes"
+            STASH_CREATED=$?
+        else
+            echo "No unstaged changes to stash"
+            STASH_CREATED=1
+        fi
+        
+        # Run the build command
+        npm run build
+        
+        # Check if build command succeeded
+        if [ $? -ne 0 ]; then
+            echo "Error: npm run build failed"
+            if [ $STASH_CREATED -eq 0 ]; then
+                echo "You can restore your unstaged changes with: git stash pop"
+            fi
+            exit 1
+        fi
+
+        # Go back to repo root
+        cd ../../..
+        
+        # Check if build output was created/updated
+        if [ -f "tools/server/public/index.html.gz" ]; then
+            # Add the build output and commit it
+            git add tools/server/public/index.html.gz
+            if ! git diff --cached --quiet; then
+                echo "Committing updated build output..."
+                git commit -m "chore: update webui build output"
+                echo "✅ Build output committed successfully"
+            else
+                echo "Build output unchanged"
+            fi
+        else
+            echo "Error: Build output not found after build"
+            if [ $STASH_CREATED -eq 0 ]; then
+                echo "You can restore your unstaged changes with: git stash pop"
+            fi
+            exit 1
+        fi
+        
+        if [ $STASH_CREATED -eq 0 ]; then
+            echo "✅ Build completed. Your unstaged changes have been stashed."
+            echo "They will be automatically restored after the push."
+            # Create a marker file to indicate stash was created by pre-push hook
+            touch .git/WEBUI_PUSH_STASH_MARKER
+        fi
+    else
+        echo "✅ Build output is up-to-date"
     fi
     
-    echo "Webui code formatted successfully"
+    echo "✅ Webui ready for push"
 fi
 
 exit 0
 EOF
 
-# Create the post-commit hook
-cat > "$POST_COMMIT_HOOK" << 'EOF'
+# Create the post-push hook (for restoring stashed changes after push)
+cat > "$REPO_ROOT/.git/hooks/post-push" << 'EOF'
 #!/bin/bash
 
-# Check if we have a stash marker from the pre-commit hook
-if [ -f .git/WEBUI_STASH_MARKER ]; then
-    echo "Restoring your unstaged changes..."
+# Check if we have a stash marker from the pre-push hook
+if [ -f .git/WEBUI_PUSH_STASH_MARKER ]; then
+    echo "Restoring your unstaged changes after push..."
     git stash pop
-    rm -f .git/WEBUI_STASH_MARKER
+    rm -f .git/WEBUI_PUSH_STASH_MARKER
     echo "✅ Your unstaged changes have been restored."
 fi
 
 exit 0
 EOF
 
-# Make both hooks executable
+# Make all hooks executable
 chmod +x "$PRE_COMMIT_HOOK"
-chmod +x "$POST_COMMIT_HOOK"
+chmod +x "$PRE_PUSH_HOOK"
+chmod +x "$REPO_ROOT/.git/hooks/post-push"
 
 if [ $? -eq 0 ]; then
-    echo "✅ Pre-commit and post-commit hooks installed successfully!"
-    echo "   Pre-commit:  $PRE_COMMIT_HOOK"
-    echo "   Post-commit: $POST_COMMIT_HOOK"
+    echo "✅ Git hooks installed successfully!"
+    echo "   Pre-commit: $PRE_COMMIT_HOOK"
+    echo "   Pre-push:   $PRE_PUSH_HOOK"
+    echo "   Post-push:  $REPO_ROOT/.git/hooks/post-push"
     echo ""
     echo "The hooks will automatically:"
-    echo "  • Format, lint, check, and build webui code before commits"
-    echo "  • Stash unstaged changes during the process"
-    echo "  • Restore your unstaged changes after the commit"
+    echo "  • Format and check webui code before commits (pre-commit)"
+    echo "  • Build webui code before pushes (pre-push)"
+    echo "  • Stash unstaged changes during build process"
+    echo "  • Restore your unstaged changes after the push"
     echo ""
-    echo "To test the hooks, make a change to a file in the webui directory and commit it."
+    echo "To test the hooks:"
+    echo "  • Make a change to a file in the webui directory and commit it (triggers format/check)"
+    echo "  • Push your commits to trigger the build process"
 else
     echo "❌ Failed to make hooks executable"
     exit 1

+ 1 - 1
tools/server/webui/scripts/post-build.sh

@@ -1,3 +1,3 @@
 rm -rf ../public/_app;
 rm ../public/favicon.svg;
-rm ../public/index.html;
+rm ../public/index.html;

+ 3 - 3
tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageActions.svelte

@@ -50,7 +50,7 @@
 
 <div class="relative {justify === 'start' ? 'mt-2' : ''} flex h-6 items-center justify-{justify}">
 	<div
-		class="flex items-center text-xs text-muted-foreground transition-opacity group-hover:opacity-0"
+		class="hidden items-center text-xs text-muted-foreground transition-opacity md:flex md:group-hover:opacity-0"
 	>
 		{new Date(message.timestamp).toLocaleTimeString(undefined, {
 			hour: '2-digit',
@@ -61,14 +61,14 @@
 	<div
 		class="absolute top-0 {actionsPosition === 'left'
 			? 'left-0'
-			: 'right-0'} flex items-center gap-2 opacity-0 transition-opacity group-hover:opacity-100"
+			: 'right-0'} flex items-center gap-2 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100"
 	>
 		{#if siblingInfo && siblingInfo.totalSiblings > 1}
 			<ChatMessageBranchingControls {siblingInfo} {onNavigateToSibling} />
 		{/if}
 
 		<div
-			class="pointer-events-none inset-0 flex items-center gap-1 opacity-0 transition-all duration-150 group-hover:pointer-events-auto group-hover:opacity-100"
+			class="pointer-events-auto inset-0 flex items-center gap-1 opacity-100 transition-all duration-150 md:pointer-events-none md:opacity-0 md:group-hover:pointer-events-auto md:group-hover:opacity-100"
 		>
 			<ActionButton icon={Copy} tooltip="Copy" onclick={onCopy} />
 

+ 34 - 4
tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageUser.svelte

@@ -52,11 +52,38 @@
 		onShowDeleteDialogChange,
 		textareaElement = $bindable()
 	}: Props = $props();
+
+	let isMultiline = $state(false);
+	let messageElement: HTMLElement | undefined = $state();
+
+	$effect(() => {
+		if (!messageElement || !message.content.trim()) return;
+
+		if (message.content.includes('\n')) {
+			isMultiline = true;
+			return;
+		}
+
+		const resizeObserver = new ResizeObserver((entries) => {
+			for (const entry of entries) {
+				const element = entry.target as HTMLElement;
+				const estimatedSingleLineHeight = 24; // Typical line height for text-md
+
+				isMultiline = element.offsetHeight > estimatedSingleLineHeight * 1.5;
+			}
+		});
+
+		resizeObserver.observe(messageElement);
+
+		return () => {
+			resizeObserver.disconnect();
+		};
+	});
 </script>
 
 <div
 	aria-label="User message with actions"
-	class="group flex flex-col items-end gap-2 {className}"
+	class="group flex flex-col items-end gap-3 md:gap-2 {className}"
 	role="group"
 >
 	{#if isEditing}
@@ -92,10 +119,13 @@
 		{/if}
 
 		{#if message.content.trim()}
-			<Card class="max-w-[80%] rounded-2xl bg-primary px-2.5 py-1.5 text-primary-foreground">
-				<div class="text-md whitespace-pre-wrap">
+			<Card
+				class="max-w-[80%] rounded-[1.125rem] bg-primary px-3.75 py-1.5 text-primary-foreground data-[multiline]:py-2.5"
+				data-multiline={isMultiline ? '' : undefined}
+			>
+				<span bind:this={messageElement} class="text-md whitespace-pre-wrap">
 					{message.content}
-				</div>
+				</span>
 			</Card>
 		{/if}
 

+ 1 - 1
tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte

@@ -196,7 +196,7 @@
 
 <style>
 	/* Base typography styles */
-	div :global(p) {
+	div :global(p:not(:last-child)) {
 		margin-bottom: 1rem;
 		line-height: 1.75;
 	}