11import { Component , For , createSignal , createEffect , Show , onMount , onCleanup , createMemo } from "solid-js"
2- import { getInstanceLogs , instances , isInstanceLogStreaming , setInstanceLogStreaming } from "../stores/instances"
3- import { ChevronDown } from "lucide-solid"
2+ import { getInstanceLogs , instances , isInstanceLogStreaming , setInstanceLogStreaming , clearLogs } from "../stores/instances"
3+ import { ArrowLeft , Trash2 } from "lucide-solid"
44import InstanceInfo from "./instance-info"
55import { useI18n } from "../lib/i18n"
66
77interface InfoViewProps {
88 instanceId : string
9+ onBackToConversation ?: ( ) => void
910}
1011
1112const logsScrollState = new Map < string , { scrollTop : number ; autoScroll : boolean } > ( )
@@ -15,19 +16,27 @@ const InfoView: Component<InfoViewProps> = (props) => {
1516 let scrollRef : HTMLDivElement | undefined
1617 const savedState = logsScrollState . get ( props . instanceId )
1718 const [ autoScroll , setAutoScroll ] = createSignal ( savedState ?. autoScroll ?? false )
19+ const [ showScrollTopButton , setShowScrollTopButton ] = createSignal ( false )
20+ const [ showScrollBottomButton , setShowScrollBottomButton ] = createSignal ( false )
1821
1922 const instance = ( ) => instances ( ) . get ( props . instanceId )
2023 const logs = createMemo ( ( ) => getInstanceLogs ( props . instanceId ) )
2124 const streamingEnabled = createMemo ( ( ) => isInstanceLogStreaming ( props . instanceId ) )
2225
2326 const handleEnableLogs = ( ) => setInstanceLogStreaming ( props . instanceId , true )
2427 const handleDisableLogs = ( ) => setInstanceLogStreaming ( props . instanceId , false )
28+ const handleClearLogs = ( ) => {
29+ clearLogs ( props . instanceId )
30+ updateScrollButtons ( )
31+ }
2532
2633 onMount ( ( ) => {
2734
2835 if ( scrollRef && savedState ) {
2936 scrollRef . scrollTop = savedState . scrollTop
3037 }
38+ // 初始化滾動按鈕可見性 / Initialize scroll button visibility
39+ updateScrollButtons ( )
3140 } )
3241
3342 onCleanup ( ( ) => {
@@ -45,18 +54,51 @@ const InfoView: Component<InfoViewProps> = (props) => {
4554 }
4655 } )
4756
57+ // 監聽日誌變化並更新滾動按鈕 / Listen for log changes and update scroll buttons
58+ createEffect ( ( ) => {
59+ logs ( ) // 追蹤 logs 變化
60+ updateScrollButtons ( )
61+ } )
62+
63+ /** 更新滾動按鈕顯示狀態 / Update scroll button visibility */
64+ const updateScrollButtons = ( ) => {
65+ if ( ! scrollRef ) return
66+
67+ const scrollTop = scrollRef . scrollTop
68+ const scrollHeight = scrollRef . scrollHeight
69+ const clientHeight = scrollRef . clientHeight
70+ const hasItems = logs ( ) . length > 0
71+
72+ const atBottom = scrollHeight - ( scrollTop + clientHeight ) <= 50
73+ const atTop = scrollTop <= 50
74+
75+ setShowScrollBottomButton ( hasItems && ! atBottom )
76+ setShowScrollTopButton ( hasItems && ! atTop )
77+ }
78+
79+ /** 滾動至頂部 / Scroll to top */
80+ const scrollToTop = ( ) => {
81+ if ( scrollRef ) {
82+ scrollRef . scrollTop = 0
83+ setAutoScroll ( false )
84+ updateScrollButtons ( )
85+ }
86+ }
87+
4888 const handleScroll = ( ) => {
4989 if ( ! scrollRef ) return
5090
5191 const isAtBottom = scrollRef . scrollHeight - scrollRef . scrollTop <= scrollRef . clientHeight + 50
5292
5393 setAutoScroll ( isAtBottom )
94+ updateScrollButtons ( )
5495 }
5596
5697 const scrollToBottom = ( ) => {
5798 if ( scrollRef ) {
5899 scrollRef . scrollTop = scrollRef . scrollHeight
59100 setAutoScroll ( true )
101+ updateScrollButtons ( )
60102 }
61103 }
62104
@@ -83,6 +125,11 @@ const InfoView: Component<InfoViewProps> = (props) => {
83125 }
84126 }
85127
128+ /** 是否顯示浮動滾動按鈕 / Whether to show floating scroll buttons */
129+ const showScrollButtons = createMemo ( ( ) => {
130+ return streamingEnabled ( ) && ( showScrollTopButton ( ) || showScrollBottomButton ( ) )
131+ } )
132+
86133 return (
87134 < div class = "log-container" >
88135 < div class = "flex-1 flex flex-col lg:flex-row gap-4 p-4 overflow-hidden" >
@@ -94,6 +141,38 @@ const InfoView: Component<InfoViewProps> = (props) => {
94141 < div class = "log-header" >
95142 < h2 class = "panel-title" > { t ( "infoView.logs.title" ) } </ h2 >
96143 < div class = "flex items-center gap-2" >
144+ < Show when = { props . onBackToConversation } >
145+ { ( onBack ) => (
146+ < button
147+ type = "button"
148+ class = "button-tertiary"
149+ onClick = { onBack }
150+ title = { t ( "infoView.logs.actions.back" ) }
151+ >
152+ < ArrowLeft class = "w-4 h-4" />
153+ </ button >
154+ ) }
155+ </ Show >
156+ < Show when = { logs ( ) . length > 0 } >
157+ < button
158+ type = "button"
159+ class = "button-tertiary"
160+ onClick = { handleClearLogs }
161+ title = { t ( "infoView.logs.actions.clear" ) }
162+ >
163+ < Trash2 class = "w-4 h-4" />
164+ </ button >
165+ </ Show >
166+ < Show when = { logs ( ) . length > 0 } >
167+ < button
168+ type = "button"
169+ class = "button-tertiary"
170+ onClick = { handleClearLogs }
171+ title = { t ( "infoView.logs.actions.clear" ) }
172+ >
173+ < Trash2 class = "w-4 h-4" />
174+ </ button >
175+ </ Show >
97176 < Show
98177 when = { streamingEnabled ( ) }
99178 fallback = {
@@ -143,15 +222,37 @@ const InfoView: Component<InfoViewProps> = (props) => {
143222 </ Show >
144223 </ Show >
145224 </ div >
146-
147- < Show when = { ! autoScroll ( ) && streamingEnabled ( ) } >
148- < button
149- onClick = { scrollToBottom }
150- class = "scroll-to-bottom"
151- >
152- < ChevronDown class = "w-4 h-4" />
153- { t ( "infoView.logs.scrollToBottom" ) }
154- </ button >
225+
226+ { /* 浮動滾動按鈕 / Floating scroll buttons */ }
227+ < Show when = { showScrollButtons ( ) } >
228+ < div class = "message-scroll-button-wrapper" >
229+ < Show when = { showScrollTopButton ( ) } >
230+ < button
231+ type = "button"
232+ class = "message-scroll-button"
233+ onClick = { scrollToTop }
234+ aria-label = { t ( "infoView.logs.scrollToTop" ) }
235+ title = { t ( "infoView.logs.scrollToTop" ) }
236+ >
237+ < span class = "message-scroll-icon" aria-hidden = "true" >
238+ ↑
239+ </ span >
240+ </ button >
241+ </ Show >
242+ < Show when = { showScrollBottomButton ( ) } >
243+ < button
244+ type = "button"
245+ class = "message-scroll-button"
246+ onClick = { scrollToBottom }
247+ aria-label = { t ( "infoView.logs.scrollToBottom" ) }
248+ title = { t ( "infoView.logs.scrollToBottom" ) }
249+ >
250+ < span class = "message-scroll-icon" aria-hidden = "true" >
251+ ↓
252+ </ span >
253+ </ button >
254+ </ Show >
255+ </ div >
155256 </ Show >
156257 </ div >
157258 </ div >
0 commit comments