Преглед изворни кода

Add style to the game page

Lee Morgan пре 3 недеља
родитељ
комит
90baed0ce7
1 измењених фајлова са 324 додато и 16 уклоњено
  1. 324 16
      src/routes/(app)/game/[game_id]/+page.svelte

+ 324 - 16
src/routes/(app)/game/[game_id]/+page.svelte

@@ -2,6 +2,7 @@
     import {page} from "$app/state";
     import {PUBLIC_API_URL} from "$env/static/public";
 
+    import DashboardNav from "../../dashboard/DashboardNav.svelte";
     import Message from "./Message.svelte";
 
     let game = $state(page.data.data.game);
@@ -11,6 +12,25 @@
     let tokensRemaining = $state(page.data.data.tokens_remaining);
     let streaming = $state(false);
 
+    let inputText = $state("");
+
+    const autogrow = (node) => {
+        const update = () => {
+            node.style.height = "auto";
+            node.style.height = Math.min(node.scrollHeight, 200) + "px";
+        };
+        node.addEventListener("input", update);
+        return { destroy: () => node.removeEventListener("input", update) };
+    };
+
+    const handleSubmit = (e) => {
+        e.preventDefault();
+        const text = inputText.trim();
+        if (!text || streaming) return;
+        inputText = "";
+        sendMessage(text);
+    };
+
     const sendMessage = async (text)=>{
         let newTurn = {user_text: text};
         turns.push(newTurn);
@@ -51,23 +71,311 @@
     }
 
     if(turns.length === 0){
-        console.log("turning");
         sendMessage("");
     }
 </script>
 
