Adam пре 3 месеци
родитељ
комит
0ac943de90

+ 10 - 5
packages/desktop/src/pages/index.tsx

@@ -13,6 +13,7 @@ import {
   DiffChanges,
   ProgressCircle,
   Message,
+  Typewriter,
 } from "@opencode-ai/ui"
 import { FileIcon } from "@/ui"
 import FileTree from "@/components/file-tree"
@@ -544,7 +545,6 @@ export default function Page() {
                               <For each={local.session.userMessages()}>
                                 {(message) => {
                                   const diffs = createMemo(() => message.summary?.diffs ?? [])
-
                                   return (
                                     <li
                                       class="group/li flex items-center gap-x-2 py-1 self-stretch cursor-default"
@@ -570,9 +570,9 @@ export default function Page() {
                             <div class="flex flex-col items-start gap-50 pb-50">
                               <For each={local.session.userMessages()}>
                                 {(message) => {
+                                  const [initialized, setInitialized] = createSignal(!!message.summary?.title)
                                   const [expanded, setExpanded] = createSignal(false)
                                   const parts = createMemo(() => sync.data.part[message.id])
-                                  const prompt = createMemo(() => local.session.getMessageText(message))
                                   const title = createMemo(() => message.summary?.title)
                                   const summary = createMemo(() => message.summary?.body)
                                   const assistantMessages = createMemo(() => {
@@ -581,6 +581,9 @@ export default function Page() {
                                     ) as AssistantMessageType[]
                                   })
                                   const working = createMemo(() => !summary())
+                                  createEffect(() => {
+                                    setTimeout(() => setInitialized(!!title()), 10_000)
+                                  })
 
                                   return (
                                     <div
@@ -589,9 +592,11 @@ export default function Page() {
                                     >
                                       {/* Title */}
                                       <div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10">
-                                        <h1 class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0">
-                                          {title() ?? prompt()}
-                                        </h1>
+                                        <div class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0">
+                                          <Show when={initialized()} fallback={<Typewriter as="h1" text={title()} />}>
+                                            <h1>{title()}</h1>
+                                          </Show>
+                                        </div>
                                       </div>
                                       <Show when={title}>
                                         <div class="-mt-8">

+ 1 - 0
packages/ui/src/components/index.ts

@@ -20,6 +20,7 @@ export * from "./select-dialog"
 export * from "./tabs"
 export * from "./basic-tool"
 export * from "./tooltip"
+export * from "./typewriter"
 
 export * from "../context/helper"
 export * from "../context/shiki"

+ 14 - 0
packages/ui/src/components/typewriter.css

@@ -0,0 +1,14 @@
+@keyframes blink {
+  0%,
+  50% {
+    opacity: 1;
+  }
+  51%,
+  100% {
+    opacity: 0;
+  }
+}
+
+.blinking-cursor {
+  animation: blink 1s step-end infinite;
+}

+ 54 - 0
packages/ui/src/components/typewriter.tsx

@@ -0,0 +1,54 @@
+import { createEffect, Show, type ValidComponent } from "solid-js"
+import { createStore } from "solid-js/store"
+import { Dynamic } from "solid-js/web"
+
+export const Typewriter = <T extends ValidComponent = "p">(props: {
+  text?: string
+  class?: string
+  as?: T
+}) => {
+  const [store, setStore] = createStore({
+    typing: false,
+    displayed: "",
+    cursor: true,
+  })
+
+  createEffect(() => {
+    const text = props.text
+    if (!text) return
+
+    let i = 0
+    setStore("typing", true)
+    setStore("displayed", "")
+    setStore("cursor", true)
+
+    const getTypingDelay = () => {
+      const random = Math.random()
+      if (random < 0.05) return 150 + Math.random() * 100
+      if (random < 0.15) return 80 + Math.random() * 60
+      return 30 + Math.random() * 50
+    }
+
+    const type = () => {
+      if (i < text.length) {
+        setStore("displayed", text.slice(0, i + 1))
+        i++
+        setTimeout(type, getTypingDelay())
+      } else {
+        setStore("typing", false)
+        setTimeout(() => setStore("cursor", false), 2000)
+      }
+    }
+
+    setTimeout(type, 200)
+  })
+
+  return (
+    <Dynamic component={props.as || "p"} class={props.class}>
+      {store.displayed}
+      <Show when={store.cursor}>
+        <span classList={{ "blinking-cursor": !store.typing }}>│</span>
+      </Show>
+    </Dynamic>
+  )
+}

+ 1 - 0
packages/ui/src/styles/index.css

@@ -25,5 +25,6 @@
 @import "../components/select-dialog.css" layer(components);
 @import "../components/tabs.css" layer(components);
 @import "../components/tooltip.css" layer(components);
+@import "../components/typewriter.css" layer(components);
 
 @import "./utilities.css" layer(utilities);