Просмотр исходного кода

feat: enhance LLM functionality with nginx configuration context and update ESLint auto-imports

0xJacky 5 месяцев назад
Родитель
Сommit
7214befc8c
71 измененных файлов с 4433 добавлено и 2808 удалено
  1. 2 1
      .claude/settings.local.json
  2. 28 12
      api/llm/llm.go
  3. 34 12
      api/llm/session.go
  4. 1 0
      app/.eslint-auto-import.mjs
  5. 8 0
      app/auto-imports.d.ts
  6. 3 0
      app/components.d.ts
  7. 3 1
      app/src/App.vue
  8. 12 3
      app/src/api/llm.ts
  9. 69 8
      app/src/components/LLM/ChatMessageInput.vue
  10. 146 22
      app/src/components/LLM/ChatMessageList.vue
  11. 173 118
      app/src/components/LLM/LLM.vue
  12. 307 120
      app/src/components/LLM/LLMSessionTabs.vue
  13. 6 1
      app/src/components/LLM/chatService.ts
  14. 41 21
      app/src/components/LLM/llm.ts
  15. 14 4
      app/src/components/LLM/sessionStore.ts
  16. 3 3
      app/src/components/NginxControl/NginxControl.vue
  17. 2 1
      app/src/components/Notification/Notification.vue
  18. 2 1
      app/src/components/TwoFA/use2FAModal.ts
  19. 24 18
      app/src/components/UpstreamCards/UpstreamCards.vue
  20. 0 163
      app/src/composables/useSSE.ts
  21. 221 175
      app/src/language/ar/app.po
  22. 213 166
      app/src/language/de_DE/app.po
  23. 70 40
      app/src/language/en/app.po
  24. 211 164
      app/src/language/es/app.po
  25. 217 170
      app/src/language/fr_FR/app.po
  26. 268 157
      app/src/language/ja_JP/app.po
  27. 248 155
      app/src/language/ko_KR/app.po
  28. 70 40
      app/src/language/messages.pot
  29. 221 168
      app/src/language/pt_PT/app.po
  30. 214 170
      app/src/language/ru_RU/app.po
  31. 214 166
      app/src/language/tr_TR/app.po
  32. 221 178
      app/src/language/uk_UA/app.po
  33. 217 174
      app/src/language/vi_VN/app.po
  34. 245 157
      app/src/language/zh_CN/app.po
  35. 250 163
      app/src/language/zh_TW/app.po
  36. 1 1
      app/src/lib/http/error.ts
  37. 2 1
      app/src/views/backup/components/BackupCreator.vue
  38. 3 1
      app/src/views/certificate/ACMEUser.vue
  39. 3 1
      app/src/views/certificate/CertificateEditor.vue
  40. 11 10
      app/src/views/certificate/components/CertificateBasicInfo.vue
  41. 7 6
      app/src/views/certificate/components/CertificateContentEditor.vue
  42. 2 1
      app/src/views/certificate/components/CertificateDownload.vue
  43. 2 1
      app/src/views/certificate/components/CertificateFileUpload.vue
  44. 2 1
      app/src/views/certificate/components/DNSIssueCertificate.vue
  45. 2 1
      app/src/views/certificate/components/RemoveCert.vue
  46. 4 1
      app/src/views/certificate/components/RenewCert.vue
  47. 0 2
      app/src/views/certificate/store.ts
  48. 6 0
      app/src/views/config/components/ConfigRightPanel/Chat.vue
  49. 14 2
      app/src/views/config/components/ConfigRightPanel/ConfigRightPanel.vue
  50. 3 1
      app/src/views/nginx_log/NginxLogList.vue
  51. 3 1
      app/src/views/nginx_log/indexing/IndexManagement.vue
  52. 3 1
      app/src/views/nginx_log/structured/StructuredLogViewer.vue
  53. 2 1
      app/src/views/preference/components/AuthSettings/AddPasskey.vue
  54. 2 1
      app/src/views/preference/components/AuthSettings/Passkey.vue
  55. 2 1
      app/src/views/preference/components/AuthSettings/RecoveryCodes.vue
  56. 2 1
      app/src/views/preference/components/AuthSettings/TOTP.vue
  57. 2 1
      app/src/views/preference/components/ExternalNotify/EnabledSwitch.vue
  58. 2 1
      app/src/views/preference/components/ExternalNotify/ExternalNotifyEditor.vue
  59. 2 1
      app/src/views/preference/store/index.ts
  60. 2 1
      app/src/views/preference/tabs/AuthSettings.vue
  61. 3 1
      app/src/views/preference/tabs/ExternalNotify.vue
  62. 49 1
      app/src/views/site/site_edit/components/RightPanel/Chat.vue
  63. 14 2
      app/src/views/site/site_edit/components/RightPanel/RightPanel.vue
  64. 2 1
      app/src/views/site/site_edit/components/SiteEditor/SiteEditor.vue
  65. 6 0
      app/src/views/stream/components/RightPanel/Chat.vue
  66. 14 2
      app/src/views/stream/components/RightPanel/RightPanel.vue
  67. 2 1
      app/src/views/stream/store.ts
  68. 104 8
      app/src/views/terminal/Terminal.vue
  69. 149 0
      app/src/views/terminal/components/TerminalRightPanel.vue
  70. 5 0
      app/vite.config.ts
  71. 23 0
      internal/llm/prompts.go

+ 2 - 1
.claude/settings.local.json

@@ -14,7 +14,8 @@
       "mcp__context7__get-library-docs",
       "Bash(find:*)",
       "Bash(sed:*)",
-      "Bash(cp:*)"
+      "Bash(cp:*)",
+      "mcp__eslint__lint-files"
     ],
     "deny": []
   }

+ 28 - 12
api/llm/llm.go

@@ -17,35 +17,51 @@ import (
 	"github.com/uozi-tech/cosy/logger"
 )
 