-<main>
-    {#each turns as turn, i}
-        {#if i !== 0}
-            <Message
-                turn={turn}
-                role="user"
-            />
-        {/if}
-        <Message
-            turn={turn}
-            role="llm"
-            streaming={streaming && i === message.length - 1}
-        />
-    {/each}
-</main>
+<div class="page">
+    <DashboardNav />
+
+    <div class="game-bar">
+        <div class="game-bar-left">
+            <div class="game-title-block">
+                <span class="game-label">Adventure</span>
+                <h1>{game.title}</h1>
+            </div>
+            <div class="divider"></div>
+            <div class="character-block">
+                <span class="game-label">Character</span>
+                <span class="character-name">{character.name}</span>
+            </div>
+        </div>
+        <div class="token-badge" class:token-low={tokensRemaining < 500}>
+            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                <circle cx="12" cy="12" r="10"/>
+                <path d="M12 6v12M9 9h4.5a2.5 2.5 0 0 1 0 5H9"/>
+            </svg>
+            {tokensRemaining.toLocaleString()} tokens
+        </div>
+    </div>
+
+    <main class="messages" id="message-list">
+        <div class="messages-inner">
+            {#each turns as turn, i}
+                {#if i !== 0}
+                    <Message {turn} role="user" />
+                {/if}
+                <Message
+                    {turn}
+                    role="llm"
+                    streaming={streaming && i === turns.length - 1}
+                />
+            {/each}
+
+            {#if streaming}
+                <div class="typing-indicator">
+                    <span></span><span></span><span></span>
+                </div>
+            {/if}
+        </div>
+    </main>
+
+    <div class="composer-wrap">
+        <form class="composer" onsubmit={handleSubmit}>
+            <textarea
+                bind:value={inputText}
+                use:autogrow
+                placeholder="What do you do?"
+                rows="1"
+                disabled={streaming}
+                onkeydown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSubmit(e); } }}
+            ></textarea>
+            <button type="submit" disabled={streaming || !inputText.trim()} aria-label="Send message">
+                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round">
+                    <path d="M12 19V5M5 12l7-7 7 7"/>
+                </svg>
+            </button>
+        </form>
+        <p class="composer-hint">Enter to send &nbsp;·&nbsp; Shift+Enter for new line</p>
+    </div>
+</div>
+
+<style>
+    .page {
+        height: 100vh;
+        display: flex;
+        flex-direction: column;
+        background: #0b0b10;
+        overflow: hidden;
+    }
+
+    /* ── Game bar ── */
+    .game-bar {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 0.75rem 2rem;
+        border-bottom: 1px solid #1a1a26;
+        background: #0d0d16;
+        flex-shrink: 0;
+        gap: 1rem;
+    }
+
+    .game-bar-left {
+        display: flex;
+        align-items: center;
+        gap: 1.25rem;
+        min-width: 0;
+    }
+
+    .game-label {
+        display: block;
+        font-size: 0.65rem;
+        font-weight: 600;
+        letter-spacing: 0.08em;
+        text-transform: uppercase;
+        color: #44445a;
+        margin-bottom: 0.15rem;
+    }
+
+    h1 {
+        font-size: 0.95rem;
+        font-weight: 700;
+        color: #e5e5ea;
+        letter-spacing: -0.2px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+    }
+
+    .divider {
+        width: 1px;
+        height: 2rem;
+        background: #1e1e2e;
+        flex-shrink: 0;
+    }
+
+    .character-name {
+        font-size: 0.95rem;
+        font-weight: 600;
+        color: #c084fc;
+    }
+
+    .token-badge {
+        display: inline-flex;
+        align-items: center;
+        gap: 5px;
+        padding: 0.35rem 0.75rem;
+        border-radius: 20px;
+        background: #13131e;
+        border: 1px solid #1e1e2e;
+        font-size: 0.78rem;
+        font-weight: 600;
+        color: #6b6b7a;
+        white-space: nowrap;
+        flex-shrink: 0;
+        transition: color 0.2s, border-color 0.2s;
+    }
+
+    .token-badge.token-low {
+        color: #e05555;
+        border-color: rgba(224, 85, 85, 0.3);
+    }
+
+    /* ── Messages ── */
+    .messages {
+        flex: 1;
+        overflow-y: auto;
+        scroll-behavior: smooth;
+    }
+
+    .messages::-webkit-scrollbar {
+        width: 6px;
+    }
+
+    .messages::-webkit-scrollbar-track {
+        background: transparent;
+    }
+
+    .messages::-webkit-scrollbar-thumb {
+        background: #1e1e2e;
+        border-radius: 3px;
+    }
+
+    .messages-inner {
+        max-width: 820px;
+        margin: 0 auto;
+        padding: 2rem 2rem 1rem;
+        display: flex;
+        flex-direction: column;
+        gap: 1rem;
+    }
+
+    /* ── Typing indicator ── */
+    .typing-indicator {
+        display: flex;
+        align-items: center;
+        gap: 5px;
+        padding: 0.75rem 1rem;
+        background: #1a1a28;
+        border: 1px solid #2a2a3a;
+        border-radius: 16px;
+        border-bottom-left-radius: 4px;
+        width: fit-content;
+    }
+
+    .typing-indicator span {
+        width: 7px;
+        height: 7px;
+        border-radius: 50%;
+        background: #44445a;
+        animation: bounce 1.2s ease-in-out infinite;
+    }
+
+    .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
+    .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
+
+    @keyframes bounce {
+        0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
+        30% { transform: translateY(-5px); opacity: 1; }
+    }
+
+    /* ── Composer ── */
+    .composer-wrap {
+        flex-shrink: 0;
+        border-top: 1px solid #1a1a26;
+        background: #0d0d16;
+        padding: 1rem 2rem 0.6rem;
+    }
+
+    .composer {
+        max-width: 820px;
+        margin: 0 auto;
+        display: flex;
+        align-items: flex-end;
+        gap: 0.75rem;
+        background: #13131e;
+        border: 1px solid #2a2a3a;
+        border-radius: 14px;
+        padding: 0.6rem 0.6rem 0.6rem 1rem;
+        transition: border-color 0.2s, box-shadow 0.2s;
+    }
+
+    .composer:focus-within {
+        border-color: #5a5aff;
+        box-shadow: 0 0 0 3px rgba(90, 90, 255, 0.1);
+    }
+
+    .composer textarea {
+        flex: 1;
+        background: none;
+        border: none;
+        outline: none;
+        resize: none;
+        font-size: 0.95rem;
+        line-height: 1.6;
+        color: #e5e5ea;
+        font-family: inherit;
+        min-height: 26px;
+        max-height: 200px;
+        overflow-y: auto;
+        padding: 0;
+    }
+
+    .composer textarea::placeholder {
+        color: #44445a;
+        font-style: italic;
+    }
+
+    .composer textarea:disabled {
+        opacity: 0.5;
+        cursor: not-allowed;
+    }
+
+    .composer button {
+        width: 36px;
+        height: 36px;
+        border-radius: 9px;
+        border: none;
+        background: #5a5aff;
+        color: #fff;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        cursor: pointer;
+        flex-shrink: 0;
+        transition: background 0.15s, transform 0.1s, opacity 0.15s;
+    }
+
+    .composer button:hover:not(:disabled) {
+        background: #4646e0;
+    }
+
+    .composer button:active:not(:disabled) {
+        transform: scale(0.93);
+    }
+
+    .composer button:disabled {
+        opacity: 0.35;
+        cursor: not-allowed;
+    }
+
+    .composer-hint {
+        max-width: 820px;
+        margin: 0.4rem auto 0;
+        font-size: 0.7rem;
+        color: #2e2e44;
+        text-align: right;
+        padding: 0 0.25rem;
+    }
+
+    @media (max-width: 700px) {
+        .game-bar { padding: 0.6rem 1rem; }
+        .messages-inner { padding: 1.25rem 1rem 0.75rem; }
+        .composer-wrap { padding: 0.75rem 1rem 0.5rem; }
+        .game-bar-left { gap: 0.75rem; }
+        .divider { display: none; }
+        .character-block { display: none; }
+        .composer-hint { display: none; }
+    }
+</style>