Jelajahi Sumber

Add displaying of messages, with little to no style

Lee Morgan 3 minggu lalu
induk
melakukan
e396150be6

+ 67 - 2
src/routes/(app)/game/[game_id]/+page.svelte

@@ -1,8 +1,73 @@
 <script>
 <script>
     import {page} from "$app/state";
     import {page} from "$app/state";
-    console.log(page);
+    import {PUBLIC_API_URL} from "$env/static/public";
+
+    import Message from "./Message.svelte";
+
+    let game = $state(page.data.data.game);
+    let character = $state(page.data.data.character);
+    let sessions = $state(page.data.data.sessions.sort((a, b) => a.started_at > b.started_at ? 1 : -1));
+    let turns = $state(page.data.data.turns);
+    let tokensRemaining = $state(page.data.data.tokens_remaining);
+    let streaming = $state(false);
+
+    const sendMessage = async (text)=>{
+        let newTurn = {user_text: text};
+        turns.push(newTurn);
+
+        const response = await fetch(`${PUBLIC_API_URL}/game/${game.id}/session/${sessions[0].id}/turn`, {
+            method: "POST",
+            headers: {"Content-Type": "application/json"},
+            credentials: "include",
+            body: JSON.stringify({content: text})
+        });
+
+        const reader = response.body.getReader();
+        const decoder = new TextDecoder();
+        let streamingIndex = turns.length - 1;
+        let buffer = "";
+
+        while(true){
+            const {done, value} = await reader.read();
+            if(done) break;
+
+            buffer += decoder.decode(value, {stream: true});
+
+            const lines = buffer.split("\n\n");
+            buffer = lines.pop();
+
+            for(let i = 0; i < lines.length; i++){
+                if(!lines[i].startsWith("data: ")) continue;
+                const payload = JSON.parse(lines[i].slice(6));
+
+                if(payload.type === "chunk"){
+                    turns[streamingIndex].llm_text += payload.text;
+                }else if(payload.type === "done"){
+                    console.log(payload);
+                    streaming = false;
+                }
+            }
+        }
+    }
+
+    if(turns.length === 0){
+        console.log("turning");
+        sendMessage("");
+    }
 </script>
 </script>
 
 
 <main>
 <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>
 </main>

+ 117 - 0
src/routes/(app)/game/[game_id]/Message.svelte

@@ -0,0 +1,117 @@
+<script>
+    let { turn, role, streaming = false } = $props();
+
+    const isUser = role === "user";
+    const content = $derived(isUser ? turn.user_text : turn.llm_text);
+
+    function formatDateTime(dateStr) {
+        return new Date(dateStr).toLocaleString("en-US", {
+            month: "short",
+            day: "numeric",
+            hour: "numeric",
+            minute: "2-digit",
+            hour12: true
+        });
+    }
+</script>
+
+<div class="message" class:user={isUser} class:assistant={!isUser}>
+    <div class="bubble">
+        <p class="text">
+            {content}{#if streaming}<span class="cursor"></span>{/if}
+        </p>
+        {#if !streaming}
+            <div class="meta">
+                <span class="time">{formatDateTime(turn.created_at)}</span>
+                {#if !isUser}
+                    <span class="dot">·</span>
+                    <span class="tokens">{turn.tokens_used} tokens</span>
+                {/if}
+            </div>
+        {/if}
+    </div>
+</div>
+
+<style>
+    .message {
+        display: flex;
+        max-width: 100%;
+    }
+
+    .message.user {
+        justify-content: flex-end;
+    }
+
+    .message.assistant {
+        justify-content: flex-start;
+    }
+
+    .bubble {
+        max-width: 72%;
+        display: flex;
+        flex-direction: column;
+        gap: 0.4rem;
+    }
+
+    .message.user .bubble {
+        align-items: flex-end;
+    }
+
+    .message.assistant .bubble {
+        align-items: flex-start;
+    }
+
+    .text {
+        padding: 0.75rem 1rem;
+        border-radius: 16px;
+        font-size: 0.95rem;
+        line-height: 1.6;
+        color: #e5e5ea;
+        white-space: pre-wrap;
+        word-break: break-word;
+    }
+
+    .message.user .text {
+        background: #5a5aff;
+        border-bottom-right-radius: 4px;
+    }
+
+    .message.assistant .text {
+        background: #1a1a28;
+        border: 1px solid #2a2a3a;
+        border-bottom-left-radius: 4px;
+    }
+
+    .meta {
+        display: flex;
+        align-items: center;
+        gap: 0.35rem;
+        font-size: 0.72rem;
+        color: #44445a;
+        padding: 0 0.25rem;
+    }
+
+    .dot {
+        color: #2a2a3a;
+    }
+
+    .cursor {
+        display: inline-block;
+        width: 2px;
+        height: 1em;
+        background: #a855f7;
+        margin-left: 2px;
+        vertical-align: text-bottom;
+        border-radius: 1px;
+        animation: blink 0.9s ease-in-out infinite;
+    }
+
+    @keyframes blink {
+        0%, 100% { opacity: 1; }
+        50% { opacity: 0; }
+    }
+
+    @media (max-width: 600px) {
+        .bubble { max-width: 88%; }
+    }
+</style>