-const LLMInitPrompt = `You are a assistant who can help users write and optimise the configurations of Nginx,
-the first user message contains the content of the configuration file which is currently opened by the user and
-the current language code(CLC). You suppose to use the language corresponding to the CLC to give the first reply.
-Later the language environment depends on the user message.
-The first reply should involve the key information of the file and ask user what can you help them.`
 
 func MakeChatCompletionRequest(c *gin.Context) {
 	var json struct {
-		Filepath string                         `json:"filepath"`
-		Messages []openai.ChatCompletionMessage `json:"messages"`
+		Type        string                         `json:"type"`
+		Messages    []openai.ChatCompletionMessage `json:"messages"`
+		Language    string                         `json:"language,omitempty"`
+		NginxConfig string                         `json:"nginx_config,omitempty"` // Separate field for nginx configuration content
 	}
 
 	if !cosy.BindAndValid(c, &json) {
 		return
 	}
 
+	// Choose appropriate system prompt based on the type
+	var systemPrompt string
+	if json.Type == "terminal" {
+		systemPrompt = llm.TerminalAssistantPrompt
+	} else {
+		systemPrompt = llm.NginxConfigPrompt
+	}
+
+	// Append language instruction if language is provided
+	if json.Language != "" {
+		systemPrompt += fmt.Sprintf("\n\nIMPORTANT: Please respond in the language corresponding to this language code: %s", json.Language)
+	}
+
 	messages := []openai.ChatCompletionMessage{
 		{
 			Role:    openai.ChatMessageRoleSystem,
-			Content: LLMInitPrompt,
+			Content: systemPrompt,
 		},
 	}
 
-	messages = append(messages, json.Messages...)
-
-	if json.Filepath != "" {
-		messages = llm.ChatCompletionWithContext(json.Filepath, messages)
+	// Add nginx configuration context if provided
+	if json.Type != "terminal" && json.NginxConfig != "" {
+		// Add nginx configuration as context to the first user message
+		if len(json.Messages) > 0 && json.Messages[0].Role == openai.ChatMessageRoleUser {
+			// Prepend the nginx configuration to the first user message
+			contextualContent := fmt.Sprintf("Nginx Configuration:\n```nginx\n%s\n```\n\n%s", json.NginxConfig, json.Messages[0].Content)
+			json.Messages[0].Content = contextualContent
+		}
 	}
 
+	messages = append(messages, json.Messages...)
+
 	// SSE server
 	api.SetSSEHeaders(c)
 

+ 34 - 12
api/llm/session.go

@@ -15,14 +15,26 @@ import (
 	"github.com/uozi-tech/cosy/logger"
 )
 
+const TerminalAssistantPath = "__terminal_assistant__"
+
 // GetLLMSessions returns LLM sessions with optional filtering
 func GetLLMSessions(c *gin.Context) {
 	g := query.LLMSession
 	query := g.Order(g.UpdatedAt.Desc())
 	
-	// Filter by path if provided
-	if path := c.Query("path"); path != "" {
-		if !helper.IsUnderDirectory(path, nginx.GetConfPath()) {
+	// Filter by type if provided
+	if assistantType := c.Query("type"); assistantType != "" {
+		if assistantType == "terminal" {
+			// For terminal type, filter by terminal assistant path
+			query = query.Where(g.Path.Eq(TerminalAssistantPath))
+		} else if assistantType == "nginx" {
+			// For nginx type, exclude terminal assistant path
+			query = query.Where(g.Path.Neq(TerminalAssistantPath))
+		}
+	} else if path := c.Query("path"); path != "" {
+		// Filter by path if provided (legacy support)
+		// Skip path validation for terminal assistant
+		if path != TerminalAssistantPath && !helper.IsUnderDirectory(path, nginx.GetConfPath()) {
 			c.JSON(http.StatusForbidden, gin.H{
 				"message": "path is not under the nginx conf path",
 			})
@@ -59,23 +71,31 @@ func CreateLLMSession(c *gin.Context) {
 	var json struct {
 		Title string `json:"title" binding:"required"`
 		Path  string `json:"path"`
+		Type  string `json:"type"`
 	}
 
 	if !cosy.BindAndValid(c, &json) {
 		return
 	}
 
-	// Validate path if provided
-	if json.Path != "" && !helper.IsUnderDirectory(json.Path, nginx.GetConfPath()) {
-		c.JSON(http.StatusForbidden, gin.H{
-			"message": "path is not under the nginx conf path",
-		})
-		return
+	// Determine path based on type
+	var sessionPath string
+	if json.Type == "terminal" {
+		sessionPath = TerminalAssistantPath
+	} else {
+		sessionPath = json.Path
+		// Validate path for non-terminal types
+		if sessionPath != "" && !helper.IsUnderDirectory(sessionPath, nginx.GetConfPath()) {
+			c.JSON(http.StatusForbidden, gin.H{
+				"message": "path is not under the nginx conf path",
+			})
+			return
+		}
 	}
 
 	session := &model.LLMSession{
 		Title:        json.Title,
-		Path:         json.Path,
+		Path:         sessionPath,
 		Messages:     []openai.ChatCompletionMessage{},
 		MessageCount: 0,
 		IsActive:     true,
@@ -197,7 +217,8 @@ func DuplicateLLMSession(c *gin.Context) {
 func GetLLMSessionByPath(c *gin.Context) {
 	path := c.Query("path")
 
-	if !helper.IsUnderDirectory(path, nginx.GetConfPath()) {
+	// Skip path validation for terminal assistant
+	if path != TerminalAssistantPath && !helper.IsUnderDirectory(path, nginx.GetConfPath()) {
 		c.JSON(http.StatusForbidden, gin.H{
 			"message": "path is not under the nginx conf path",
 		})
@@ -250,7 +271,8 @@ func CreateOrUpdateLLMSessionByPath(c *gin.Context) {
 		return
 	}
 
-	if !helper.IsUnderDirectory(json.FileName, nginx.GetConfPath()) {
+	// Skip path validation for terminal assistant
+	if json.FileName != TerminalAssistantPath && !helper.IsUnderDirectory(json.FileName, nginx.GetConfPath()) {
 		c.JSON(http.StatusForbidden, gin.H{
 			"message": "path is not under the nginx conf path",
 		})

+ 1 - 0
app/.eslint-auto-import.mjs

@@ -4,6 +4,7 @@ export default {
     "$ngettext": true,
     "$npgettext": true,
     "$pgettext": true,
+    "App": true,
     "Component": true,
     "ComponentPublicInstance": true,
     "ComputedRef": true,

+ 8 - 0
app/auto-imports.d.ts

@@ -10,6 +10,7 @@ declare global {
   const $ngettext: typeof import('@/gettext')['$ngettext']
   const $npgettext: typeof import('@/gettext')['$npgettext']
   const $pgettext: typeof import('@/gettext')['$pgettext']
+  const App: typeof import('ant-design-vue')['App']
   const EffectScope: typeof import('vue')['EffectScope']
   const T: typeof import('@/language')['T']
   const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
@@ -38,7 +39,10 @@ declare global {
   const mapStores: typeof import('pinia')['mapStores']
   const mapWritableState: typeof import('pinia')['mapWritableState']
   const markRaw: typeof import('vue')['markRaw']
+  const message: typeof import('@/useAntApp')['message']
+  const modal: typeof import('@/useAntApp')['modal']
   const nextTick: typeof import('vue')['nextTick']
+  const notification: typeof import('@/useAntApp')['notification']
   const onActivated: typeof import('vue')['onActivated']
   const onBeforeMount: typeof import('vue')['onBeforeMount']
   const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
@@ -72,6 +76,9 @@ declare global {
   const toValue: typeof import('vue')['toValue']
   const triggerRef: typeof import('vue')['triggerRef']
   const unref: typeof import('vue')['unref']
+  const useAntAppStore: typeof import('@/pinia')['useAntAppStore']
+  const useApp: typeof import('ant-design-vue')['App.useApp']
+  const useAppUtils: typeof import('@/useAntApp')['useAppUtils']
   const useAttrs: typeof import('vue')['useAttrs']
   const useCssModule: typeof import('vue')['useCssModule']
   const useCssVars: typeof import('vue')['useCssVars']
@@ -103,6 +110,7 @@ declare module 'vue' {
     readonly $ngettext: UnwrapRef<typeof import('@/gettext')['$ngettext']>
     readonly $npgettext: UnwrapRef<typeof import('@/gettext')['$npgettext']>
     readonly $pgettext: UnwrapRef<typeof import('@/gettext')['$pgettext']>
+    readonly App: UnwrapRef<typeof import('ant-design-vue')['App']>
     readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
     readonly T: UnwrapRef<typeof import('@/language')['T']>
     readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>

+ 3 - 0
app/components.d.ts

@@ -9,6 +9,7 @@ export {}
 declare module 'vue' {
   export interface GlobalComponents {
     AAlert: typeof import('ant-design-vue/es')['Alert']
+    AApp: typeof import('ant-design-vue/es')['App']
     AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
     AAvatar: typeof import('ant-design-vue/es')['Avatar']
     ABadge: typeof import('ant-design-vue/es')['Badge']
@@ -90,6 +91,7 @@ declare module 'vue' {
     LLMChatMessageInput: typeof import('./src/components/LLM/ChatMessageInput.vue')['default']
     LLMChatMessageList: typeof import('./src/components/LLM/ChatMessageList.vue')['default']
     LLMLLM: typeof import('./src/components/LLM/LLM.vue')['default']
+    LLMLLMIframe: typeof import('./src/components/LLM/LLMIframe.vue')['default']
     LLMLLMSessionSelector: typeof import('./src/components/LLM/LLMSessionSelector.vue')['default']
     LLMLLMSessionSidebar: typeof import('./src/components/LLM/LLMSessionSidebar.vue')['default']
     LLMLLMSessionTabs: typeof import('./src/components/LLM/LLMSessionTabs.vue')['default']
@@ -123,6 +125,7 @@ declare module 'vue' {
     SelfCheckSelfCheckHeaderBanner: typeof import('./src/components/SelfCheck/SelfCheckHeaderBanner.vue')['default']
     SensitiveStringSensitiveString: typeof import('./src/components/SensitiveString/SensitiveString.vue')['default']
     SetLanguageSetLanguage: typeof import('./src/components/SetLanguage/SetLanguage.vue')['default']
+    ShadowRootShadowRoot: typeof import('./src/components/ShadowRoot/ShadowRoot.vue')['default']
     SwitchAppearanceIconsVPIconMoon: typeof import('./src/components/SwitchAppearance/icons/VPIconMoon.vue')['default']
     SwitchAppearanceIconsVPIconSun: typeof import('./src/components/SwitchAppearance/icons/VPIconSun.vue')['default']
     SwitchAppearanceSwitchAppearance: typeof import('./src/components/SwitchAppearance/SwitchAppearance.vue')['default']

+ 3 - 1
app/src/App.vue

@@ -59,7 +59,9 @@ loadTranslations(route)
     :locale="lang"
     :auto-insert-space-in-button="false"
   >
-    <RouterView />
+    <AApp>
+      <RouterView />
+    </AApp>
   </AConfigProvider>
 </template>
 

+ 12 - 3
app/src/api/llm.ts

@@ -49,15 +49,24 @@ const llm = {
   },
 
   // Session APIs
-  get_sessions(path?: string) {
+  get_sessions(pathOrType?: string, isType?: boolean) {
+    const params: Record<string, string> = {}
+    if (pathOrType) {
+      if (isType) {
+        params.type = pathOrType
+      }
+      else {
+        params.path = pathOrType
+      }
+    }
     return http.get<LLMSessionResponse[]>('/llm_sessions', {
-      params: path ? { path } : undefined,
+      params: Object.keys(params).length > 0 ? params : undefined,
     })
   },
   get_session(sessionId: string) {
     return http.get<LLMSessionResponse>(`/llm_sessions/${sessionId}`)
   },
-  create_session(data: { title: string, path?: string }) {
+  create_session(data: { title: string, path?: string, type?: string }) {
     return http.post<LLMSessionResponse>('/llm_sessions', data)
   },
   update_session(sessionId: string, data: { title?: string, messages?: ChatComplicationMessage[] }) {

+ 69 - 8
app/src/components/LLM/ChatMessageInput.vue

@@ -1,16 +1,59 @@
 <script setup lang="ts">
 import { LoadingOutlined, SendOutlined } from '@ant-design/icons-vue'
+import { useElementSize } from '@vueuse/core'
 import { storeToRefs } from 'pinia'
+import { useSettingsStore } from '@/pinia'
 import { useLLMStore } from './llm'
 
+defineProps<{
+  nginxConfig?: string
+}>()
 const llmStore = useLLMStore()
 const { loading, askBuffer, messages } = storeToRefs(llmStore)
+const { language: currentLanguage } = storeToRefs(useSettingsStore())
+
+// Get input container height for spacer
+const inputContainerRef = ref<HTMLElement>()
+const { height: inputHeight } = useElementSize(inputContainerRef)
+
+// Expose the height so parent can use it
+defineExpose({
+  inputHeight,
+})
+
+// Watch height changes to force parent updates
+watch(inputHeight, () => {
+  // Force reactivity by triggering a re-render
+  nextTick()
+})
 
 const messagesLength = computed(() => messages.value?.length ?? 0)
+
+function handleSend(event?: KeyboardEvent) {
+  // If it's a keyboard event and shift is pressed, allow default (new line)
+  if (event && event.shiftKey) {
+    return
+  }
+
+  // Prevent default Enter behavior when not shift+enter
+  if (event) {
+    event.preventDefault()
+  }
+
+  if (!askBuffer.value.trim())
+    return
+  llmStore.send(askBuffer.value, currentLanguage.value)
+}
+
+function handleButtonClick() {
+  if (!askBuffer.value.trim())
+    return
+  llmStore.send(askBuffer.value, currentLanguage.value)
+}
 </script>
 
 <template>
-  <div class="input-msg">
+  <div ref="inputContainerRef" class="input-msg">
     <div class="control-btn">
       <ASpace v-show="!loading">
         <APopconfirm
@@ -25,7 +68,7 @@ const messagesLength = computed(() => messages.value?.length ?? 0)
         </APopconfirm>
         <AButton
           type="text"
-          @click="llmStore.regenerate(messagesLength - 1)"
+          @click="llmStore.regenerate(messagesLength - 1, currentLanguage)"
         >
           {{ $gettext('Regenerate response') }}
         </AButton>
@@ -33,15 +76,16 @@ const messagesLength = computed(() => messages.value?.length ?? 0)
     </div>
     <ATextarea
       v-model:value="askBuffer"
-      auto-size
-      @press-enter="llmStore.send(askBuffer)"
+      :auto-size="{ minRows: 1, maxRows: 6 }"
+      :placeholder="$gettext('Type your message here...')"
+      @press-enter="handleSend"
     />
     <div class="send-btn">
       <AButton
         size="small"
         type="text"
-        :disabled="loading"
-        @click="llmStore.send(askBuffer)"
+        :disabled="loading || !askBuffer"
+        @click="handleButtonClick"
       >
         <LoadingOutlined v-if="loading" spin />
         <SendOutlined v-else />
@@ -52,8 +96,10 @@ const messagesLength = computed(() => messages.value?.length ?? 0)
 
 <style lang="less" scoped>
 .input-msg {
-  position: sticky;
+  position: absolute;
   bottom: 0;
+  left: 0;
+  right: 0;
   background: rgba(255, 255, 255, 0.8);
   backdrop-filter: blur(10px);
   -webkit-backdrop-filter: blur(10px);
@@ -61,16 +107,31 @@ const messagesLength = computed(() => messages.value?.length ?? 0)
   border-radius: 0 0 8px 8px;
   width: 100%;
   box-sizing: border-box;
+  z-index: 100;
+  box-shadow: 0 0 10px rgba(0, 0, 0, 0.04);
 
   .control-btn {
     display: flex;
     justify-content: center;
   }
 
+  :deep(.ant-input) {
+    padding-right: 50px; // 为发送按钮预留空间
+    resize: none;
+    min-height: 32px;
+    line-height: 1.5;
+  }
+
+  :deep(.ant-input-textarea) {
+    .ant-input {
+      min-height: 32px !important;
+    }
+  }
+
   .send-btn {
     position: absolute;
     right: 16px;
-    bottom: 19px;
+    bottom: 16px;
   }
 }
 

+ 146 - 22
app/src/components/LLM/ChatMessageList.vue

@@ -1,19 +1,137 @@
 <script setup lang="ts">
+import { useSettingsStore } from '@/pinia'
 import ChatMessage from './ChatMessage.vue'
 import { useLLMStore } from './llm'
 
+// Props
+const props = defineProps<{
+  inputHeight?: number
+}>()
+
 // Use LLM store
 const llmStore = useLLMStore()
 const { messages, editingIdx, editValue, loading } = storeToRefs(llmStore)
 
+// Get current language
+const { language: currentLanguage } = storeToRefs(useSettingsStore())
+
+// Message list container ref for scrolling
+const messageListContainer = ref<HTMLElement>()
+
+// Track user scroll state
+const userScrolledUp = ref(false)
+const isAutoScrolling = ref(false)
+const lastScrollTop = ref(0)
+const scrollDirection = ref<'up' | 'down' | 'none'>('none')
+
+// Check if user is at bottom
+function isAtBottom() {
+  if (!messageListContainer.value)
+    return false
+  const container = messageListContainer.value
+  const threshold = 10 // Allow larger margin for padding/border issues
+  const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight
+
+  return distanceFromBottom <= threshold
+}
+
+// Scroll to bottom method
+function scrollToBottom() {
+  if (messageListContainer.value) {
+    isAutoScrolling.value = true
+
+    // Scroll to absolute bottom to account for padding
+    const container = messageListContainer.value
+    const targetScrollTop = container.scrollHeight - container.clientHeight
+    container.scrollTop = targetScrollTop
+
+    // Update tracking values
+    lastScrollTop.value = targetScrollTop
+    userScrolledUp.value = false
+
+    setTimeout(() => {
+      isAutoScrolling.value = false
+    }, 100)
+  }
+}
+
+// Handle scroll events to detect user scrolling
+function handleScroll() {
+  if (isAutoScrolling.value)
+    return
+
+  const container = messageListContainer.value
+  if (!container)
+    return
+
+  const currentScrollTop = container.scrollTop
+  const previousScrollTop = lastScrollTop.value
+
+  // Determine scroll direction
+  if (currentScrollTop > previousScrollTop) {
+    scrollDirection.value = 'down'
+  }
+  else if (currentScrollTop < previousScrollTop) {
+    scrollDirection.value = 'up'
+  }
+  else {
+    scrollDirection.value = 'none'
+  }
+
+  // Only mark as user scrolled up if they actively scrolled up AND are not at bottom
+  const wasAtBottom = isAtBottom()
+  if (scrollDirection.value === 'up' && !wasAtBottom) {
+    userScrolledUp.value = true
+  }
+  else if (wasAtBottom) {
+    // If user is at bottom, allow auto scroll
+    userScrolledUp.value = false
+  }
+
+  lastScrollTop.value = currentScrollTop
+}
+
+// Auto-scroll only if user hasn't scrolled up
+function autoScrollToBottom() {
+  if (!userScrolledUp.value) {
+    scrollToBottom()
+  }
+}
+
+// Setup scroll listener
+onMounted(() => {
+  nextTick(() => {
+    if (messageListContainer.value) {
+      messageListContainer.value.addEventListener('scroll', handleScroll, { passive: true })
+    }
+  })
+})
+
+onUnmounted(() => {
+  if (messageListContainer.value) {
+    messageListContainer.value.removeEventListener('scroll', handleScroll)
+  }
+})
+
+// Reset scroll state (call when new messages arrive)
+function resetScrollState() {
+  userScrolledUp.value = false
+}
+
+// Expose scroll method
+defineExpose({
+  scrollToBottom: autoScrollToBottom,
+  resetScrollState,
+})
+
 function handleEdit(index: number) {
   llmStore.startEdit(index)
 }
 
-async function handleSave() {
+async function handleSave(index: number) {
   llmStore.saveEdit()
   await nextTick()
-  llmStore.request()
+  llmStore.regenerate(index, currentLanguage.value)
 }
 
 function handleCancel() {
@@ -21,30 +139,33 @@ function handleCancel() {
 }
 
 async function handleRegenerate(index: number) {
-  llmStore.regenerate(index)
+  llmStore.regenerate(index, currentLanguage.value)
 }
 </script>
 
 <template>
-  <div class="message-list-container">
+  <div
+    ref="messageListContainer"
+    class="message-list-container"
+    :style="{ paddingBottom: props.inputHeight ? `${props.inputHeight + 32}px` : '32px' }"
+  >
     <AList
-      class="llm-log"
+      class="llm-log pt-12"
       item-layout="horizontal"
-      :data-source="messages"
     >
-      <template #renderItem="{ item, index }">
-        <ChatMessage
-          :edit-value="editValue"
-          :message="item"
-          :index="index"
-          :is-editing="editingIdx === index"
-          :loading="loading"
-          @edit="handleEdit"
-          @save="handleSave"
-          @cancel="handleCancel"
-          @regenerate="handleRegenerate"
-        />
-      </template>
+      <ChatMessage
+        v-for="(item, index) in messages"
+        :key="index"
+        :edit-value="editValue"
+        :message="item"
+        :index="index"
+        :is-editing="editingIdx === index"
+        :loading="loading"
+        @edit="handleEdit"
+        @save="handleSave"
+        @cancel="handleCancel"
+        @regenerate="handleRegenerate"
+      />
     </AList>
   </div>
 </template>
@@ -52,6 +173,12 @@ async function handleRegenerate(index: number) {
 <style lang="less" scoped>
 .message-list-container {
   width: 100%;
+  overflow-y: auto;
+  overflow-x: hidden;
+
+  :deep(.ant-list-empty-text) {
+    display: none;
+  }
 
   .llm-log {
     :deep(.ant-list-item) {
@@ -74,9 +201,6 @@ async function handleRegenerate(index: number) {
       }
     }
 
-    :deep(.ant-list-item:first-child) {
-      display: none;
-    }
   }
 }
 </style>

+ 173 - 118
app/src/components/LLM/LLM.vue

@@ -1,31 +1,64 @@
 <script setup lang="ts">
-import { useElementVisibility } from '@vueuse/core'
+import { theme } from 'ant-design-vue'
 import { storeToRefs } from 'pinia'
 import { useSettingsStore } from '@/pinia'
+import { useAnimationCoordinator } from './animationCoordinator'
 import ChatMessageInput from './ChatMessageInput.vue'
 import ChatMessageList from './ChatMessageList.vue'
-import { buildLLMContext } from './contextBuilder'
 import { useLLMStore } from './llm'
 import LLMSessionTabs from './LLMSessionTabs.vue'
 import { useLLMSessionStore } from './sessionStore'
 
 const props = defineProps<{
-  content: string
   path?: string
+  nginxConfig?: string
+  type?: 'terminal' | 'nginx'
+  theme?: 'light' | 'dark'
+  height?: string
 }>()
 
-const { language: current } = storeToRefs(useSettingsStore())
+const { theme: settingsTheme } = storeToRefs(useSettingsStore())
+
+const currentTheme = computed(() => props.theme || settingsTheme.value)
+
+// Create theme config for AConfigProvider
+const llmTheme = computed(() => {
+  const isDark = currentTheme.value === 'dark'
+  return {
+    algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
+  }
+})
 
 // Use LLM store and session store
 const llmStore = useLLMStore()
 const sessionStore = useLLMSessionStore()
-const { messageContainerRef } = storeToRefs(llmStore)
 const { activeSessionId, sortedSessions } = storeToRefs(sessionStore)
 
-// Initialize sessions and handle path changes
-watch(() => props.path, async () => {
-  // Load sessions for current path
-  await sessionStore.loadSessions(props.path)
+// Animation coordinator
+const { state: animationState, isMessageAnimationComplete } = useAnimationCoordinator()
+
+// Message list ref for scrolling
+const messageListRef = ref<InstanceType<typeof ChatMessageList>>()
+
+// Get input height for padding calculation
+const chatInputRef = ref<InstanceType<typeof ChatMessageInput>>()
+const inputHeight = computed(() => chatInputRef.value?.inputHeight || 0)
+
+// Initialize sessions and handle type changes
+watch(() => props.type, async () => {
+  // Set assistant type
+  if (props.type === 'terminal') {
+    llmStore.setType('terminal')
+  }
+
+  // Load sessions for current type
+  if (props.type) {
+    await sessionStore.loadSessions(props.type, true) // true indicates it's a type, not a path
+  }
+  else {
+    // Fallback to path-based loading if no type specified
+    await sessionStore.loadSessions(props.path)
+  }
 
   // Check if we have sessions available
   if (sortedSessions.value.length > 0 && !activeSessionId.value) {
@@ -35,60 +68,39 @@ watch(() => props.path, async () => {
     sessionStore.setActiveSession(latestSession.session_id)
   }
   else if (sortedSessions.value.length === 0) {
-    // No sessions exist for this path, create a new one automatically
-    const title = props.path ? `Chat for ${props.path.split('/').pop()}` : 'New Chat'
+    // No sessions exist for this type/path, create a new one automatically
+    let title = $gettext('New Chat')
+    if (props.type === 'terminal') {
+      title = $gettext('Terminal Assistant')
+    }
+    else if (props.path) {
+      title = $gettext('Chat for %{path}', { path: props.path.split('/').pop() || '' })
+    }
     try {
-      const session = await sessionStore.createSession(title, props.path)
+      const session = await sessionStore.createSession(title, props.path, props.type)
       await llmStore.switchSession(session.session_id)
-
-      // Initialize with first message
-      await nextTick()
-      await sendFirstMessage()
+      // Auto-initialization removed - no initial message sent
     }
     catch (error) {
       console.error('Failed to create initial session:', error)
       // Fallback to legacy mode
       await llmStore.initMessages(props.path)
-      await nextTick()
-      if (llmStore.messages.length === 0) {
-        await sendFirstMessage()
-      }
+      // Auto-initialization removed - no initial message sent
     }
   }
 }, { immediate: true })
 
-// Build context and send first message
-async function sendFirstMessage() {
-  if (!props.path) {
-    // If no path, use original content only
-    await llmStore.send(props.content, current.value, props.content)
-    return
-  }
-
-  try {
-    // Build complete context including included files
-    const context = await buildLLMContext(props.path, props.content)
-
-    // Send with enhanced context
-    await llmStore.send(props.content, current.value, context.contextText)
-  }
-  catch (error) {
-    console.error('Failed to build enhanced context, falling back to original:', error)
-    // Fallback to original behavior
-    await llmStore.send(props.content, current.value, props.content)
-  }
-}
-
 // Handle new session creation
 async function handleNewSessionCreated() {
   // Reload sessions to update the list
-  await sessionStore.loadSessions(props.path)
-  await nextTick()
-
-  // Auto-send first message if no messages exist
-  if (llmStore.messages.length === 0) {
-    await sendFirstMessage()
+  if (props.type) {
+    await sessionStore.loadSessions(props.type, true)
   }
+  else {
+    await sessionStore.loadSessions(props.path)
+  }
+  await nextTick()
+  // Auto-initialization removed - no initial message sent
 }
 
 // Handle when all sessions are cleared
@@ -96,100 +108,143 @@ function handleSessionCleared() {
   // Reset to initial state - could create a welcome message or just stay empty
 }
 
-const isVisible = useElementVisibility(messageContainerRef)
+// Handle scrolling when messages change (immediate scroll for new messages)
+watch(
+  () => llmStore.messages.length,
+  () => {
+    // Reset scroll state for new messages
+    if (messageListRef.value) {
+      messageListRef.value.resetScrollState()
+    }
 
-watch(isVisible, visible => {
-  if (visible) {
-    llmStore.scrollToBottom()
+    nextTick(() => {
+      if (messageListRef.value) {
+        messageListRef.value.scrollToBottom()
+      }
+    })
+  },
+)
+
+// Watch animation state and scroll when all message animations complete
+watch(animationState, (_, oldState) => {
+  // When message animations complete, scroll to bottom with a final force scroll
+  if (oldState && (oldState.messageStreaming || oldState.messageTyping) && isMessageAnimationComplete()) {
+    nextTick(() => {
+      if (messageListRef.value) {
+        messageListRef.value.scrollToBottom()
+
+        // Final force scroll after a small delay to ensure everything is rendered
+        setTimeout(() => {
+          if (messageListRef.value) {
+            messageListRef.value.scrollToBottom()
+          }
+        }, 200)
+      }
+    })
   }
+}, { deep: true })
+
+// Also scroll during typing animation to keep up with content changes
+watch(
+  () => animationState.value.messageTyping,
+  (isTyping, _wasTyping) => {
+    if (isTyping) {
+      // During typing, continuously scroll to bottom with a small delay
+      const scrollInterval = setInterval(() => {
+        if (!animationState.value.messageTyping) {
+          clearInterval(scrollInterval)
+          // Final scroll when typing stops
+          setTimeout(() => {
+            if (messageListRef.value) {
+              messageListRef.value.scrollToBottom()
+            }
+          }, 100)
+          return
+        }
+        if (messageListRef.value) {
+          messageListRef.value.scrollToBottom()
+        }
+      }, 100)
+    }
+  },
+)
+
+watch(() => props.nginxConfig, v => {
+  llmStore.setNginxConfig(v || '')
 }, { immediate: true })
 </script>
 
 <template>
-  <div class="llm-container">
-    <div class="session-header">
-      <LLMSessionTabs
-        :content="props.content"
-        :path="props.path"
-        @new-session-created="handleNewSessionCreated"
-        @session-cleared="handleSessionCleared"
-      />
-    </div>
-
+  <AConfigProvider :theme="llmTheme">
     <div
-      ref="messageContainerRef"
-      class="message-container"
+      class="llm-wrapper"
     >
-      <ChatMessageList />
-      <ChatMessageInput />
+      <div class="llm-container">
+        <!-- Session Tabs -->
+        <div class="session-header">
+          <LLMSessionTabs
+            :path="path"
+            :type="type"
+            @new-session-created="handleNewSessionCreated"
+            @session-cleared="handleSessionCleared"
+          />
+        </div>
+
+        <!-- Message List -->
+        <div class="message-container">
+          <ChatMessageList
+            ref="messageListRef"
+            :input-height="inputHeight"
+          />
+        </div>
+
+        <!-- Input Container -->
+        <div class="input-container">
+          <ChatMessageInput ref="chatInputRef" />
+        </div>
+      </div>
     </div>
-  </div>
+  </AConfigProvider>
 </template>
 
 <style lang="less" scoped>
+.llm-wrapper {
+  width: 100%;
+  height: max(600px, calc(100vh - 200px));
+}
+
 .llm-container {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
   width: 100%;
+  height: 100%;
   position: relative;
-
-  // 为 backdrop-filter 提供背景内容
-  &::before {
-    content: '';
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    background: linear-gradient(135deg,
-      rgba(0, 0, 0, 0.02) 0%,
-      rgba(255, 255, 255, 0.01) 50%,
-      rgba(0, 0, 0, 0.02) 100%);
-    pointer-events: none;
-    z-index: 0;
-  }
 }
 
 .session-header {
-  flex-shrink: 0;
-  position: relative;
-  z-index: 1;
+  border-bottom: 1px solid var(--ant-color-border);
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 48px;
 }
 
 .message-container {
-  flex: 1;
-  margin: 0 auto;
-  width: 100%;
-  max-width: 800px;
-  max-height: calc(100vh - 332px);
-  overflow-y: auto;
-  overflow-x: hidden;
+  height: calc(100% - 48px - 56px); // Total height - header height - default input height
+  overflow: hidden;
   position: relative;
-  z-index: 1;
-  background: linear-gradient(to bottom,
-    rgba(0, 0, 0, 0.01) 0%,
-    rgba(255, 255, 255, 0.005) 30%,
-    rgba(0, 0, 0, 0.01) 60%,
-    rgba(255, 255, 255, 0.01) 100%);
-}
 
-.dark {
-  .llm-container {
-    &::before {
-      background: linear-gradient(135deg,
-        rgba(255, 255, 255, 0.02) 0%,
-        rgba(0, 0, 0, 0.01) 50%,
-        rgba(255, 255, 255, 0.02) 100%);
-    }
+  :deep(.message-list-container) {
+    height: 100%;
   }
+}
 
-  .message-container {
-    background: linear-gradient(to bottom,
-      rgba(255, 255, 255, 0.01) 0%,
-      rgba(0, 0, 0, 0.005) 30%,
-      rgba(255, 255, 255, 0.01) 60%,
-      rgba(0, 0, 0, 0.01) 100%);
-  }
+.input-container {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  min-height: 56px;
+  background: var(--ant-color-bg-container);
+  border-top: 1px solid var(--ant-color-border);
 }
 </style>

+ 307 - 120
app/src/components/LLM/LLMSessionTabs.vue

@@ -1,10 +1,10 @@
 <script setup lang="ts">
 import {
-  ClockCircleOutlined,
   CloseOutlined,
   CopyOutlined,
   DeleteOutlined,
   EditOutlined,
+  HistoryOutlined,
   MoreOutlined,
   PlusOutlined,
 } from '@ant-design/icons-vue'
@@ -13,8 +13,8 @@ import { useLLMStore } from './llm'
 import { useLLMSessionStore } from './sessionStore'
 
 const props = defineProps<{
-  content?: string
   path?: string
+  type?: 'terminal' | 'nginx'
 }>()
 
 const emit = defineEmits<{
@@ -30,19 +30,30 @@ const { loading: llmLoading } = storeToRefs(llmStore)
 const editingSessionId = ref<string | null>(null)
 const editingTitle = ref('')
 const historyDrawerVisible = ref(false)
+const sessionsDropdownVisible = ref(false)
+const searchText = ref('')
 
 // Only show first 3 sessions in tabs, rest in history
 const visibleSessions = computed(() => sortedSessions.value.slice(0, 3))
-const historySessions = computed(() => sortedSessions.value.slice(3))
+
+// Filtered sessions for dropdown search
+const filteredSessions = computed(() => {
+  if (!searchText.value.trim()) {
+    return sortedSessions.value
+  }
+  return sortedSessions.value.filter(session =>
+    session.title.toLowerCase().includes(searchText.value.toLowerCase()),
+  )
+})
 
 async function createNewSession() {
   if (llmLoading.value) {
     return // Don't create new session while LLM is generating output
   }
 
-  const title = `New Chat`
+  const title = props.type === 'terminal' ? 'Terminal Assistant' : 'New Chat'
   try {
-    const session = await sessionStore.createSession(title, props.path)
+    const session = await sessionStore.createSession(title, props.path, props.type)
     await llmStore.switchSession(session.session_id)
     emit('newSessionCreated')
   }
@@ -62,6 +73,7 @@ async function selectSession(sessionId: string) {
   await llmStore.switchSession(sessionId)
   sessionStore.setActiveSession(sessionId)
   historyDrawerVisible.value = false
+  sessionsDropdownVisible.value = false
 }
 
 async function closeSession(sessionId: string, event: Event) {
@@ -71,6 +83,11 @@ async function closeSession(sessionId: string, event: Event) {
     return // Don't delete sessions while LLM is generating output
   }
 
+  // Don't allow deleting the only session
+  if (sortedSessions.value.length <= 1) {
+    return
+  }
+
   const sessionIndex = sortedSessions.value.findIndex(s => s.session_id === sessionId)
 
   try {
@@ -177,10 +194,6 @@ function formatDate(dateStr: string) {
     return date.toLocaleDateString()
   }
 }
-
-function showHistoryDrawer() {
-  historyDrawerVisible.value = true
-}
 </script>
 
 <template>
@@ -223,9 +236,7 @@ function showHistoryDrawer() {
                   class="tab-action-btn"
                   @click.stop
                 >
-                  <template #icon>
-                    <MoreOutlined />
-                  </template>
+                  <MoreOutlined />
                 </AButton>
                 <template #overlay>
                   <AMenu>
@@ -237,16 +248,19 @@ function showHistoryDrawer() {
                       <CopyOutlined />
                       {{ $gettext('Duplicate') }}
                     </AMenuItem>
-                    <AMenuDivider />
-                    <AMenuItem danger @click="closeSession(session.session_id, $event)">
-                      <DeleteOutlined />
-                      {{ $gettext('Delete') }}
-                    </AMenuItem>
+                    <template v-if="sortedSessions.length > 1">
+                      <AMenuDivider />
+                      <AMenuItem danger @click="closeSession(session.session_id, $event)">
+                        <DeleteOutlined />
+                        {{ $gettext('Delete') }}
+                      </AMenuItem>
+                    </template>
                   </AMenu>
                 </template>
               </ADropdown>
 
               <AButton
+                v-if="sortedSessions.length > 1"
                 type="text"
                 size="small"
                 class="tab-close-btn"
@@ -261,16 +275,86 @@ function showHistoryDrawer() {
 
       <!-- Actions -->
       <div class="tab-actions-group">
-        <!-- History button (only show if there are sessions beyond the visible ones) -->
-        <AButton
-          v-if="historySessions.length > 0"
-          type="text"
-          size="small"
-          class="history-btn"
-          @click="showHistoryDrawer"
+        <!-- Sessions list button -->
+        <APopover
+          v-model:open="sessionsDropdownVisible"
+          :trigger="['click']"
+          placement="bottomRight"
+          overlay-class-name="sessions-popover"
+          @open-change="(open) => { if (!open) searchText = '' }"
         >
-          <ClockCircleOutlined />
-        </AButton>
+          <AButton
+            type="text"
+            size="small"
+            class="sessions-btn"
+          >
+            <HistoryOutlined />
+          </AButton>
+          <template #content>
+            <div class="sessions-dropdown">
+              <div class="sessions-search">
+                <AInput
+                  v-model:value="searchText"
+                  placeholder="Search sessions..."
+                  size="small"
+                  allow-clear
+                  @click.stop
+                />
+              </div>
+              <div class="sessions-list">
+                <div
+                  v-for="session in filteredSessions"
+                  :key="session.session_id"
+                  class="session-item"
+                  :class="{ active: session.session_id === activeSessionId }"
+                  @click="selectSession(session.session_id)"
+                >
+                  <div class="session-info">
+                    <div class="session-title">
+                      {{ session.title }}
+                    </div>
+                    <div class="session-meta">
+                      <span class="session-date">{{ formatDate(session.updated_at) }}</span>
+                      <span v-if="session.message_count > 0" class="session-count">
+                        {{ session.message_count }} messages
+                      </span>
+                    </div>
+                  </div>
+                  <div class="session-actions">
+                    <ADropdown :trigger="['click']" placement="bottomLeft">
+                      <AButton
+                        type="text"
+                        size="small"
+                        @click.stop
+                      >
+                        <MoreOutlined />
+                      </AButton>
+                      <template #overlay>
+                        <AMenu>
+                          <AMenuItem @click.stop="startEditingTitle(session.session_id, session.title, $event)">
+                            <EditOutlined />
+                            {{ $gettext('Rename') }}
+                          </AMenuItem>
+                          <AMenuItem @click.stop="duplicateSession(session.session_id, $event)">
+                            <CopyOutlined />
+                            {{ $gettext('Duplicate') }}
+                          </AMenuItem>
+                          <template v-if="sortedSessions.length > 1">
+                            <AMenuDivider />
+                            <AMenuItem danger @click.stop="closeSession(session.session_id, $event)">
+                              <DeleteOutlined />
+                              {{ $gettext('Delete') }}
+                            </AMenuItem>
+                          </template>
+                        </AMenu>
+                      </template>
+                    </ADropdown>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </template>
+        </APopover>
 
         <!-- Add new session button -->
         <AButton
@@ -284,89 +368,19 @@ function showHistoryDrawer() {
         </AButton>
       </div>
     </div>
-
-    <!-- History Drawer -->
-    <ADrawer
-      v-model:open="historyDrawerVisible"
-      title="Chat History"
-      placement="right"
-      :width="320"
-    >
-      <div class="history-list">
-        <div
-          v-for="session in historySessions"
-          :key="session.session_id"
-          class="history-item" :class="[
-            {
-              active: session.session_id === activeSessionId,
-              disabled: llmLoading,
-            },
-          ]"
-          @click="selectSession(session.session_id)"
-        >
-          <div class="history-content">
-            <div class="history-main">
-              <div class="history-title">
-                {{ session.title }}
-              </div>
-              <div class="history-meta">
-                <span class="history-date">{{ formatDate(session.updated_at) }}</span>
-                <span v-if="session.message_count > 0" class="history-count">
-                  {{ session.message_count }} messages
-                </span>
-              </div>
-            </div>
-
-            <div class="history-actions">
-              <ADropdown :trigger="['click']" placement="bottomLeft">
-                <AButton
-                  type="text"
-                  size="small"
-                  @click.stop
-                >
-                  <MoreOutlined />
-                </AButton>
-                <template #overlay>
-                  <AMenu>
-                    <AMenuItem @click="startEditingTitle(session.session_id, session.title, $event)">
-                      <EditOutlined />
-                      {{ $gettext('Rename') }}
-                    </AMenuItem>
-                    <AMenuItem @click="duplicateSession(session.session_id, $event)">
-                      <CopyOutlined />
-                      {{ $gettext('Duplicate') }}
-                    </AMenuItem>
-                    <AMenuDivider />
-                    <AMenuItem danger @click="closeSession(session.session_id, $event)">
-                      <DeleteOutlined />
-                      {{ $gettext('Delete') }}
-                    </AMenuItem>
-                  </AMenu>
-                </template>
-              </ADropdown>
-            </div>
-          </div>
-        </div>
-
-        <AEmpty
-          v-if="historySessions.length === 0"
-          :description="$gettext('No more sessions')"
-          size="small"
-        />
-      </div>
-    </ADrawer>
   </div>
 </template>
 
 <style lang="less" scoped>
 .llm-session-tabs {
-  background: rgba(255, 255, 255, 0.3);
+  background: rgba(255, 255, 255, 0.8);
   backdrop-filter: blur(10px);
   -webkit-backdrop-filter: blur(10px);
   width: 100%;
   position: sticky;
   top: 0;
-  z-index: 10;
+  z-index: 2;
+  box-shadow: 0 0 10px rgba(0, 0, 0, 0.04);
 
   .tabs-container {
     display: flex;
@@ -399,13 +413,13 @@ function showHistoryDrawer() {
     flex-shrink: 0;
     display: flex;
     align-items: center;
-    padding: 8px 12px;
+    padding: 8px 8px;
     cursor: pointer;
     transition: all 0.15s ease;
     background: transparent;
     border-right: 1px solid var(--color-border);
-    max-width: 240px;
-    min-width: 140px;
+    max-width: 120px;
+    min-width: 80px;
     position: relative;
     height: 34px;
     box-sizing: border-box;
@@ -428,23 +442,31 @@ function showHistoryDrawer() {
 
       .tab-actions {
         opacity: 1;
+        visibility: visible;
+        transform: translateY(-50%) translateX(0);
       }
     }
 
     &.active {
-      background: var(--color-bg-container);
+      background: var(--color-primary-light-9);
       color: var(--color-text-1);
       margin-bottom: -1px;
       z-index: 2;
       position: relative;
+      border-radius: 6px;
+      border: 1px solid var(--color-primary-light-7);
 
       .tab-title {
         font-weight: 500;
         color: var(--color-text-1);
       }
 
-      .tab-actions {
-        opacity: 1;
+      &:hover {
+        .tab-actions {
+          opacity: 1;
+          visibility: visible;
+          transform: translateY(-50%) translateX(0);
+        }
       }
     }
 
@@ -457,7 +479,6 @@ function showHistoryDrawer() {
   .tab-content {
     display: flex;
     align-items: center;
-    gap: 8px;
     width: 100%;
   }
 
@@ -466,7 +487,7 @@ function showHistoryDrawer() {
     overflow: hidden;
     text-overflow: ellipsis;
     white-space: nowrap;
-    font-size: 14px;
+    font-size: 13px;
     color: var(--color-text-2);
     transition: color 0.15s ease;
   }
@@ -476,7 +497,7 @@ function showHistoryDrawer() {
 
     :deep(.ant-input) {
       padding: 4px 0;
-      font-size: 14px;
+      font-size: 13px;
       background: transparent;
       border: none;
       color: var(--color-text-1);
@@ -492,11 +513,21 @@ function showHistoryDrawer() {
   }
 
   .tab-actions {
+    position: absolute;
+    right: 0;
+    top: 50%;
+    transform: translateY(-50%) translateX(8px);
     display: flex;
     align-items: center;
-    gap: 4px;
+    gap: 1px;
     opacity: 0;
-    transition: opacity 0.15s ease;
+    visibility: hidden;
+    transition: all 0.2s ease;
+    background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, #ffffff 20%);
+    border: 1px solid var(--color-border);
+    border-radius: 4px;
+    padding: 2px 3px 2px 6px;
+    z-index: 10;
   }
 
   .tab-action-btn,
@@ -542,6 +573,7 @@ function showHistoryDrawer() {
     margin-left: 8px;
     position: relative;
 
+    .sessions-btn,
     .history-btn,
     .add-btn {
       width: 24px;
@@ -583,14 +615,9 @@ function showHistoryDrawer() {
     border-radius: 8px;
     margin-bottom: 6px;
     transition: all 0.15s ease;
-    border: 1px solid transparent;
-    background: var(--color-bg-container);
 
     &:hover:not(.disabled) {
       background: var(--color-fill-1);
-      border-color: var(--color-border-2);
-      transform: translateY(-1px);
-      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
     }
 
     &.active {
@@ -684,6 +711,121 @@ function showHistoryDrawer() {
   }
 }
 
+.sessions-dropdown {
+  width: 360px;
+
+  .sessions-search {
+    padding: 8px 10px;
+    border-bottom: 1px solid var(--color-border);
+
+    :deep(.ant-input) {
+      border-radius: 6px;
+      font-size: 13px;
+    }
+  }
+
+  .sessions-list {
+    max-height: 380px;
+    overflow-y: auto;
+    padding: 4px 0;
+
+    &::-webkit-scrollbar {
+      width: 6px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: var(--color-fill-3);
+      border-radius: 3px;
+    }
+  }
+
+  .session-item {
+    padding: 6px 10px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    cursor: pointer;
+    transition: all 0.15s ease;
+
+    &:hover {
+      background: var(--color-fill-1);
+
+      .session-actions {
+        opacity: 1;
+      }
+    }
+
+    &.active {
+      background: var(--color-primary-light-9);
+
+      .session-title {
+        color: var(--color-primary);
+        font-weight: 500;
+      }
+    }
+
+    .session-info {
+      flex: 1;
+      min-width: 0;
+    }
+
+    .session-title {
+      font-size: 13px;
+      font-weight: 450;
+      margin-bottom: 2px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      color: var(--color-text-1);
+    }
+
+    .session-meta {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      font-size: 11px;
+      color: var(--color-text-3);
+
+      .session-date {
+        font-weight: 400;
+      }
+
+      .session-count {
+        padding: 0 4px;
+        background: var(--color-fill-2);
+        border-radius: 8px;
+        font-size: 10px;
+        line-height: 16px;
+        color: var(--color-text-2);
+      }
+    }
+
+    .session-actions {
+      opacity: 0;
+      transition: opacity 0.15s ease;
+
+      .ant-btn {
+        width: 20px;
+        height: 20px;
+        padding: 0;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+
+        :deep(.anticon) {
+          font-size: 11px;
+        }
+      }
+    }
+  }
+}
+
+:deep(.sessions-popover) {
+  .ant-popover-inner {
+    padding: 0;
+  }
+}
+
 .dark {
   .llm-session-tabs {
     background: rgba(30, 30, 30, 0.8);
@@ -701,8 +843,10 @@ function showHistoryDrawer() {
       }
 
       &.active {
-        background: rgba(30, 30, 30, 0.8);
+        background: rgba(var(--primary-6), 0.15);
         color: #ffffff;
+        border: 1px solid var(--color-primary);
+        border-radius: 6px;
       }
     }
 
@@ -721,6 +865,7 @@ function showHistoryDrawer() {
 
     .tab-title-input :deep(.ant-input) {
       color: #ffffff;
+      font-size: 13px;
 
       &:focus {
         background: rgba(255, 255, 255, 0.1);
@@ -746,6 +891,7 @@ function showHistoryDrawer() {
       background: transparent;
       border-color: rgba(255, 255, 255, 0.1);
 
+      .sessions-btn,
       .history-btn,
       .add-btn {
         color: rgba(255, 255, 255, 0.6);
@@ -760,12 +906,8 @@ function showHistoryDrawer() {
 
   .history-list {
     .history-item {
-      background: #1a1a1a;
-
       &:hover:not(.disabled) {
         background: #2a2a2a;
-        border-color: #404040;
-        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
       }
 
       &.active {
@@ -800,5 +942,50 @@ function showHistoryDrawer() {
       }
     }
   }
+
+  .tab-actions {
+    background: linear-gradient(to right, rgba(26, 26, 26, 0) 0%, #1a1a1a 20%);
+    border-color: rgba(255, 255, 255, 0.1);
+  }
+
+  .sessions-dropdown {
+    .sessions-search {
+      border-bottom-color: rgba(255, 255, 255, 0.1);
+    }
+
+    .session-item {
+      &:hover {
+        background: #2a2a2a;
+      }
+
+      &.active {
+        background: rgba(var(--primary-6), 0.15);
+
+        .session-title {
+          color: var(--color-primary);
+        }
+      }
+    }
+
+    .session-title {
+      color: #e8e8e8;
+    }
+
+    .session-meta {
+      color: #888888;
+
+      .session-count {
+        background: #333333;
+        color: #aaaaaa;
+      }
+    }
+  }
+
+  :deep(.sessions-popover) {
+    .ant-popover-inner {
+      background: #1a1a1a;
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
+    }
+  }
 }
 </style>

+ 6 - 1
app/src/components/LLM/chatService.ts

@@ -49,9 +49,11 @@ export class ChatService {
 
   // request: Send messages to server, receive SSE, and process chunks
   async request(
-    path: string | undefined,
+    type: string | undefined,
     messages: ChatComplicationMessage[],
     onProgress?: (message: ChatComplicationMessage) => void,
+    language?: string,
+    nginxConfig?: string,
   ): Promise<ChatComplicationMessage> {
     // Reset buffer flags each time
     this.buffer = ''
@@ -74,7 +76,10 @@ export class ChatService {
         Authorization: token.value,
       },
       body: JSON.stringify({
+        type,
         messages: requestMessages,
+        language,
+        nginx_config: nginxConfig,
       }),
     })
 

+ 41 - 21
app/src/components/LLM/llm.ts

@@ -7,6 +7,7 @@ import { useLLMSessionStore } from './sessionStore'
 export const useLLMStore = defineStore('llm', () => {
   // State
   const path = ref('')
+  const nginxConfig = ref('')
   const currentSessionId = ref<string | null>(null)
   const messages = ref<ChatComplicationMessage[]>([])
   const messageContainerRef = ref<HTMLDivElement>()
@@ -50,7 +51,10 @@ export const useLLMStore = defineStore('llm', () => {
       currentSessionId.value = sessionId
       const session = await llm.get_session(sessionId)
       messages.value = session.messages || []
-      path.value = session.path || ''
+      // Only update path if it's not already set to terminal assistant
+      if (path.value !== '__terminal_assistant__') {
+        path.value = session.path || ''
+      }
       cancelEdit()
     }
     catch (error) {
@@ -151,6 +155,16 @@ export const useLLMStore = defineStore('llm', () => {
     }
   }
 
+  // Current assistant type
+  const assistantType = ref<string>('nginx')
+
+  // Set assistant type for the current session
+  function setType(type: 'terminal') {
+    if (type === 'terminal') {
+      assistantType.value = 'terminal'
+    }
+  }
+
   // Clear chat record on server
   async function clearRecord() {
     if (!path.value)
@@ -316,7 +330,7 @@ export const useLLMStore = defineStore('llm', () => {
   }
 
   // Request: Send messages to server using chat service
-  async function request() {
+  async function request(language?: string) {
     setLoading(true)
     animationCoordinator.reset() // Reset all animation states
     animationCoordinator.setMessageStreaming(true)
@@ -330,17 +344,19 @@ export const useLLMStore = defineStore('llm', () => {
     try {
       const chatService = new ChatService()
       const assistantMessage = await chatService.request(
-        path.value,
+        assistantType.value,
         messages.value.slice(0, -1), // Exclude the empty assistant message
         message => {
           // Update the current assistant message in real-time
           updateLastAssistantMessage(message.content)
         },
+        language,
+        nginxConfig.value,
       )
 
       // Update the final content
       updateLastAssistantMessage(assistantMessage.content)
-      
+
       // If no typing animation starts within a reasonable time, end streaming
       // This handles cases where content is too short for typewriter effect
       setTimeout(() => {
@@ -363,7 +379,7 @@ export const useLLMStore = defineStore('llm', () => {
     }
     finally {
       // Don't clear streaming index immediately - let typewriter animation complete first
-      
+
       // Ensure all DOM updates are complete before final scroll
       await nextTick()
       await nextTick()
@@ -387,39 +403,35 @@ export const useLLMStore = defineStore('llm', () => {
 
         // Final scroll after everything is truly complete
         scrollToBottom(true)
+
+        // Generate session title after everything is complete
+        await tryGenerateSessionTitle()
       }, 100)
     }
   }
 
   // Send: Add user message into messages then call request
-  async function send(content: string, currentLanguage?: string, fileContent?: string) {
-    if (messages.value.length === 0) {
-      // The first message - include file content as context
-      const firstMessage = fileContent
-        ? `File Content:\n${fileContent}\n\n${content}\n\nCurrent Language Code: ${currentLanguage}`
-        : `${content}\n\nCurrent Language Code: ${currentLanguage}`
-      addUserMessage(firstMessage)
-    }
-    else {
-      // Append user's new message
-      addUserMessage(askBuffer.value)
-      clearAskBuffer()
-    }
+  async function send(content: string, currentLanguage?: string) {
+    // Add user message directly without embedding file content
+    addUserMessage(content)
+
+    // Clear ask buffer
+    clearAskBuffer()
 
     // Add empty assistant message for real-time updates
     addAssistantMessage('')
 
-    await request()
+    await request(currentLanguage)
   }
 
   // Regenerate: Removes messages after index and re-request the answer
-  async function regenerate(index: number) {
+  async function regenerate(index: number, currentLanguage?: string) {
     prepareRegenerate(index)
 
     // Add empty assistant message for real-time updates
     addAssistantMessage('')
 
-    await request()
+    await request(currentLanguage)
   }
 
   // Auto-generate title for sessions with user messages
@@ -445,6 +457,10 @@ export const useLLMStore = defineStore('llm', () => {
     }
   }
 
+  function setNginxConfig(config: string) {
+    nginxConfig.value = config
+  }
+
   // Listen for title animation trigger from coordinator
   onMounted(() => {
     window.addEventListener('startTitleAnimation', () => {
@@ -482,6 +498,7 @@ export const useLLMStore = defineStore('llm', () => {
   return {
     // State
     path,
+    nginxConfig,
     currentSessionId,
     messages,
     loading,
@@ -492,6 +509,7 @@ export const useLLMStore = defineStore('llm', () => {
     streamingMessageIndex,
     userScrolledUp,
     messageTypingCompleted,
+    assistantType,
 
     // Getters
     isEditing,
@@ -500,6 +518,7 @@ export const useLLMStore = defineStore('llm', () => {
 
     // Actions
     initMessages,
+    setNginxConfig,
     switchSession,
     saveSession,
     startEdit,
@@ -511,6 +530,7 @@ export const useLLMStore = defineStore('llm', () => {
     prepareRegenerate,
     clearMessages,
     storeRecord,
+    setType,
     clearRecord,
     setLoading,
     setAskBuffer,

+ 14 - 4
app/src/components/LLM/sessionStore.ts

@@ -26,10 +26,10 @@ export const useLLMSessionStore = defineStore('llm-session', () => {
   const hasActiveSession = computed(() => activeSessionId.value !== null)
 
   // Actions
-  async function loadSessions(path?: string) {
+  async function loadSessions(pathOrType?: string, isType?: boolean) {
     loading.value = true
     try {
-      const response = await llm.get_sessions(path)
+      const response = await llm.get_sessions(pathOrType, isType)
       sessions.value = response
     }
     catch (error) {
@@ -40,9 +40,19 @@ export const useLLMSessionStore = defineStore('llm-session', () => {
     }
   }
 
-  async function createSession(title: string, path?: string) {
+  async function createSession(title: string, path?: string, type?: string) {
     try {
-      const response = await llm.create_session({ title, path })
+      const sessionData: { title: string, path?: string, type?: string } = { title }
+
+      // For terminal type, don't pass path
+      if (type === 'terminal') {
+        sessionData.type = type
+      }
+      else if (path) {
+        sessionData.path = path
+      }
+
+      const response = await llm.create_session(sessionData)
       sessions.value.unshift(response)
       activeSessionId.value = response.session_id
       return response

+ 3 - 3
app/src/components/NginxControl/NginxControl.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import { ReloadOutlined } from '@ant-design/icons-vue'
-import { message } from 'ant-design-vue'
 import ngx from '@/api/ngx'
 import { NginxStatus } from '@/constants'
 import { useGlobalStore } from '@/pinia'
@@ -8,6 +7,7 @@ import { logLevel } from '@/views/config/constants'
 
 const global = useGlobalStore()
 const { nginxStatus: status } = storeToRefs(global)
+const { message } = App.useApp()
 
 async function getStatus() {
   const r = await ngx.status()
@@ -25,7 +25,7 @@ function reloadNginx() {
     if (r.level < logLevel.Warn)
       message.success($gettext('Nginx reloaded successfully'))
     else if (r.level === logLevel.Warn)
-      message.warn(r.message)
+      message.warning(r.message)
     else
       message.error(r.message)
   }).finally(() => getStatus())
@@ -39,7 +39,7 @@ async function restartNginx() {
     if (r.level < logLevel.Warn)
       message.success($gettext('Nginx restarted successfully'))
     else if (r.level === logLevel.Warn)
-      message.warn(r.message)
+      message.warning(r.message)
     else
       message.error(r.message)
   })

+ 2 - 1
app/src/components/Notification/Notification.vue

@@ -2,7 +2,6 @@
 import type { Ref } from 'vue'
 import type { Notification } from '@/api/notification'
 import { BellOutlined, CheckCircleOutlined, CloseCircleOutlined, DeleteOutlined, InfoCircleOutlined, WarningOutlined } from '@ant-design/icons-vue'
-import { message, notification } from 'ant-design-vue'
 import dayjs from 'dayjs'
 import relativeTime from 'dayjs/plugin/relativeTime'
 import notificationApi from '@/api/notification'
@@ -16,6 +15,8 @@ defineProps<{
   headerRef: HTMLElement
 }>()
 
+const { message, notification } = App.useApp()
+
 dayjs.extend(relativeTime)
 
 const loading = ref(false)

+ 2 - 1
app/src/components/TwoFA/use2FAModal.ts

@@ -1,10 +1,11 @@
-import { message, Modal } from 'ant-design-vue'
+import { Modal } from 'ant-design-vue'
 import { createVNode, render } from 'vue'
 import twoFA from '@/api/2fa'
 import Authorization from '@/components/TwoFA/Authorization.vue'
 import { useUserStore } from '@/pinia'
 
 function use2FAModal() {
+  const { message } = App.useApp()
   const refOTPAuthorization = ref<typeof Authorization>()
   // eslint-disable-next-line sonarjs/pseudo-random
   const randomId = Math.random().toString(36).substring(2, 8)

+ 24 - 18
app/src/components/UpstreamCards/UpstreamCards.vue

@@ -89,17 +89,18 @@ function getCardStatusColor(target: ProxyTarget): string {
 
 <style scoped lang="less">
 .upstream-cards {
-  margin-bottom: 24px;
+  padding: 0 12px;
+  margin-bottom: 16px;
 
   .upstream-header {
     display: flex;
     justify-content: space-between;
     align-items: center;
-    margin-bottom: 16px;
+    margin-bottom: 12px;
 
     .upstream-title {
       margin: 0;
-      font-size: 16px;
+      font-size: 14px;
       font-weight: 600;
       color: #333;
 
@@ -112,12 +113,12 @@ function getCardStatusColor(target: ProxyTarget): string {
       display: inline-flex;
       align-items: center;
       justify-content: center;
-      min-width: 20px;
-      height: 20px;
-      padding: 0 6px;
+      min-width: 18px;
+      height: 18px;
+      padding: 0 5px;
       background-color: #f0f0f0;
       color: #666;
-      font-size: 12px;
+      font-size: 11px;
       font-weight: 500;
       border-radius: 50%;
 
@@ -130,13 +131,13 @@ function getCardStatusColor(target: ProxyTarget): string {
 
   .cards-grid {
     display: grid;
-    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
-    gap: 16px;
+    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+    gap: 10px;
   }
 
   .upstream-card {
     border: 1px solid #e8e9ea;
-    border-radius: 8px;
+    border-radius: 6px;
     background: #ffffff;
     transition: all 0.2s ease;
 
@@ -149,28 +150,31 @@ function getCardStatusColor(target: ProxyTarget): string {
       cursor: pointer;
 
       &:hover {
-        box-shadow: 0 0 8px rgba(0, 0, 0, 0.09);
+        box-shadow: 0 0 6px rgba(0, 0, 0, 0.08);
 
         .dark & {
-          box-shadow: 0 0 8px rgba(255, 255, 255, 0.1);
+          box-shadow: 0 0 6px rgba(255, 255, 255, 0.08);
         }
       }
     }
 
     .card-content {
-      padding: 16px;
+      padding: 10px 12px;
 
       .card-info {
         display: flex;
         align-items: center;
-        gap: 8px;
+        gap: 6px;
 
         .card-status-text {
           font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
-          font-size: 12px;
+          font-size: 11px;
           color: #666;
-          line-height: 1.4;
+          line-height: 1.3;
           flex: 1;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
 
           .dark & {
             color: #999;
@@ -179,9 +183,11 @@ function getCardStatusColor(target: ProxyTarget): string {
 
         .type-tag {
           margin: 0;
-          font-size: 10px;
+          font-size: 9px;
           font-weight: bold;
-          border-radius: 4px;
+          border-radius: 3px;
+          padding: 0 4px;
+          line-height: 14px;
         }
       }
     }

+ 0 - 163
app/src/composables/useSSE.ts

@@ -1,163 +0,0 @@
-import type { SSEvent } from 'sse.js'
-import { storeToRefs } from 'pinia'
-import { SSE } from 'sse.js'
-import { urlJoin } from '@/lib/helper'
-import { useSettingsStore, useUserStore } from '@/pinia'
-
-const userStore = useUserStore()
-const { token } = storeToRefs(userStore)
-const settings = useSettingsStore()
-
-export interface SSEOptions {
-  url: string
-  // eslint-disable-next-line ts/no-explicit-any
-  onMessage?: (data: any) => void
-  onError?: () => void
-  parseData?: boolean
-  reconnectInterval?: number
-}
-
-/**
- * Build SSE URL based on environment
- */
-function buildSSEUrl(url: string): string {
-  // In development mode, connect directly to backend server
-  if (import.meta.env.DEV) {
-    const proxyTarget = import.meta.env.VITE_PROXY_TARGET || 'http://localhost:9000'
-
-    return urlJoin(proxyTarget, url)
-  }
-
-  // In production mode, use relative path
-  return urlJoin(window.location.pathname, url)
-}
-
-/**
- * SSE Composable
- * Provide the ability to create, manage, and automatically clean up SSE connections
- */
-export function useSSE() {
-  const sseInstance = shallowRef<SSE>()
-  const reconnectTimer = shallowRef<ReturnType<typeof setTimeout>>()
-  const isReconnecting = ref(false)
-  const currentOptions = shallowRef<SSEOptions>()
-
-  /**
-   * Clear reconnect timer
-   */
-  function clearReconnectTimer() {
-    if (reconnectTimer.value) {
-      clearTimeout(reconnectTimer.value)
-      reconnectTimer.value = undefined
-    }
-  }
-
-  /**
-   * Connect to SSE service
-   */
-  function connect(options: SSEOptions) {
-    const {
-      url,
-      onMessage,
-      onError,
-      parseData = true,
-      reconnectInterval = 5000,
-    } = options
-
-    // Store current options for reconnection
-    currentOptions.value = options
-
-    // Clear any existing reconnect timer
-    clearReconnectTimer()
-
-    // Disconnect existing connection before creating new one
-    if (sseInstance.value) {
-      sseInstance.value.close()
-    }
-
-    const fullUrl = buildSSEUrl(url)
-
-    const headers: Record<string, string> = {}
-
-    if (token.value) {
-      headers.Authorization = token.value
-    }
-
-    if (settings.node.id) {
-      headers['X-Node-ID'] = settings.node.id.toString()
-    }
-
-    const sse = new SSE(fullUrl, {
-      headers,
-    })
-
-    // Handle messages
-    sse.onmessage = (e: SSEvent) => {
-      if (!e.data) {
-        return
-      }
-
-      // Reset reconnecting state on successful message
-      isReconnecting.value = false
-
-      try {
-        const parsedData = parseData ? JSON.parse(e.data) : e.data
-        onMessage?.(parsedData)
-      }
-      catch (error) {
-        console.error('Error parsing SSE message:', error)
-      }
-    }
-
-    // Handle errors and reconnect
-    sse.onerror = () => {
-      onError?.()
-
-      // Only attempt reconnection if not already reconnecting and we have current options
-      if (!isReconnecting.value && currentOptions.value) {
-        isReconnecting.value = true
-
-        // Clear any existing timer before setting new one
-        clearReconnectTimer()
-
-        reconnectTimer.value = setTimeout(() => {
-          if (currentOptions.value && isReconnecting.value) {
-            connect(currentOptions.value)
-          }
-        }, reconnectInterval)
-      }
-    }
-
-    sseInstance.value = sse
-    return sse
-  }
-
-  /**
-   * Disconnect SSE connection
-   */
-  function disconnect() {
-    // Clear reconnect timer and state
-    clearReconnectTimer()
-    isReconnecting.value = false
-    currentOptions.value = undefined
-
-    if (sseInstance.value) {
-      sseInstance.value.close()
-      sseInstance.value = undefined
-    }
-  }
-
-  // Automatically disconnect when the component is unmounted
-  if (getCurrentInstance()) {
-    onUnmounted(() => {
-      disconnect()
-    })
-  }
-
-  return {
-    connect,
-    disconnect,
-    sseInstance,
-    isReconnecting: readonly(isReconnecting),
-  }
-}

Разница между файлами не показана из-за своего большого размера
+ 221 - 175
app/src/language/ar/app.po


Разница между файлами не показана из-за своего большого размера
+ 213 - 166
app/src/language/de_DE/app.po


+ 70 - 40
app/src/language/en/app.po

@@ -233,7 +233,7 @@ msgstr ""
 msgid "Additional"
 msgstr ""
 
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:100
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:101
 #: src/views/stream/components/StreamEditor.vue:63
 msgid "Advance Mode"
 msgstr ""
@@ -347,7 +347,7 @@ msgstr ""
 msgid "Are you sure you want to clear all notifications?"
 msgstr ""
 
-#: src/components/LLM/ChatMessageInput.vue:19
+#: src/components/LLM/ChatMessageInput.vue:62
 msgid "Are you sure you want to clear the record of chat?"
 msgstr ""
 
@@ -388,7 +388,7 @@ msgstr ""
 msgid "Ascending"
 msgstr ""
 
-#: src/components/LLM/ChatMessage.vue:187
+#: src/components/LLM/ChatMessage.vue:216
 msgid "Assistant"
 msgstr ""
 
@@ -500,7 +500,7 @@ msgstr ""
 #: src/views/config/components/ConfigLeftPanel.vue:273
 #: src/views/config/ConfigList.vue:120 src/views/config/ConfigList.vue:217
 #: src/views/nginx_log/NginxLog.vue:92
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:169
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:170
 #: src/views/stream/components/StreamEditor.vue:123
 msgid "Back"
 msgstr ""
@@ -604,13 +604,13 @@ msgstr ""
 msgid "Based on M2 Pro (12 cores) testing"
 msgstr ""
 
-#: src/views/config/components/ConfigRightPanel/ConfigRightPanel.vue:29
-#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:31
-#: src/views/stream/components/RightPanel/RightPanel.vue:19
+#: src/views/config/components/ConfigRightPanel/ConfigRightPanel.vue:41
+#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:43
+#: src/views/stream/components/RightPanel/RightPanel.vue:31
 msgid "Basic"
 msgstr ""
 
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:103
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:104
 #: src/views/stream/components/StreamEditor.vue:66
 msgid "Basic Mode"
 msgstr ""
@@ -710,7 +710,7 @@ msgid ""
 "performance depends on hardware, configuration, and workload"
 msgstr ""
 
-#: src/components/LLM/ChatMessage.vue:216
+#: src/components/LLM/ChatMessage.vue:245
 #: src/components/NgxConfigEditor/NgxServer.vue:61
 #: src/components/NgxConfigEditor/NgxUpstream.vue:32 src/language/curd.ts:37
 #: src/views/config/components/Delete.vue:98
@@ -897,12 +897,16 @@ msgstr ""
 msgid "Channel"
 msgstr ""
 
-#: src/views/config/components/ConfigRightPanel/ConfigRightPanel.vue:38
-#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:41
-#: src/views/stream/components/RightPanel/RightPanel.vue:22
+#: src/views/config/components/ConfigRightPanel/ConfigRightPanel.vue:50
+#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:53
+#: src/views/stream/components/RightPanel/RightPanel.vue:34
 msgid "Chat"
 msgstr ""
 
+#: src/components/LLM/LLM.vue:77
+msgid "Chat for %{path}"
+msgstr ""
+
 #: src/components/SelfCheck/SelfCheckHeaderBanner.vue:40
 #: src/components/SelfCheck/SelfCheckHeaderBanner.vue:64
 msgid "Check"
@@ -1026,7 +1030,7 @@ msgstr ""
 msgid "Cleaning environment variables"
 msgstr ""
 
-#: src/components/LLM/ChatMessageInput.vue:23
+#: src/components/LLM/ChatMessageInput.vue:66
 #: src/components/Notification/Notification.vue:115
 #: src/views/notification/Notification.vue:45
 msgid "Clear"
@@ -1177,7 +1181,7 @@ msgstr ""
 msgid "Config path is empty"
 msgstr ""
 
-#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:37
+#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:49
 msgid "Config Template"
 msgstr ""
 
@@ -1230,7 +1234,7 @@ msgstr ""
 msgid "Connection error, trying to reconnect..."
 msgstr ""
 
-#: src/views/terminal/Terminal.vue:149
+#: src/views/terminal/Terminal.vue:179
 msgid "Connection lost, please refresh the page."
 msgstr ""
 
@@ -1472,6 +1476,8 @@ msgstr ""
 msgid "Define shared memory zone name and size, e.g. proxy_cache:10m"
 msgstr ""
 
+#: src/components/LLM/LLMSessionTabs.vue:256
+#: src/components/LLM/LLMSessionTabs.vue:347
 #: src/components/NgxConfigEditor/NgxServer.vue:110
 #: src/components/NgxConfigEditor/NgxUpstream.vue:78 src/language/curd.ts:9
 #: src/views/certificate/components/RemoveCert.vue:98
@@ -1708,7 +1714,7 @@ msgstr ""
 #: src/views/preference/tabs/NodeSettings.vue:25
 #: src/views/preference/tabs/NodeSettings.vue:30
 #: src/views/site/components/SiteStatusSelect.vue:161
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:68
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:69
 #: src/views/site/site_list/columns.tsx:146 src/views/stream/columns.tsx:112
 #: src/views/stream/components/StreamEditor.vue:38
 #: src/views/user/userColumns.tsx:39
@@ -1836,6 +1842,8 @@ msgid ""
 "non-HTTPS websites, except when running on localhost."
 msgstr ""
 
+#: src/components/LLM/LLMSessionTabs.vue:250
+#: src/components/LLM/LLMSessionTabs.vue:341
 #: src/views/site/site_list/SiteDuplicate.vue:72
 #: src/views/site/site_list/SiteList.vue:86
 #: src/views/stream/components/StreamDuplicate.vue:64
@@ -1873,7 +1881,7 @@ msgstr ""
 msgid "Edit"
 msgstr ""
 
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:57
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:58
 #: src/views/stream/components/StreamEditor.vue:27
 msgid "Edit %{n}"
 msgstr ""
@@ -1913,7 +1921,7 @@ msgstr ""
 msgid "Enable 2FA successfully"
 msgstr ""
 
-#: src/views/nginx_log/NginxLogList.vue:451
+#: src/views/nginx_log/NginxLogList.vue:452
 msgid "Enable Advanced Indexing"
 msgstr ""
 
@@ -2023,7 +2031,7 @@ msgstr ""
 #: src/views/preference/tabs/NodeSettings.vue:25
 #: src/views/preference/tabs/NodeSettings.vue:30
 #: src/views/site/components/SiteStatusSelect.vue:158
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:62
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:63
 #: src/views/site/site_list/columns.tsx:142 src/views/stream/columns.tsx:108
 #: src/views/stream/components/RightPanel/Basic.vue:24
 #: src/views/stream/components/StreamEditor.vue:32
@@ -2848,6 +2856,10 @@ msgstr ""
 msgid "Hide"
 msgstr ""
 
+#: src/views/terminal/Terminal.vue:203
+msgid "Hide Assistant"
+msgstr ""
+
 #: src/composables/useGeoTranslation.ts:165
 #: src/views/nginx_log/dashboard/components/ChinaMapChart/ChinaMapChart.vue:135
 #: src/views/nginx_log/dashboard/components/WorldMapChart/WorldMapChart.vue:103
@@ -2859,7 +2871,7 @@ msgid "Higher value means better connection reuse"
 msgstr ""
 
 #: src/views/config/components/ConfigLeftPanel.vue:254
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:87
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:88
 #: src/views/stream/components/StreamEditor.vue:51
 msgid "History"
 msgstr ""
@@ -2980,7 +2992,7 @@ msgid "Indexing"
 msgstr ""
 
 #: src/views/nginx_log/components/LoadingState.vue:33
-#: src/views/nginx_log/NginxLogList.vue:440
+#: src/views/nginx_log/NginxLogList.vue:441
 msgid "Indexing logs..."
 msgstr ""
 
@@ -3104,7 +3116,7 @@ msgstr ""
 msgid "Invalid padding in decrypted data"
 msgstr ""
 
-#: src/components/TwoFA/use2FAModal.ts:61
+#: src/components/TwoFA/use2FAModal.ts:62
 msgid "Invalid passcode or recovery code"
 msgstr ""
 
@@ -3430,7 +3442,7 @@ msgstr ""
 msgid "Log indexing completed! Loading updated data..."
 msgstr ""
 
-#: src/routes/modules/nginx_log.ts:39 src/views/nginx_log/NginxLogList.vue:413
+#: src/routes/modules/nginx_log.ts:39 src/views/nginx_log/NginxLogList.vue:414
 msgid "Log List"
 msgstr ""
 
@@ -3476,7 +3488,7 @@ msgid "Main Node"
 msgstr ""
 
 #: src/views/site/components/SiteStatusSelect.vue:164
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:74
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:75
 #: src/views/site/site_list/columns.tsx:150
 msgid "Maintenance"
 msgstr ""
@@ -3672,7 +3684,7 @@ msgstr ""
 msgid "Modified At"
 msgstr ""
 
-#: src/components/LLM/ChatMessage.vue:212 src/views/config/ConfigList.vue:182
+#: src/components/LLM/ChatMessage.vue:241 src/views/config/ConfigList.vue:182
 msgid "Modify"
 msgstr ""
 
@@ -3777,6 +3789,10 @@ msgstr ""
 msgid "Network Statistics"
 msgstr ""
 
+#: src/components/LLM/LLM.vue:72
+msgid "New Chat"
+msgstr ""
+
 #: src/constants/errors/cert.ts:15
 msgid "New dns challenge provider error: {0}"
 msgstr ""
@@ -3873,7 +3889,7 @@ msgstr ""
 msgid "Nginx configuration has been restored"
 msgstr ""
 
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:121
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:122
 #: src/views/stream/components/StreamEditor.vue:81
 msgid "Nginx Configuration Parse Error"
 msgstr ""
@@ -4024,7 +4040,7 @@ msgstr ""
 msgid "Nginx.conf includes streams-enabled directory"
 msgstr ""
 
-#: src/components/LLM/ChatMessageInput.vue:17
+#: src/components/LLM/ChatMessageInput.vue:60
 #: src/components/NamespaceTabs/NamespaceTabs.vue:132
 #: src/components/NamespaceTabs/NamespaceTabs.vue:144
 #: src/components/NgxConfigEditor/directive/DirectiveEditorItem.vue:102
@@ -4249,7 +4265,7 @@ msgstr ""
 msgid "Offline GeoIP analysis"
 msgstr ""
 
-#: src/components/LLM/ChatMessageInput.vue:18
+#: src/components/LLM/ChatMessageInput.vue:61
 #: src/components/NgxConfigEditor/NgxServer.vue:60
 #: src/components/NgxConfigEditor/NgxUpstream.vue:31
 #: src/components/Notification/Notification.vue:109 src/language/curd.ts:15
@@ -4676,8 +4692,8 @@ msgstr ""
 msgid "Port 80 must be open for HTTP-01 challenge validation"
 msgstr ""
 
-#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:44
-#: src/views/stream/components/RightPanel/RightPanel.vue:25
+#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:56
+#: src/views/stream/components/RightPanel/RightPanel.vue:37
 msgid "Port Scanner"
 msgstr ""
 
@@ -4827,7 +4843,7 @@ msgstr ""
 msgid "Real-time analytics dashboard"
 msgstr ""
 
-#: src/views/nginx_log/NginxLogList.vue:478
+#: src/views/nginx_log/NginxLogList.vue:479
 msgid "Rebuild"
 msgstr ""
 
@@ -4884,7 +4900,7 @@ msgstr ""
 msgid "Refresh"
 msgstr ""
 
-#: src/components/LLM/ChatMessageInput.vue:30
+#: src/components/LLM/ChatMessageInput.vue:73
 msgid "Regenerate response"
 msgstr ""
 
@@ -4930,7 +4946,7 @@ msgstr ""
 msgid "Release Note"
 msgstr ""
 
-#: src/components/LLM/ChatMessage.vue:222
+#: src/components/LLM/ChatMessage.vue:251
 #: src/components/NginxControl/NginxControl.vue:103
 msgid "Reload"
 msgstr ""
@@ -4991,6 +5007,8 @@ msgstr ""
 msgid "Removed successfully"
 msgstr ""
 
+#: src/components/LLM/LLMSessionTabs.vue:246
+#: src/components/LLM/LLMSessionTabs.vue:337
 #: src/components/NgxConfigEditor/NgxUpstream.vue:75
 #: src/views/config/components/ConfigName.vue:51
 #: src/views/config/components/Rename.vue:56
@@ -5335,7 +5353,7 @@ msgstr ""
 msgid "Saturday"
 msgstr ""
 
-#: src/components/LLM/ChatMessage.vue:215
+#: src/components/LLM/ChatMessage.vue:244
 #: src/components/NgxConfigEditor/directive/DirectiveEditorItem.vue:132
 #: src/language/curd.ts:18
 #: src/views/certificate/components/CertificateActions.vue:29
@@ -5345,7 +5363,7 @@ msgstr ""
 #: src/views/preference/components/AuthSettings/Passkey.vue:130
 #: src/views/preference/Preference.vue:117
 #: src/views/site/site_edit/components/ConfigName/ConfigName.vue:52
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:176
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:177
 #: src/views/stream/components/ConfigName.vue:52
 #: src/views/stream/components/StreamEditor.vue:130
 msgid "Save"
@@ -5405,7 +5423,7 @@ msgstr ""
 #: src/components/NgxConfigEditor/directive/DirectiveEditorItem.vue:48
 #: src/language/curd.ts:28 src/views/config/components/ConfigLeftPanel.vue:198
 #: src/views/site/site_add/SiteAdd.vue:36
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:46
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:47
 #: src/views/stream/store.ts:70
 msgid "Saved successfully"
 msgstr ""
@@ -5632,6 +5650,10 @@ msgstr ""
 msgid "Show"
 msgstr ""
 
+#: src/views/terminal/Terminal.vue:203
+msgid "Show Assistant"
+msgstr ""
+
 #: src/views/other/Login.vue:295
 msgid "Sign in with a passkey"
 msgstr ""
@@ -6048,6 +6070,10 @@ msgstr ""
 msgid "Terminal"
 msgstr ""
 
+#: src/components/LLM/LLM.vue:74
+msgid "Terminal Assistant"
+msgstr ""
+
 #: src/views/preference/tabs/TerminalSettings.vue:10
 msgid "Terminal Start Command"
 msgstr ""
@@ -6497,7 +6523,7 @@ msgstr ""
 msgid "Tuesday"
 msgstr ""
 
-#: src/components/TwoFA/use2FAModal.ts:67
+#: src/components/TwoFA/use2FAModal.ts:68
 msgid "Two-factor authentication required"
 msgstr ""
 
@@ -6531,6 +6557,10 @@ msgstr ""
 msgid "Type or select status codes"
 msgstr ""
 
+#: src/components/LLM/ChatMessageInput.vue:80
+msgid "Type your message here..."
+msgstr ""
+
 #: src/views/nginx_log/structured/StructuredLogViewer.vue:778
 msgid "Unique Pages"
 msgstr ""
@@ -6636,7 +6666,7 @@ msgstr ""
 msgid "Use Temporary Path"
 msgstr ""
 
-#: src/components/LLM/ChatMessage.vue:187
+#: src/components/LLM/ChatMessage.vue:216
 msgid "User"
 msgstr ""
 
@@ -6709,7 +6739,7 @@ msgstr ""
 msgid "Version"
 msgstr ""
 
-#: src/language/curd.ts:7 src/views/nginx_log/NginxLogList.vue:467
+#: src/language/curd.ts:7 src/views/nginx_log/NginxLogList.vue:468
 #: src/views/site/site_edit/components/ConfigTemplate/ConfigTemplate.vue:108
 #: src/views/system/Licenses.vue:180 src/views/system/Licenses.vue:215
 #: src/views/system/Licenses.vue:250
@@ -6902,7 +6932,7 @@ msgstr ""
 msgid "Yes"
 msgstr ""
 
-#: src/views/terminal/Terminal.vue:142
+#: src/views/terminal/Terminal.vue:172
 msgid ""
 "You are accessing this terminal over an insecure HTTP connection on a non-"
 "localhost domain. This may expose sensitive information."

Разница между файлами не показана из-за своего большого размера
+ 211 - 164
app/src/language/es/app.po


Разница между файлами не показана из-за своего большого размера
+ 217 - 170
app/src/language/fr_FR/app.po


Разница между файлами не показана из-за своего большого размера
+ 268 - 157
app/src/language/ja_JP/app.po


Разница между файлами не показана из-за своего большого размера
+ 248 - 155
app/src/language/ko_KR/app.po


+ 70 - 40
app/src/language/messages.pot

@@ -244,7 +244,7 @@ msgstr ""
 msgid "Additional"
 msgstr ""
 
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:100
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:101
 #: src/views/stream/components/StreamEditor.vue:63
 msgid "Advance Mode"
 msgstr ""
@@ -355,7 +355,7 @@ msgstr ""
 msgid "Are you sure you want to clear all notifications?"
 msgstr ""
 
-#: src/components/LLM/ChatMessageInput.vue:19
+#: src/components/LLM/ChatMessageInput.vue:62
 msgid "Are you sure you want to clear the record of chat?"
 msgstr ""
 
@@ -397,7 +397,7 @@ msgstr ""
 msgid "Ascending"
 msgstr ""
 
-#: src/components/LLM/ChatMessage.vue:187
+#: src/components/LLM/ChatMessage.vue:216
 msgid "Assistant"
 msgstr ""
 
@@ -511,7 +511,7 @@ msgstr ""
 #: src/views/config/ConfigList.vue:120
 #: src/views/config/ConfigList.vue:217
 #: src/views/nginx_log/NginxLog.vue:92
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:169
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:170
 #: src/views/stream/components/StreamEditor.vue:123
 msgid "Back"
 msgstr ""
@@ -614,13 +614,13 @@ msgstr ""
 msgid "Based on M2 Pro (12 cores) testing"
 msgstr ""
 
-#: src/views/config/components/ConfigRightPanel/ConfigRightPanel.vue:29
-#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:31
-#: src/views/stream/components/RightPanel/RightPanel.vue:19
+#: src/views/config/components/ConfigRightPanel/ConfigRightPanel.vue:41
+#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:43
+#: src/views/stream/components/RightPanel/RightPanel.vue:31
 msgid "Basic"
 msgstr ""
 
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:103
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:104
 #: src/views/stream/components/StreamEditor.vue:66
 msgid "Basic Mode"
 msgstr ""
@@ -715,7 +715,7 @@ msgstr ""
 msgid "Calculated based on worker_processes * worker_connections. Actual performance depends on hardware, configuration, and workload"
 msgstr ""
 
-#: src/components/LLM/ChatMessage.vue:216
+#: src/components/LLM/ChatMessage.vue:245
 #: src/components/NgxConfigEditor/NgxServer.vue:61
 #: src/components/NgxConfigEditor/NgxUpstream.vue:32
 #: src/language/curd.ts:37
@@ -904,12 +904,16 @@ msgstr ""
 msgid "Channel"
 msgstr ""
 
-#: src/views/config/components/ConfigRightPanel/ConfigRightPanel.vue:38
-#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:41
-#: src/views/stream/components/RightPanel/RightPanel.vue:22
+#: src/views/config/components/ConfigRightPanel/ConfigRightPanel.vue:50
+#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:53
+#: src/views/stream/components/RightPanel/RightPanel.vue:34
 msgid "Chat"
 msgstr ""
 
+#: src/components/LLM/LLM.vue:77
+msgid "Chat for %{path}"
+msgstr ""
+
 #: src/components/SelfCheck/SelfCheckHeaderBanner.vue:40
 #: src/components/SelfCheck/SelfCheckHeaderBanner.vue:64
 msgid "Check"
@@ -1000,7 +1004,7 @@ msgstr ""
 msgid "Cleaning environment variables"
 msgstr ""
 
-#: src/components/LLM/ChatMessageInput.vue:23
+#: src/components/LLM/ChatMessageInput.vue:66
 #: src/components/Notification/Notification.vue:115
 #: src/views/notification/Notification.vue:45
 msgid "Clear"
@@ -1154,7 +1158,7 @@ msgstr ""
 msgid "Config path is empty"
 msgstr ""
 
-#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:37
+#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:49
 msgid "Config Template"
 msgstr ""
 
@@ -1207,7 +1211,7 @@ msgstr ""
 msgid "Connection error, trying to reconnect..."
 msgstr ""
 
-#: src/views/terminal/Terminal.vue:149
+#: src/views/terminal/Terminal.vue:179
 msgid "Connection lost, please refresh the page."
 msgstr ""
 
@@ -1447,6 +1451,8 @@ msgstr ""
 msgid "Define shared memory zone name and size, e.g. proxy_cache:10m"
 msgstr ""
 
+#: src/components/LLM/LLMSessionTabs.vue:256
+#: src/components/LLM/LLMSessionTabs.vue:347
 #: src/components/NgxConfigEditor/NgxServer.vue:110
 #: src/components/NgxConfigEditor/NgxUpstream.vue:78
 #: src/language/curd.ts:9
@@ -1688,7 +1694,7 @@ msgstr ""
 #: src/views/preference/tabs/NodeSettings.vue:25
 #: src/views/preference/tabs/NodeSettings.vue:30
 #: src/views/site/components/SiteStatusSelect.vue:161
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:68
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:69
 #: src/views/site/site_list/columns.tsx:146
 #: src/views/stream/columns.tsx:112
 #: src/views/stream/components/StreamEditor.vue:38
@@ -1816,6 +1822,8 @@ msgstr ""
 msgid "Due to the security policies of some browsers, you cannot use passkeys on non-HTTPS websites, except when running on localhost."
 msgstr ""
 
+#: src/components/LLM/LLMSessionTabs.vue:250
+#: src/components/LLM/LLMSessionTabs.vue:341
 #: src/views/site/site_list/SiteDuplicate.vue:72
 #: src/views/site/site_list/SiteList.vue:86
 #: src/views/stream/components/StreamDuplicate.vue:64
@@ -1853,7 +1861,7 @@ msgstr ""
 msgid "Edit"
 msgstr ""
 
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:57
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:58
 #: src/views/stream/components/StreamEditor.vue:27
 msgid "Edit %{n}"
 msgstr ""
@@ -1893,7 +1901,7 @@ msgstr ""
 msgid "Enable 2FA successfully"
 msgstr ""
 
-#: src/views/nginx_log/NginxLogList.vue:451
+#: src/views/nginx_log/NginxLogList.vue:452
 msgid "Enable Advanced Indexing"
 msgstr ""
 
@@ -2004,7 +2012,7 @@ msgstr ""
 #: src/views/preference/tabs/NodeSettings.vue:25
 #: src/views/preference/tabs/NodeSettings.vue:30
 #: src/views/site/components/SiteStatusSelect.vue:158
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:62
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:63
 #: src/views/site/site_list/columns.tsx:142
 #: src/views/stream/columns.tsx:108
 #: src/views/stream/components/RightPanel/Basic.vue:24
@@ -2816,6 +2824,10 @@ msgstr ""
 msgid "Hide"
 msgstr ""
 
+#: src/views/terminal/Terminal.vue:203
+msgid "Hide Assistant"
+msgstr ""
+
 #: src/composables/useGeoTranslation.ts:165
 #: src/views/nginx_log/dashboard/components/ChinaMapChart/ChinaMapChart.vue:135
 #: src/views/nginx_log/dashboard/components/WorldMapChart/WorldMapChart.vue:103
@@ -2827,7 +2839,7 @@ msgid "Higher value means better connection reuse"
 msgstr ""
 
 #: src/views/config/components/ConfigLeftPanel.vue:254
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:87
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:88
 #: src/views/stream/components/StreamEditor.vue:51
 msgid "History"
 msgstr ""
@@ -2940,7 +2952,7 @@ msgid "Indexing"
 msgstr ""
 
 #: src/views/nginx_log/components/LoadingState.vue:33
-#: src/views/nginx_log/NginxLogList.vue:440
+#: src/views/nginx_log/NginxLogList.vue:441
 msgid "Indexing logs..."
 msgstr ""
 
@@ -3063,7 +3075,7 @@ msgstr ""
 msgid "Invalid padding in decrypted data"
 msgstr ""
 
-#: src/components/TwoFA/use2FAModal.ts:61
+#: src/components/TwoFA/use2FAModal.ts:62
 msgid "Invalid passcode or recovery code"
 msgstr ""
 
@@ -3388,7 +3400,7 @@ msgid "Log indexing completed! Loading updated data..."
 msgstr ""
 
 #: src/routes/modules/nginx_log.ts:39
-#: src/views/nginx_log/NginxLogList.vue:413
+#: src/views/nginx_log/NginxLogList.vue:414
 msgid "Log List"
 msgstr ""
 
@@ -3429,7 +3441,7 @@ msgid "Main Node"
 msgstr ""
 
 #: src/views/site/components/SiteStatusSelect.vue:164
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:74
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:75
 #: src/views/site/site_list/columns.tsx:150
 msgid "Maintenance"
 msgstr ""
@@ -3626,7 +3638,7 @@ msgstr ""
 msgid "Modified At"
 msgstr ""
 
-#: src/components/LLM/ChatMessage.vue:212
+#: src/components/LLM/ChatMessage.vue:241
 #: src/views/config/ConfigList.vue:182
 msgid "Modify"
 msgstr ""
@@ -3738,6 +3750,10 @@ msgstr ""
 msgid "Network Statistics"
 msgstr ""
 
+#: src/components/LLM/LLM.vue:72
+msgid "New Chat"
+msgstr ""
+
 #: src/constants/errors/cert.ts:15
 msgid "New dns challenge provider error: {0}"
 msgstr ""
@@ -3835,7 +3851,7 @@ msgstr ""
 msgid "Nginx configuration has been restored"
 msgstr ""
 
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:121
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:122
 #: src/views/stream/components/StreamEditor.vue:81
 msgid "Nginx Configuration Parse Error"
 msgstr ""
@@ -3987,7 +4003,7 @@ msgstr ""
 msgid "Nginx.conf includes streams-enabled directory"
 msgstr ""
 
-#: src/components/LLM/ChatMessageInput.vue:17
+#: src/components/LLM/ChatMessageInput.vue:60
 #: src/components/NamespaceTabs/NamespaceTabs.vue:132
 #: src/components/NamespaceTabs/NamespaceTabs.vue:144
 #: src/components/NgxConfigEditor/directive/DirectiveEditorItem.vue:102
@@ -4208,7 +4224,7 @@ msgstr ""
 msgid "Offline GeoIP analysis"
 msgstr ""
 
-#: src/components/LLM/ChatMessageInput.vue:18
+#: src/components/LLM/ChatMessageInput.vue:61
 #: src/components/NgxConfigEditor/NgxServer.vue:60
 #: src/components/NgxConfigEditor/NgxUpstream.vue:31
 #: src/components/Notification/Notification.vue:109
@@ -4626,8 +4642,8 @@ msgstr ""
 msgid "Port 80 must be open for HTTP-01 challenge validation"
 msgstr ""
 
-#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:44
-#: src/views/stream/components/RightPanel/RightPanel.vue:25
+#: src/views/site/site_edit/components/RightPanel/RightPanel.vue:56
+#: src/views/stream/components/RightPanel/RightPanel.vue:37
 msgid "Port Scanner"
 msgstr ""
 
@@ -4780,7 +4796,7 @@ msgstr ""
 msgid "Real-time analytics dashboard"
 msgstr ""
 
-#: src/views/nginx_log/NginxLogList.vue:478
+#: src/views/nginx_log/NginxLogList.vue:479
 msgid "Rebuild"
 msgstr ""
 
@@ -4835,7 +4851,7 @@ msgstr ""
 msgid "Refresh"
 msgstr ""
 
-#: src/components/LLM/ChatMessageInput.vue:30
+#: src/components/LLM/ChatMessageInput.vue:73
 msgid "Regenerate response"
 msgstr ""
 
@@ -4879,7 +4895,7 @@ msgstr ""
 msgid "Release Note"
 msgstr ""
 
-#: src/components/LLM/ChatMessage.vue:222
+#: src/components/LLM/ChatMessage.vue:251
 #: src/components/NginxControl/NginxControl.vue:103
 msgid "Reload"
 msgstr ""
@@ -4942,6 +4958,8 @@ msgstr ""
 msgid "Removed successfully"
 msgstr ""
 
+#: src/components/LLM/LLMSessionTabs.vue:246
+#: src/components/LLM/LLMSessionTabs.vue:337
 #: src/components/NgxConfigEditor/NgxUpstream.vue:75
 #: src/views/config/components/ConfigName.vue:51
 #: src/views/config/components/Rename.vue:56
@@ -5285,7 +5303,7 @@ msgstr ""
 msgid "Saturday"
 msgstr ""
 
-#: src/components/LLM/ChatMessage.vue:215
+#: src/components/LLM/ChatMessage.vue:244
 #: src/components/NgxConfigEditor/directive/DirectiveEditorItem.vue:132
 #: src/language/curd.ts:18
 #: src/views/certificate/components/CertificateActions.vue:29
@@ -5295,7 +5313,7 @@ msgstr ""
 #: src/views/preference/components/AuthSettings/Passkey.vue:130
 #: src/views/preference/Preference.vue:117
 #: src/views/site/site_edit/components/ConfigName/ConfigName.vue:52
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:176
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:177
 #: src/views/stream/components/ConfigName.vue:52
 #: src/views/stream/components/StreamEditor.vue:130
 msgid "Save"
@@ -5357,7 +5375,7 @@ msgstr ""
 #: src/language/curd.ts:28
 #: src/views/config/components/ConfigLeftPanel.vue:198
 #: src/views/site/site_add/SiteAdd.vue:36
-#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:46
+#: src/views/site/site_edit/components/SiteEditor/SiteEditor.vue:47
 #: src/views/stream/store.ts:70
 msgid "Saved successfully"
 msgstr ""
@@ -5580,6 +5598,10 @@ msgstr ""
 msgid "Show"
 msgstr ""
 
+#: src/views/terminal/Terminal.vue:203
+msgid "Show Assistant"
+msgstr ""
+
 #: src/views/other/Login.vue:295
 msgid "Sign in with a passkey"
 msgstr ""
@@ -6000,6 +6022,10 @@ msgstr ""
 msgid "Terminal"
 msgstr ""
 
+#: src/components/LLM/LLM.vue:74
+msgid "Terminal Assistant"
+msgstr ""
+
 #: src/views/preference/tabs/TerminalSettings.vue:10
 msgid "Terminal Start Command"
 msgstr ""
@@ -6395,7 +6421,7 @@ msgstr ""
 msgid "Tuesday"
 msgstr ""
 
-#: src/components/TwoFA/use2FAModal.ts:67
+#: src/components/TwoFA/use2FAModal.ts:68
 msgid "Two-factor authentication required"
 msgstr ""
 
@@ -6429,6 +6455,10 @@ msgstr ""
 msgid "Type or select status codes"
 msgstr ""
 
+#: src/components/LLM/ChatMessageInput.vue:80
+msgid "Type your message here..."
+msgstr ""
+
 #: src/views/nginx_log/structured/StructuredLogViewer.vue:778
 msgid "Unique Pages"
 msgstr ""
@@ -6540,7 +6570,7 @@ msgstr ""
 msgid "Use Temporary Path"
 msgstr ""
 
-#: src/components/LLM/ChatMessage.vue:187
+#: src/components/LLM/ChatMessage.vue:216
 msgid "User"
 msgstr ""
 
@@ -6617,7 +6647,7 @@ msgid "Version"
 msgstr ""
 
 #: src/language/curd.ts:7
-#: src/views/nginx_log/NginxLogList.vue:467
+#: src/views/nginx_log/NginxLogList.vue:468
 #: src/views/site/site_edit/components/ConfigTemplate/ConfigTemplate.vue:108
 #: src/views/system/Licenses.vue:180
 #: src/views/system/Licenses.vue:215
@@ -6797,7 +6827,7 @@ msgstr ""
 msgid "Yes"
 msgstr ""
 
-#: src/views/terminal/Terminal.vue:142
+#: src/views/terminal/Terminal.vue:172
 msgid "You are accessing this terminal over an insecure HTTP connection on a non-localhost domain. This may expose sensitive information."
 msgstr ""
 

Разница между файлами не показана из-за своего большого размера
+ 221 - 168
app/src/language/pt_PT/app.po


Разница между файлами не показана из-за своего большого размера
+ 214 - 170
app/src/language/ru_RU/app.po


Разница между файлами не показана из-за своего большого размера
+ 214 - 166
app/src/language/tr_TR/app.po


Разница между файлами не показана из-за своего большого размера
+ 221 - 178
app/src/language/uk_UA/app.po


Разница между файлами не показана из-за своего большого размера
+ 217 - 174
app/src/language/vi_VN/app.po


Разница между файлами не показана из-за своего большого размера
+ 245 - 157
app/src/language/zh_CN/app.po


Разница между файлами не показана из-за своего большого размера
+ 250 - 163
app/src/language/zh_TW/app.po


+ 1 - 1
app/src/lib/http/error.ts

@@ -1,5 +1,4 @@
 import type { CosyError, CosyErrorRecord } from './types'
-import { message } from 'ant-design-vue'
 
 const errors: Record<string, CosyErrorRecord> = {}
 
@@ -20,6 +19,7 @@ export function useMessageDedupe(interval = 5000): MessageDedupe {
       const now = Date.now()
       if (!lastMessages.has(content) || (now - (lastMessages.get(content) || 0)) > interval) {
         lastMessages.set(content, now)
+        const { message } = App.useApp()
         message.error(content, duration)
       }
     },

+ 2 - 1
app/src/views/backup/components/BackupCreator.vue

@@ -1,9 +1,10 @@
 <script setup lang="tsx">
 import { CheckOutlined, CopyOutlined, InfoCircleFilled, WarningOutlined } from '@ant-design/icons-vue'
 import { UseClipboard } from '@vueuse/components'
-import { message } from 'ant-design-vue'
 import backup from '@/api/backup'
 
+const { message } = App.useApp()
+
 const isCreatingBackup = ref(false)
 const showSecurityModal = ref(false)
 const currentSecurityToken = ref('')

+ 3 - 1
app/src/views/certificate/ACMEUser.vue

@@ -2,10 +2,12 @@
 import type { CustomRenderArgs, StdTableColumn } from '@uozi-admin/curd'
 import type { AcmeUser } from '@/api/acme_user'
 import { datetimeRender, StdCurd } from '@uozi-admin/curd'
-import { message, Tag } from 'ant-design-vue'
+import { Tag } from 'ant-design-vue'
 
 import acme_user from '@/api/acme_user'
 
+const { message } = App.useApp()
+
 const columns: StdTableColumn[] = [
   {
     title: () => $gettext('Name'),

+ 3 - 1
app/src/views/certificate/CertificateEditor.vue

@@ -1,7 +1,6 @@
 <script setup lang="ts">
 import type { Ref } from 'vue'
 import type { Cert } from '@/api/cert'
-import { message } from 'ant-design-vue'
 import cert from '@/api/cert'
 import { AutoCertState } from '@/constants'
 
@@ -12,6 +11,8 @@ import CertificateContentEditor from './components/CertificateContentEditor.vue'
 import CertificateDownload from './components/CertificateDownload.vue'
 import { useCertStore } from './store'
 
+const { message } = App.useApp()
+
 const route = useRoute()
 const certStore = useCertStore()
 const router = useRouter()
@@ -45,6 +46,7 @@ onMounted(() => {
 async function save() {
   try {
     await certStore.save()
+    message.success($gettext('Save successfully'))
     errors.value = {}
     await router.push(`/certificates/${certStore.data.id}`)
   }

+ 11 - 10
app/src/views/certificate/components/CertificateBasicInfo.vue

@@ -2,17 +2,18 @@
 import type { Cert } from '@/api/cert'
 import { CopyOutlined } from '@ant-design/icons-vue'
 import { useClipboard } from '@vueuse/core'
-import { message } from 'ant-design-vue'
 import NodeSelector from '@/components/NodeSelector'
 
 interface Props {
   data: Cert
-  errors: Record<string, string>
+  errors?: Record<string, string>
   isManaged: boolean
 }
 
 defineProps<Props>()
 
+const { message } = App.useApp()
+
 // Use defineModel for two-way binding
 const data = defineModel<Cert>('data', { required: true })
 
@@ -41,8 +42,8 @@ async function copyToClipboard(text: string, label: string) {
   >
     <AFormItem
       :label="$gettext('Name')"
-      :validate-status="errors.name ? 'error' : ''"
-      :help="errors.name === 'required'
+      :validate-status="errors?.name ? 'error' : ''"
+      :help="errors?.name === 'required'
         ? $gettext('This field is required')
         : ''"
     >
@@ -75,9 +76,9 @@ async function copyToClipboard(text: string, label: string) {
 
     <AFormItem
       :label="$gettext('SSL Certificate Path')"
-      :validate-status="errors.ssl_certificate_path ? 'error' : ''"
-      :help="errors.ssl_certificate_path === 'required' ? $gettext('This field is required')
-        : errors.ssl_certificate_path === 'certificate_path'
+      :validate-status="errors?.ssl_certificate_path ? 'error' : ''"
+      :help="errors?.ssl_certificate_path === 'required' ? $gettext('This field is required')
+        : errors?.ssl_certificate_path === 'certificate_path'
           ? $gettext('The path exists, but the file is not a certificate') : ''"
     >
       <div v-if="isManaged" class="copy-container">
@@ -109,9 +110,9 @@ async function copyToClipboard(text: string, label: string) {
 
     <AFormItem
       :label="$gettext('SSL Certificate Key Path')"
-      :validate-status="errors.ssl_certificate_key_path ? 'error' : ''"
-      :help="errors.ssl_certificate_key_path === 'required' ? $gettext('This field is required')
-        : errors.ssl_certificate_key_path === 'privatekey_path'
+      :validate-status="errors?.ssl_certificate_key_path ? 'error' : ''"
+      :help="errors?.ssl_certificate_key_path === 'required' ? $gettext('This field is required')
+        : errors?.ssl_certificate_key_path === 'privatekey_path'
           ? $gettext('The path exists, but the file is not a private key') : ''"
     >
       <div v-if="isManaged" class="copy-container">

+ 7 - 6
app/src/views/certificate/components/CertificateContentEditor.vue

@@ -2,18 +2,19 @@
 import type { Cert } from '@/api/cert'
 import { CopyOutlined, InboxOutlined } from '@ant-design/icons-vue'
 import { useClipboard } from '@vueuse/core'
-import { message } from 'ant-design-vue'
 import CodeEditor from '@/components/CodeEditor'
 import CertificateFileUpload from './CertificateFileUpload.vue'
 
 interface Props {
   data: Cert
-  errors: Record<string, string>
+  errors?: Record<string, string>
   readonly: boolean
 }
 
 defineProps<Props>()
 
+const { message } = App.useApp()
+
 // Use defineModel for two-way binding
 const data = defineModel<Cert>('data', { required: true })
 
@@ -109,8 +110,8 @@ function handleDrop(e: DragEvent, type: 'certificate' | 'key') {
   <div class="certificate-content-editor">
     <!-- SSL Certificate Content -->
     <AFormItem
-      :validate-status="errors.ssl_certificate ? 'error' : ''"
-      :help="errors.ssl_certificate === 'certificate'
+      :validate-status="errors?.ssl_certificate ? 'error' : ''"
+      :help="errors?.ssl_certificate === 'certificate'
         ? $gettext('The input is not a SSL Certificate') : ''"
     >
       <template #label>
@@ -170,8 +171,8 @@ function handleDrop(e: DragEvent, type: 'certificate' | 'key') {
 
     <!-- SSL Certificate Key Content -->
     <AFormItem
-      :validate-status="errors.ssl_certificate_key ? 'error' : ''"
-      :help="errors.ssl_certificate_key === 'privatekey'
+      :validate-status="errors?.ssl_certificate_key ? 'error' : ''"
+      :help="errors?.ssl_certificate_key === 'privatekey'
         ? $gettext('The input is not a SSL Certificate Key') : ''"
     >
       <template #label>

+ 2 - 1
app/src/views/certificate/components/CertificateDownload.vue

@@ -1,7 +1,6 @@
 <script setup lang="ts">
 import type { Cert } from '@/api/cert'
 import { DownloadOutlined } from '@ant-design/icons-vue'
-import { message } from 'ant-design-vue'
 
 interface Props {
   data: Cert
@@ -9,6 +8,8 @@ interface Props {
 
 const props = defineProps<Props>()
 
+const { message } = App.useApp()
+
 // Download state
 const isDownloading = ref(false)
 

+ 2 - 1
app/src/views/certificate/components/CertificateFileUpload.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import { UploadOutlined } from '@ant-design/icons-vue'
-import { message } from 'ant-design-vue'
 
 interface Props {
   type: 'certificate' | 'key'
@@ -15,6 +14,8 @@ const emit = defineEmits<{
   upload: [content: string]
 }>()
 
+const { message } = App.useApp()
+
 // File upload state
 const fileInput = ref<HTMLInputElement>()
 

+ 2 - 1
app/src/views/certificate/components/DNSIssueCertificate.vue

@@ -1,7 +1,6 @@
 <script setup lang="ts">
 import type { Ref } from 'vue'
 import type { AutoCertOptions } from '@/api/auto_cert'
-import { message } from 'ant-design-vue'
 import AutoCertForm from '@/components/AutoCertForm'
 import ObtainCertLive from '@/views/site/site_edit/components/Cert/ObtainCertLive.vue'
 
@@ -9,6 +8,8 @@ const emit = defineEmits<{
   issued: [void]
 }>()
 
+const { message } = App.useApp()
+
 const step = ref(0)
 const visible = ref(false)
 const data = ref({}) as Ref<AutoCertOptions>

+ 2 - 1
app/src/views/certificate/components/RemoveCert.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import type { Cert } from '@/api/cert'
-import { message } from 'ant-design-vue'
 import cert from '@/api/cert'
 import { AutoCertState } from '@/constants'
 import websocket from '@/lib/websocket'
@@ -13,6 +12,8 @@ const props = defineProps<{
 
 const emit = defineEmits(['removed'])
 
+const { message } = App.useApp()
+
 const modalVisible = ref(false)
 const confirmLoading = ref(false)
 const shouldRevoke = ref(false)

+ 4 - 1
app/src/views/certificate/components/RenewCert.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import type { AutoCertOptions } from '@/api/auto_cert'
-import { message } from 'ant-design-vue'
 import { useGlobalStore } from '@/pinia'
 import ObtainCertLive from '@/views/site/site_edit/components/Cert/ObtainCertLive.vue'
 import { useCertStore } from '../store'
@@ -13,6 +12,8 @@ const emit = defineEmits<{
   renewed: [void]
 }>()
 
+const { message } = App.useApp()
+
 const certStore = useCertStore()
 
 const modalVisible = ref(false)
@@ -22,6 +23,8 @@ const refObtainCertLive = useTemplateRef('refObtainCertLive')
 async function issueCert() {
   await certStore.save()
 
+  message.success($gettext('Save successfully'))
+
   modalVisible.value = true
 
   const { name, domains, key_type } = props.options

+ 0 - 2
app/src/views/certificate/store.ts

@@ -1,5 +1,4 @@
 import type { Cert } from '@/api/cert'
-import { message } from 'ant-design-vue'
 import cert from '@/api/cert'
 
 export const useCertStore = defineStore('cert', () => {
@@ -10,7 +9,6 @@ export const useCertStore = defineStore('cert', () => {
       ? await cert.updateItem(data.value.id, data.value)
       : await cert.createItem(data.value)
     data.value = r
-    message.success($gettext('Save successfully'))
   }
 
   return {

+ 6 - 0
app/src/views/config/components/ConfigRightPanel/Chat.vue

@@ -2,6 +2,11 @@
 import type { Config } from '@/api/config'
 import LLM from '@/components/LLM'
 
+interface Props {
+  chatHeight: string
+}
+
+defineProps<Props>()
 const data = defineModel<Config>('data', { required: true })
 </script>
 
@@ -10,6 +15,7 @@ const data = defineModel<Config>('data', { required: true })
     <LLM
       :content="data.content"
       :path="data.filepath"
+      :height="chatHeight"
     />
   </div>
 </template>

+ 14 - 2
app/src/views/config/components/ConfigRightPanel/ConfigRightPanel.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
 import type { Config } from '@/api/config'
+import { useElementSize } from '@vueuse/core'
 import Basic from './Basic.vue'
 import Chat from './Chat.vue'
 
@@ -14,10 +15,21 @@ const props = defineProps<ConfigRightPanelProps>()
 const data = defineModel<Config>('data', { required: true })
 
 const activeKey = ref('basic')
+
+// Get container height for Chat component
+const containerRef = ref<HTMLElement>()
+const { height: containerHeight } = useElementSize(containerRef)
+
+// Calculate chat height (container height - tabs nav height - padding)
+const chatHeight = computed(() => {
+  const tabsNavHeight = 55
+  const padding = 48 // top and bottom padding
+  return `${containerHeight.value - tabsNavHeight - padding}px`
+})
 </script>
 
 <template>
-  <div class="right-settings-container">
+  <div ref="containerRef" class="right-settings-container">
     <ACard
       class="right-settings"
       :bordered="false"
@@ -36,7 +48,7 @@ const activeKey = ref('basic')
           />
         </ATabPane>
         <ATabPane key="chat" :tab="$gettext('Chat')">
-          <Chat v-model:data="data" />
+          <Chat v-model:data="data" :chat-height="chatHeight" />
         </ATabPane>
       </ATabs>
     </ACard>

+ 3 - 1
app/src/views/nginx_log/NginxLogList.vue

@@ -5,7 +5,7 @@ import type { TabOption } from '@/components/TabFilter'
 import { CheckCircleOutlined, ExclamationCircleOutlined, SyncOutlined } from '@ant-design/icons-vue'
 import { StdCurd } from '@uozi-admin/curd'
 import { useRouteQuery } from '@vueuse/router'
-import { Badge, message, Tag, Tooltip } from 'ant-design-vue'
+import { Badge, Tag, Tooltip } from 'ant-design-vue'
 import dayjs from 'dayjs'
 import nginxLog from '@/api/nginx_log'
 import { TabFilter } from '@/components/TabFilter'
@@ -16,6 +16,8 @@ import { useIndexProgress } from './composables/useIndexProgress'
 import IndexProgressBar from './indexing/components/IndexProgressBar.vue'
 import IndexManagement from './indexing/IndexManagement.vue'
 
+const { message } = App.useApp()
+
 const router = useRouter()
 const stdCurdRef = ref()
 const indexManagementRef = ref()

+ 3 - 1
app/src/views/nginx_log/indexing/IndexManagement.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { message, Modal } from 'ant-design-vue'
+import { Modal } from 'ant-design-vue'
 import nginxLog from '@/api/nginx_log'
 
 // Props
@@ -18,6 +18,8 @@ const emit = defineEmits<{
   refresh: []
 }>()
 
+const { message } = App.useApp()
+
 // Reactive state
 const loading = ref(false)
 

+ 3 - 1
app/src/views/nginx_log/structured/StructuredLogViewer.vue

@@ -2,7 +2,7 @@
 import type { SorterResult, TablePaginationConfig } from 'ant-design-vue/es/table/interface'
 import type { AccessLogEntry, AdvancedSearchRequest, PreflightResponse } from '@/api/nginx_log'
 import { DownOutlined, ExclamationCircleOutlined, ReloadOutlined } from '@ant-design/icons-vue'
-import { message, Tag } from 'ant-design-vue'
+import { Tag } from 'ant-design-vue'
 import dayjs from 'dayjs'
 import nginx_log from '@/api/nginx_log'
 import { useWebSocketEventBus } from '@/composables/useWebSocketEventBus'
@@ -25,6 +25,8 @@ interface SearchSummary {
 
 const props = defineProps<Props>()
 
+const { message } = App.useApp()
+
 // Route and router
 const route = useRoute()
 

+ 2 - 1
app/src/views/preference/components/AuthSettings/AddPasskey.vue

@@ -1,11 +1,12 @@
 <script setup lang="ts">
 import { startRegistration } from '@simplewebauthn/browser'
-import { message } from 'ant-design-vue'
 import passkey from '@/api/passkey'
 import { useUserStore } from '@/pinia'
 
 const emit = defineEmits(['created'])
 
+const { message } = App.useApp()
+
 const user = useUserStore()
 const passkeyName = ref('')
 const addPasskeyModelOpen = ref(false)

+ 2 - 1
app/src/views/preference/components/AuthSettings/Passkey.vue

@@ -1,7 +1,6 @@
 <script setup lang="ts">
 import type { Passkey } from '@/api/passkey'
 import { DeleteOutlined, EditOutlined, KeyOutlined } from '@ant-design/icons-vue'
-import { message } from 'ant-design-vue'
 import dayjs from 'dayjs'
 import relativeTime from 'dayjs/plugin/relativeTime'
 import passkey from '@/api/passkey'
@@ -12,6 +11,8 @@ import AddPasskey from './AddPasskey.vue'
 
 dayjs.extend(relativeTime)
 
+const { message } = App.useApp()
+
 const user = useUserStore()
 
 const getListLoading = ref(true)

+ 2 - 1
app/src/views/preference/components/AuthSettings/RecoveryCodes.vue

@@ -3,7 +3,6 @@ import type { TwoFAStatus } from '@/api/2fa'
 import type { RecoveryCode } from '@/api/recovery'
 import { CopyOutlined, WarningOutlined } from '@ant-design/icons-vue'
 import { UseClipboard } from '@vueuse/components'
-import { message } from 'ant-design-vue'
 import recovery from '@/api/recovery'
 import { use2FAModal } from '@/components/TwoFA'
 
@@ -16,6 +15,8 @@ const emit = defineEmits<{
   refresh: [void]
 }>()
 
+const { message } = App.useApp()
+
 const _codes = ref<RecoveryCode[]>()
 const codes = computed(() => _codes.value ?? props.recoveryCodes)
 const newGenerated = ref(false)

+ 2 - 1
app/src/views/preference/components/AuthSettings/TOTP.vue

@@ -2,7 +2,6 @@
 import type { RecoveryCode } from '@/api/recovery'
 import { CheckCircleOutlined } from '@ant-design/icons-vue'
 import { UseClipboard } from '@vueuse/components'
-import { message } from 'ant-design-vue'
 import otp from '@/api/otp'
 import OTPInput from '@/components/OTPInput'
 import { use2FAModal } from '@/components/TwoFA'
@@ -15,6 +14,8 @@ const emit = defineEmits<{
   refresh: [void]
 }>()
 
+const { message } = App.useApp()
+
 const recoveryCodes = defineModel<RecoveryCode[]>('recoveryCodes')
 
 const enrolling = ref(false)

+ 2 - 1
app/src/views/preference/components/ExternalNotify/EnabledSwitch.vue

@@ -1,12 +1,13 @@
 <script setup lang="ts">
 import type { ExternalNotify } from '@/api/external_notify'
-import { message } from 'ant-design-vue'
 import externalNotify from '@/api/external_notify'
 
 const props = defineProps<{
   record: ExternalNotify
 }>()
 
+const { message } = App.useApp()
+
 const loading = ref(false)
 const enabled = defineModel<boolean>('enabled')
 

+ 2 - 1
app/src/views/preference/components/ExternalNotify/ExternalNotifyEditor.vue

@@ -2,7 +2,6 @@
 import type { StdTableColumn } from '@uozi-admin/curd'
 import type { ExternalNotifyConfig } from './types'
 import { StdForm } from '@uozi-admin/curd'
-import { message } from 'ant-design-vue'
 import { testMessage } from '@/api/external_notify'
 import gettext from '@/gettext'
 import configMap from './index'
@@ -11,6 +10,8 @@ const props = defineProps<{
   type?: string
 }>()
 
+const { message } = App.useApp()
+
 const modelValue = defineModel<Record<string, string>>({ default: reactive({}) })
 
 const currentConfig = computed<ExternalNotifyConfig | undefined>(() => {

+ 2 - 1
app/src/views/preference/store/index.ts

@@ -1,10 +1,11 @@
 import type { Settings } from '@/api/settings'
-import { message } from 'ant-design-vue'
 import settings from '@/api/settings'
 import { use2FAModal } from '@/components/TwoFA'
 import { useSettingsStore } from '@/pinia'
 
 const useSystemSettingsStore = defineStore('systemSettings', () => {
+  const { message } = App.useApp()
+
   const data = ref<Settings>({
     app: {
       page_size: 10,

+ 2 - 1
app/src/views/preference/tabs/AuthSettings.vue

@@ -2,11 +2,12 @@
 import type { Ref } from 'vue'
 
 import type { BannedIP } from '@/api/settings'
-import { message } from 'ant-design-vue'
 import dayjs from 'dayjs'
 import setting from '@/api/settings'
 import useSystemSettingsStore from '../store'
 
+const { message } = App.useApp()
+
 const systemSettingsStore = useSystemSettingsStore()
 const { data } = storeToRefs(systemSettingsStore)
 

+ 3 - 1
app/src/views/preference/tabs/ExternalNotify.vue

@@ -1,10 +1,12 @@
 <script setup lang="ts">
 import type { ExternalNotify } from '@/api/external_notify'
 import { StdCurd } from '@uozi-admin/curd'
-import { Button, message } from 'ant-design-vue'
+import { Button } from 'ant-design-vue'
 import externalNotify, { testMessage } from '@/api/external_notify'
 import columns from '../components/ExternalNotify/columns'
 
+const { message } = App.useApp()
+
 const loadingStates = ref<Record<number, boolean>>({})
 
 async function handleTestSingleMessage(record: ExternalNotify) {

+ 49 - 1
app/src/views/site/site_edit/components/RightPanel/Chat.vue

@@ -2,6 +2,11 @@
 import LLM from '@/components/LLM'
 import { useSiteEditorStore } from '../SiteEditor/store'
 
+interface Props {
+  chatHeight: string
+}
+
+defineProps<Props>()
 const editorStore = useSiteEditorStore()
 const {
   configText,
@@ -12,8 +17,9 @@ const {
 <template>
   <div class="mt--6">
     <LLM
-      :content="configText"
+      :nginx-config="configText"
       :path="filepath"
+      :height="chatHeight"
     />
   </div>
 </template>
@@ -26,4 +32,46 @@ const {
 :deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
   padding: 0 0 10px 0;
 }
+
+// LLM组件高度覆盖
+:deep(.llm-wrapper) {
+  height: calc(100vh - 260px);
+}
+
+// LLM组件滚动设置 - 与 TerminalRightPanel 保持一致
+:deep(.llm-container) {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  position: relative; // 确保定位上下文
+}
+
+:deep(.session-header) {
+  flex-shrink: 0;
+}
+
+:deep(.message-container) {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0; // 重要:允许 flex 子元素缩小
+  position: relative; // 为绝对定位提供参考点
+}
+
+// 消息列表容器可滚动,为输入框预留空间
+:deep(.message-list-container) {
+  flex: 1;
+  overflow-y: auto;
+  min-height: 0;
+}
+
+// 输入框绝对定位在底部
+:deep(.input-msg) {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  z-index: 10;
+}
 </style>

+ 14 - 2
app/src/views/site/site_edit/components/RightPanel/RightPanel.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { useElementSize } from '@vueuse/core'
 import { PortScannerCompact } from '@/components/PortScanner'
 import { useSiteEditorStore } from '../SiteEditor/store'
 import Basic from './Basic.vue'
@@ -10,6 +11,17 @@ const activeKey = ref('basic')
 const editorStore = useSiteEditorStore()
 const { advanceMode, loading } = storeToRefs(editorStore)
 
+// Get container height for Chat component
+const containerRef = ref<HTMLElement>()
+const { height: containerHeight } = useElementSize(containerRef)
+
+// Calculate chat height
+const chatHeight = computed(() => {
+  const tabsNavHeight = 55
+  const padding = 48
+  return `${containerHeight.value - tabsNavHeight - padding}px`
+})
+
 watch(advanceMode, val => {
   if (val) {
     activeKey.value = 'basic'
@@ -18,7 +30,7 @@ watch(advanceMode, val => {
 </script>
 
 <template>
-  <div class="right-settings-container">
+  <div ref="containerRef" class="right-settings-container">
     <ACard
       class="right-settings"
       :bordered="false"
@@ -39,7 +51,7 @@ watch(advanceMode, val => {
           <ConfigTemplate />
         </ATabPane>
         <ATabPane key="chat" :tab="$gettext('Chat')">
-          <Chat />
+          <Chat :chat-height="chatHeight" />
         </ATabPane>
         <ATabPane key="port-scanner" :tab="$gettext('Port Scanner')">
           <PortScannerCompact />

+ 2 - 1
app/src/views/site/site_edit/components/SiteEditor/SiteEditor.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import { HistoryOutlined } from '@ant-design/icons-vue'
-import { message } from 'ant-design-vue'
 import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
 import ConfigHistory from '@/components/ConfigHistory'
 import FooterToolBar from '@/components/FooterToolbar'
@@ -11,6 +10,8 @@ import Cert from '@/views/site/site_edit/components/Cert'
 import EnableTLS from '@/views/site/site_edit/components/EnableTLS'
 import { useSiteEditorStore } from './store'
 
+const { message } = App.useApp()
+
 const route = useRoute()
 
 const name = computed(() => decodeURIComponent(route.params?.name?.toString() ?? ''))

+ 6 - 0
app/src/views/stream/components/RightPanel/Chat.vue

@@ -2,6 +2,11 @@
 import LLM from '@/components/LLM'
 import { useStreamEditorStore } from '../../store'
 
+interface Props {
+  chatHeight: string
+}
+
+defineProps<Props>()
 const store = useStreamEditorStore()
 const { configText, filepath } = storeToRefs(store)
 </script>
@@ -11,6 +16,7 @@ const { configText, filepath } = storeToRefs(store)
     <LLM
       :content="configText"
       :path="filepath"
+      :height="chatHeight"
     />
   </div>
 </template>

+ 14 - 2
app/src/views/stream/components/RightPanel/RightPanel.vue

@@ -1,13 +1,25 @@
 <script setup lang="ts">
+import { useElementSize } from '@vueuse/core'
 import { PortScannerCompact } from '@/components/PortScanner'
 import Basic from './Basic.vue'
 import Chat from './Chat.vue'
 
 const activeKey = ref('basic')
+
+// Get container height for Chat component
+const containerRef = ref<HTMLElement>()
+const { height: containerHeight } = useElementSize(containerRef)
+
+// Calculate chat height
+const chatHeight = computed(() => {
+  const tabsNavHeight = 55
+  const padding = 48
+  return `${containerHeight.value - tabsNavHeight - padding}px`
+})
 </script>
 
 <template>
-  <div class="right-settings-container">
+  <div ref="containerRef" class="right-settings-container">
     <ACard
       class="right-settings"
       :bordered="false"
@@ -20,7 +32,7 @@ const activeKey = ref('basic')
           <Basic />
         </ATabPane>
         <ATabPane key="chat" :tab="$gettext('Chat')">
-          <Chat />
+          <Chat :chat-height="chatHeight" />
         </ATabPane>
         <ATabPane key="port-scanner" :tab="$gettext('Port Scanner')">
           <PortScannerCompact />

+ 2 - 1
app/src/views/stream/store.ts

@@ -1,7 +1,6 @@
 import type { CertificateInfo } from '@/api/cert'
 import type { Stream } from '@/api/stream'
 import type { CheckedType } from '@/types'
-import { message } from 'ant-design-vue'
 import config from '@/api/config'
 import ngx from '@/api/ngx'
 import stream from '@/api/stream'
@@ -9,6 +8,8 @@ import { useNgxConfigStore } from '@/components/NgxConfigEditor'
 import { ConfigStatus } from '@/constants'
 
 export const useStreamEditorStore = defineStore('streamEditor', () => {
+  const { message } = App.useApp()
+
   const name = ref('')
   const advanceMode = ref(false)
   const parseErrorStatus = ref(false)

+ 104 - 8
app/src/views/terminal/Terminal.vue

@@ -6,6 +6,7 @@ import { Terminal } from '@xterm/xterm'
 import { throttle } from 'lodash'
 import use2FAModal from '@/components/TwoFA/use2FAModal'
 import ws from '@/lib/websocket'
+import TerminalRightPanel from './components/TerminalRightPanel.vue'
 import TerminalStatusBar from './components/TerminalStatusBar.vue'
 import '@xterm/xterm/css/xterm.css'
 
@@ -17,6 +18,11 @@ const websocket = shallowRef<ReconnectingWebSocket | WebSocket>()
 const lostConnection = ref(false)
 const insecureConnection = ref(false)
 const isWebSocketReady = ref(false)
+const isRightPanelVisible = ref(false)
+const rightPanelRef = ref<InstanceType<typeof TerminalRightPanel>>()
+
+// Keep ref for terminal layout
+const terminalLayoutRef = ref<HTMLElement>()
 
 // Check if using HTTP in a non-localhost environment
 function checkSecureConnection() {
@@ -98,6 +104,9 @@ function initTerm() {
       Type: 1,
     }
 
+    // Monitor terminal input for LLM context
+    handleTerminalInput(key)
+
     sendMessage(order)
   })
   term.onBinary(data => {
@@ -136,6 +145,21 @@ onUnmounted(() => {
 function refreshTerminal() {
   window.location.reload()
 }
+
+function toggleRightPanel() {
+  isRightPanelVisible.value = !isRightPanelVisible.value
+}
+
+// Monitor terminal input to provide context to LLM
+function handleTerminalInput(data: string) {
+  if (rightPanelRef.value && data.includes('\r')) {
+    // Extract command when Enter is pressed
+    const command = data.replace('\r', '').trim()
+    if (command) {
+      rightPanelRef.value.updateCurrentCommand(command)
+    }
+  }
+}
 </script>
 
 <template>
@@ -167,30 +191,90 @@ function refreshTerminal() {
         </AButton>
       </template>
     </AAlert>
-    <div class="terminal-container">
-      <div
-        id="terminal"
-        class="console"
+    <div ref="terminalLayoutRef" class="terminal-layout">
+      <div class="terminal-container">
+        <div class="terminal-header">
+          <div class="header-actions">
+            <AButton
+              type="text"
+              size="small"
+              @click="toggleRightPanel"
+            >
+              {{ isRightPanelVisible ? $gettext('Hide Assistant') : $gettext('Show Assistant') }}
+            </AButton>
+          </div>
+        </div>
+        <div
+          id="terminal"
+          class="console"
+        />
+        <TerminalStatusBar />
+      </div>
+
+      <TerminalRightPanel
+        ref="rightPanelRef"
+        :is-visible="isRightPanelVisible"
       />
-      <TerminalStatusBar />
     </div>
   </div>
 </template>
 
 <style lang="less" scoped>
-.terminal-container {
+.terminal-layout {
   display: flex;
-  flex-direction: column;
-  min-height: calc(100vh - 200px);
+  min-height: max(585px, calc(100vh - 200px));
   border-radius: 5px;
   overflow: hidden;
   background: #000;
+  position: relative;
+  width: 100%;
 
   @media (max-width: 512px) {
     border-radius: 0;
   }
 }
 
+.terminal-container {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  min-width: 0;
+  transition: all 0.3s ease;
+  background: #000;
+}
+
+.terminal-header {
+  background: #1a1a1a;
+  border-bottom: 1px solid #333;
+  padding: 8px 12px;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  min-height: 40px;
+
+  .header-actions {
+    display: flex;
+    gap: 8px;
+    align-items: center;
+
+    .icon {
+      font-size: 16px;
+    }
+  }
+
+  :deep(.ant-btn) {
+    color: #e0e0e0;
+    border: 1px solid #444;
+    background: transparent;
+
+    &:hover {
+      color: #4a9eff;
+      border-color: #4a9eff;
+      background: rgba(74, 158, 255, 0.1);
+    }
+  }
+}
+
 .console {
   flex: 1;
 
@@ -203,4 +287,16 @@ function refreshTerminal() {
     border-radius: 0;
   }
 }
+
+@media (max-width: 768px) {
+  .terminal-header {
+    padding: 6px 8px;
+    min-height: 36px;
+
+    :deep(.ant-btn) {
+      font-size: 12px;
+      padding: 4px 8px;
+    }
+  }
+}
 </style>

+ 149 - 0
app/src/views/terminal/components/TerminalRightPanel.vue

@@ -0,0 +1,149 @@
+<script setup lang="ts">
+import type { AnalyticInit } from '@/api/analytic'
+import analytic from '@/api/analytic'
+import LLM from '@/components/LLM/LLM.vue'
+
+interface Props {
+  isVisible: boolean
+}
+
+defineProps<Props>()
+
+// Get current terminal command context
+const currentCommand = ref('')
+const systemInfo = ref<AnalyticInit | null>(null)
+
+// No longer need to calculate height since we have CSS min-height
+
+// Fetch system information
+async function fetchSystemInfo() {
+  try {
+    systemInfo.value = await analytic.init()
+  }
+  catch (error) {
+    console.error('Failed to fetch system info:', error)
+  }
+}
+
+// Build terminal context with system information
+const terminalContext = computed(() => {
+  let context = ''
+
+  if (systemInfo.value?.host?.platformVersion) {
+    context += `System: ${systemInfo.value.host.platformVersion}\n\n`
+  }
+
+  if (currentCommand.value) {
+    context += `Current terminal command: ${currentCommand.value}\n\n`
+    context += 'Please help me with this command or terminal operation.'
+  }
+  else {
+    context += 'I need assistance with terminal operations and commands.'
+  }
+
+  return context
+})
+
+// Initialize system info when component mounts
+onMounted(() => {
+  fetchSystemInfo()
+})
+
+// Function to extract current terminal command (could be enhanced later)
+function updateCurrentCommand(command: string) {
+  currentCommand.value = command
+}
+
+defineExpose({
+  updateCurrentCommand,
+})
+</script>
+
+<template>
+  <div
+    v-if="isVisible"
+    class="terminal-right-panel dark"
+  >
+    <div v-if="isVisible" class="panel-content">
+      <LLM
+        :content="terminalContext"
+        type="terminal"
+        theme="dark"
+      />
+    </div>
+  </div>
+</template>
+
+<style lang="less" scoped>
+.terminal-right-panel {
+  width: 400px;
+  min-height: calc(100vh - 200px);
+  border-left: 1px solid #333;
+  display: flex;
+  flex-direction: column;
+  flex-shrink: 0;
+
+  @media (max-width: 1200px) {
+    width: 350px;
+  }
+
+  @media (max-width: 992px) {
+    width: 300px;
+  }
+
+  @media (max-width: 768px) {
+    position: fixed;
+    right: 0;
+    top: 0;
+    width: 100%;
+    height: 100vh;
+    z-index: 1000;
+    border-left: none;
+  }
+}
+
+.panel-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+
+  :deep(.llm-container) {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    position: relative; // 确保定位上下文
+  }
+
+  :deep(.session-header) {
+    flex-shrink: 0;
+  }
+
+  :deep(.message-container) {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    min-height: 0; // 重要:允许 flex 子元素缩小
+    position: relative; // 为绝对定位提供参考点
+  }
+
+  // 消息列表容器可滚动,为输入框预留空间
+  :deep(.message-list-container) {
+    flex: 1;
+    overflow-y: auto;
+    min-height: 0;
+  }
+
+  // 输入框绝对定位在底部
+  :deep(.input-msg) {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    background: rgba(30, 30, 30, 0.95);
+    backdrop-filter: blur(10px);
+    z-index: 10;
+  }
+}
+</style>

+ 5 - 0
app/vite.config.ts

@@ -58,6 +58,11 @@ export default defineConfig(({ mode }) => {
           {
             '@/language': ['T'],
           },
+          {
+            'ant-design-vue': [
+              'App',
+            ],
+          },
         ],
         vueTemplate: true,
         eslintrc: {

+ 23 - 0
internal/llm/prompts.go

@@ -0,0 +1,23 @@
+package llm
+
+const NginxConfigPrompt = `You are a assistant who can help users write and optimise the configurations of Nginx,
+the first user message contains the content of the configuration file which is currently opened by the user and
+the current language code(CLC). You suppose to use the language corresponding to the CLC to give the first reply.
+Later the language environment depends on the user message.
+The first reply should involve the key information of the file and ask user what can you help them.`
+
+const TerminalAssistantPrompt = `You are a terminal assistant for Linux/Unix systems. You help users with:
+
+1. Command line operations and troubleshooting
+2. System administration tasks  
+3. Shell scripting and automation
+4. File system operations and permissions
+5. Process management and system monitoring
+6. Network configuration and debugging
+7. Package management (apt, yum, dnf, etc.)
+8. Service management (systemctl, systemd)
+
+The user message may contain system information and current terminal context. 
+Provide helpful, accurate commands and explanations specific to their system.
+Always prioritize safety and explain potentially dangerous operations.
+Use the user's preferred language for communication.`

Некоторые файлы не были показаны из-за большого количества измененных файлов