#include #include #include #include #include #include #include Global $oErrorHandler = ObjEvent("AutoIt.Error", "COM_ErrHandler") Global $sHistoryDir = "D:\gemiox" Global $sCurrentChatFile = "" Global $bIsDarkMode = False Global $aChatHistory[0][3] Global $iEditIndexRequest = -1 Global $sConfigFile = StringRegExpReplace(@ScriptFullPath, "(?i)\.[^.]*$", ".txt") If Not FileExists($sHistoryDir) Then DirCreate($sHistoryDir) ; =================== TẠO GIAO DIỆN =================== Local $hGUI = GUICreate("Ollama Client Pro - Giao diện Chat UI", 950, 690) ; [Cột trái] Local $idLblHist = GUICtrlCreateLabel("Lịch sử chat (D:\gemiox):", 12, 12, 180, 18) Local $idListHist = GUICtrlCreateList("", 12, 35, 200, 595, BitOR($LBS_NOTIFY, $WS_VSCROLL, $WS_BORDER)) GUICtrlSetFont($idListHist, 10, 400, 0, "Segoe UI") Local $idBtnNewChat = GUICtrlCreateButton("Chat Mới (+)", 12, 642, 95, 30) Local $idBtnReloadHist = GUICtrlCreateButton("Làm Mới", 115, 642, 95, 30) ; [Cột phải - Hàng 1] Local $idLblModel = GUICtrlCreateLabel("Model:", 230, 15, 40, 20) Local $idComboModel = GUICtrlCreateCombo("", 275, 12, 200, 25, $CBS_DROPDOWNLIST) Local $idBtnRefresh = GUICtrlCreateButton("Quét Model", 485, 11, 80, 25) Local $idLblCtx = GUICtrlCreateLabel("Context:", 580, 15, 50, 20) Local $idComboCtx = GUICtrlCreateCombo("4096", 635, 12, 85, 25) GUICtrlSetData($idComboCtx, "2048|8192|16384|32768") Local $idBtnTheme = GUICtrlCreateButton("Chế độ Tối", 830, 11, 95, 25) ; [Cột phải - Hàng 2] Local $idChkNoThink = GUICtrlCreateCheckbox("Tắt Thinking ()", 230, 45, 150, 20) Local $idChkWebSearch = GUICtrlCreateCheckbox("Tìm kiếm Online", 390, 45, 130, 20) ; [Cột phải - Hàng 3] KHUNG HIỂN THỊ CHAT (Web Browser) Global $oIE = ObjCreate("Shell.Explorer.2") Local $idBrowser = GUICtrlCreateObj($oIE, 230, 75, 700, 450) ; [Cột phải - Hàng 4] KHUNG NHẬP PROMPT Local $idLblPrompt = GUICtrlCreateLabel("Prompt (Shift+Enter để xuống dòng, Enter để gửi):", 230, 532, 400, 18) Global $idPrompt = GUICtrlCreateEdit("", 230, 555, 700, 80, BitOR($ES_AUTOVSCROLL, $WS_VSCROLL, $ES_MULTILINE)) GUICtrlSetFont($idPrompt, 11, 400, 0, "Segoe UI") ; [Cột phải - Hàng 5] Global $idBtnSend = GUICtrlCreateButton("Gửi Prompt", 230, 642, 95, 30) Global $idLblStatus = GUICtrlCreateLabel("Trạng thái: Sẵn sàng.", 335, 648, 545, 20) ; --- THIẾT LẬP PHÍM TẮT --- Local $idDummyEnter = GUICtrlCreateDummy() Local $idDummyShiftEnter = GUICtrlCreateDummy() Local $aAccelKeys[2][2] = [["{ENTER}", $idDummyEnter], ["+{ENTER}", $idDummyShiftEnter]] GUISetAccelerators($aAccelKeys) ; Khởi tạo Web HTML $oIE.navigate("about:blank") While $oIE.readyState <> 4 Sleep(10) WEnd InitHTMLContainer() Global $oIEEvents = ObjEvent($oIE, "IEEvent_") ; Gom nhóm controls đổi Theme Global $aLabels[] = [$idLblHist, $idLblModel, $idLblCtx, $idLblPrompt, $idLblStatus] Global $aChecks[] = [$idChkNoThink, $idChkWebSearch] Global $aEdits[] = [$idPrompt, $idListHist] Local $sSavedModel = LoadSettings($hGUI, $idBtnTheme, $idComboCtx, $idChkNoThink, $idChkWebSearch) GUISetState(@SW_SHOW, $hGUI) ScanOllamaModels($idComboModel, $idLblStatus, $sSavedModel) RefreshHistoryList($idListHist) ; =================== VÒNG LẶP SỰ KIỆN =================== While 1 If $iEditIndexRequest <> -1 Then EditPreviousPrompt($iEditIndexRequest) $iEditIndexRequest = -1 EndIf Switch GUIGetMsg() Case $GUI_EVENT_CLOSE SaveSettings($idComboModel, $idComboCtx, $idChkNoThink, $idChkWebSearch) ExitLoop Case $idDummyShiftEnter Local $hFocus = ControlGetHandle($hGUI, "", ControlGetFocus($hGUI)) If $hFocus = GUICtrlGetHandle($idPrompt) Then _GUICtrlEdit_ReplaceSel($idPrompt, @CRLF) EndIf Case $idDummyEnter Local $hFocus = ControlGetHandle($hGUI, "", ControlGetFocus($hGUI)) If $hFocus = GUICtrlGetHandle($idPrompt) Then ProcessSendPrompt() EndIf Case $idBtnTheme $bIsDarkMode = Not $bIsDarkMode ApplyTheme($hGUI, $idBtnTheme, $bIsDarkMode) SaveSettings($idComboModel, $idComboCtx, $idChkNoThink, $idChkWebSearch) Case $idBtnRefresh ScanOllamaModels($idComboModel, $idLblStatus, GUICtrlRead($idComboModel)) Case $idBtnReloadHist RefreshHistoryList($idListHist) Case $idBtnNewChat $sCurrentChatFile = "" Dim $aChatHistory[0][3] GUICtrlSetData($idPrompt, "") RenderHTML() GUICtrlSetData($idLblStatus, "Trạng thái: Đã bắt đầu phiên chat mới.") Case $idListHist Local $sSelectedFile = GUICtrlRead($idListHist) If $sSelectedFile <> "" Then Local $sFullPath = $sHistoryDir & "\" & $sSelectedFile If FileExists($sFullPath) Then $sCurrentChatFile = $sFullPath LoadChatFromFile($sFullPath) GUICtrlSetData($idLblStatus, "Đang mở: " & $sSelectedFile) EndIf EndIf Case $idBtnSend ProcessSendPrompt() EndSwitch WEnd ; =================== HÀM GỬI PROMPT CHÍNH =================== Func ProcessSendPrompt() Local $sModel = GUICtrlRead($idComboModel) Local $sPrompt = StringStripWS(GUICtrlRead($idPrompt), 3) Local $iNumCtx = Int(GUICtrlRead($idComboCtx)) If $sModel = "" Or $sPrompt = "" Then Return GUICtrlSetState($idBtnSend, $GUI_DISABLE) AddChatEntry("user", $sPrompt, @HOUR & ":" & @MIN & ":" & @SEC) RenderHTML() GUICtrlSetData($idPrompt, "") Local $sFinalPrompt = $sPrompt If BitAND(GUICtrlRead($idChkWebSearch), $GUI_CHECKED) = $GUI_CHECKED Then GUICtrlSetData($idLblStatus, "Trạng thái: Đang tìm kiếm online...") Local $sWebData = PerformWebSearch($sPrompt) If $sWebData <> "" Then $sFinalPrompt = "[THÔNG TIN TỪ INTERNET]:" & @CRLF & $sWebData & @CRLF & "Yêu cầu: " & $sPrompt EndIf EndIf GUICtrlSetData($idLblStatus, "Trạng thái: Đang chờ [" & $sModel & "] trả lời...") Local $hTimer = TimerInit() Local $sResponse = QueryOllama($sModel, $sFinalPrompt, $iNumCtx) Local $fTimeSec = TimerDiff($hTimer) / 1000 Local $sTimeFormatted = StringFormat("%.1f", $fTimeSec) & "s" If BitAND(GUICtrlRead($idChkNoThink), $GUI_CHECKED) = $GUI_CHECKED Then $sResponse = StringRegExpReplace($sResponse, '(?s).*?', '') $sResponse = StringStripWS($sResponse, 3) EndIf AddChatEntry("ai", $sResponse, $sModel & " ⏱ " & $sTimeFormatted) RenderHTML() If $sCurrentChatFile = "" Then $sCurrentChatFile = $sHistoryDir & "\" & @YEAR & @MON & @MDAY & "_" & @HOUR & @MIN & @SEC & "_" & RemoveVietnameseAccents($sPrompt) & ".txt" EndIf SaveCurrentChatToFile() GUICtrlSetData($idLblStatus, "Phản hồi trong " & $sTimeFormatted & ". Đã lưu file lịch sử.") GUICtrlSetState($idBtnSend, $GUI_ENABLE) RefreshHistoryList($idListHist) EndFunc ; =================== BẮT SỰ KIỆN GIAO DIỆN WEB =================== ; 1. Nhận lệnh Edit thông qua sự kiện đổi Title của Javascript (Cực kỳ ổn định) Func IEEvent_TitleChange($sText) If StringLeft($sText, 5) = "EDIT_" Then $iEditIndexRequest = Int(StringTrimLeft($sText, 5)) EndIf EndFunc ; 2. Bắt link thực tế (Click mở bằng trình duyệt ngoài nếu AI nhả link http) Func IEEvent_BeforeNavigate2($pDisp, ByRef $sURL, ByRef $iFlags, ByRef $sTargetFrameName, ByRef $vPostData, ByRef $sHeaders, ByRef $bCancel) If StringLeft($sURL, 4) = "http" Then $bCancel = True ShellExecute($sURL) EndIf EndFunc ; ================================================================= Func EditPreviousPrompt($iIndex) If $iIndex < 0 Or $iIndex >= UBound($aChatHistory) Then Return GUICtrlSetData($idPrompt, $aChatHistory[$iIndex][1]) ReDim $aChatHistory[$iIndex][3] RenderHTML() SaveCurrentChatToFile() GUICtrlSetData($idLblStatus, "Trạng thái: Đang sửa câu hỏi...") EndFunc Func AddChatEntry($sRole, $sText, $sMeta) Local $iIdx = UBound($aChatHistory) ReDim $aChatHistory[$iIdx + 1][3] $aChatHistory[$iIdx][0] = $sRole $aChatHistory[$iIdx][1] = $sText $aChatHistory[$iIdx][2] = $sMeta EndFunc Func InitHTMLContainer() Local $sBase = "
" $oIE.document.write($sBase) $oIE.document.close() EndFunc Func RenderHTML() Local $sHTML = "" For $i = 0 To UBound($aChatHistory) - 1 Local $sEscapedText = EscapeHTML($aChatHistory[$i][1]) If $aChatHistory[$i][0] = "user" Then $sHTML &= "
" & $sEscapedText ; Sửa lỗi URL hoàn toàn: Dùng thẻ SPAN thay vì thẻ A, đổi document.title và reset lại title sau 100ms Local $sOnClick = "document.title='EDIT_" & $i & "'; setTimeout(function(){document.title='Chat';}, 100);" $sHTML &= "
✏️ Sửa câu hỏi
" Else $sHTML &= "
" & $sEscapedText & "
" If $aChatHistory[$i][2] <> "" Then $sHTML &= "
" & $aChatHistory[$i][2] & "
" EndIf $sHTML &= "
" EndIf Next $oIE.document.getElementById("content").innerHTML = $sHTML $oIE.document.body.className = $bIsDarkMode ? "dark" : "light" $oIE.document.parentWindow.scrollTo(0, 99999) EndFunc Func EscapeHTML($sString) $sString = StringReplace($sString, "&", "&") $sString = StringReplace($sString, "<", "<") $sString = StringReplace($sString, ">", ">") $sString = StringReplace($sString, '"', """) Return $sString EndFunc ; =================== XỬ LÝ API VÀ FILE =================== Func SaveCurrentChatToFile() If $sCurrentChatFile = "" Then Return Local $sData = "" For $i = 0 To UBound($aChatHistory) - 1 If $aChatHistory[$i][0] = "user" Then $sData &= "=== [USER - " & $aChatHistory[$i][2] & "] ===" & @CRLF & $aChatHistory[$i][1] & @CRLF & @CRLF Else $sData &= "=== [" & $aChatHistory[$i][2] & "] ===" & @CRLF & $aChatHistory[$i][1] & @CRLF & @CRLF & "----------------------------------------" & @CRLF & @CRLF EndIf Next Local $hFile = FileOpen($sCurrentChatFile, $FO_OVERWRITE + $FO_UTF8_NOBOM) FileWrite($hFile, $sData) FileClose($hFile) EndFunc Func LoadChatFromFile($sPath) Dim $aChatHistory[0][3] Local $sContent = FileRead(FileOpen($sPath, $FO_READ + $FO_UTF8_NOBOM)) Local $aBlocks = StringSplit($sContent, "----------------------------------------", 1) For $i = 1 To $aBlocks[0] Local $sBlock = StringStripWS($aBlocks[$i], 3) If $sBlock = "" Then ContinueLoop Local $aUser = StringRegExp($sBlock, '(?s)=== \[USER - (.*?)\] ===\r?\n(.*?)(?=\r?\n=== \[|$)', 1) Local $aAI = StringRegExp($sBlock, '(?s)=== \[(?!USER)(.*?)\] ===\r?\n(.*)', 1) If IsArray($aUser) Then AddChatEntry("user", StringStripWS($aUser[1], 3), $aUser[0]) If IsArray($aAI) Then AddChatEntry("ai", StringStripWS($aAI[1], 3), $aAI[0]) Next RenderHTML() EndFunc Func QueryOllama($sModel, $sPrompt, $iNumCtx) Local $oHTTP = ObjCreate("WinHttp.WinHttpRequest.5.1") If Not IsObj($oHTTP) Then Return "Lỗi: Không khởi tạo được WinHttp." $sPrompt = StringReplace(StringReplace(StringReplace(StringReplace($sPrompt, '\', '\\'), '"', '\"'), @CRLF, '\n'), @CR, '\n') Local $sPayload = '{"model": "' & $sModel & '", "prompt": "' & $sPrompt & '", "stream": false, "options": {"num_ctx": ' & $iNumCtx & '}}' $oHTTP.Open("POST", "http://localhost:11434/api/generate", True) $oHTTP.SetRequestHeader("Content-Type", "application/json; charset=utf-8") $oHTTP.SetTimeouts(5000, 5000, 30000, 240000) $oHTTP.Send(StringToBinary($sPayload, 4)) Local $iTick = TimerInit(), $iDots = 0 While 1 If $oHTTP.WaitForResponse(0.05) Then ExitLoop If TimerDiff($iTick) > 500 Then $iTick = TimerInit() $iDots = Mod($iDots + 1, 4) GUICtrlSetData($idLblStatus, "Trạng thái: Đang chờ [" & $sModel & "] trả lời" & StringLeft("...", $iDots)) EndIf If GUIGetMsg() = $GUI_EVENT_CLOSE Then Exit WEnd If $oHTTP.Status <> 200 Then Return "Lỗi HTTP " & $oHTTP.Status Local $aMatch = StringRegExp(BinaryToString($oHTTP.ResponseBody, 4), '(?s)"response"\s*:\s*"((?:[^"\\]|\\.)*)"', 1) If @error Then Return "Lỗi JSON." Return StringReplace(StringReplace(StringReplace($aMatch[0], '\n', @CRLF), '\"', '"'), '\\', '\') EndFunc Func PerformWebSearch($sQuery) Local $oHTTP = ObjCreate("WinHttp.WinHttpRequest.5.1") $oHTTP.Open("GET", "https://html.duckduckgo.com/html/?q=" & $sQuery, True) $oHTTP.SetRequestHeader("User-Agent", "Mozilla/5.0") $oHTTP.Send() While 1 If $oHTTP.WaitForResponse(0.05) Then ExitLoop If GUIGetMsg() = $GUI_EVENT_CLOSE Then Exit WEnd Local $aSnippets = StringRegExp(BinaryToString($oHTTP.ResponseBody, 4), '(?s) 3) ? 2 : UBound($aSnippets) - 1) $sResult &= "- " & StringStripWS(StringRegExpReplace($aSnippets[$i], '<[^>]+>', ''), 3) & @CRLF Next Return StringReplace(StringReplace($sResult, '"', '"'), ''', "'") EndFunc Func SaveSettings($iCtrlModel, $iCtrlCtx, $iCtrlNoThink, $iCtrlWebSearch) IniWrite($sConfigFile, "Settings", "Model", GUICtrlRead($iCtrlModel)) IniWrite($sConfigFile, "Settings", "Context", GUICtrlRead($iCtrlCtx)) IniWrite($sConfigFile, "Settings", "DarkMode", $bIsDarkMode ? "1" : "0") IniWrite($sConfigFile, "Settings", "NoThink", (BitAND(GUICtrlRead($iCtrlNoThink), $GUI_CHECKED) = $GUI_CHECKED) ? "1" : "0") IniWrite($sConfigFile, "Settings", "WebSearch", (BitAND(GUICtrlRead($iCtrlWebSearch), $GUI_CHECKED) = $GUI_CHECKED) ? "1" : "0") EndFunc Func LoadSettings($hWindow, $iBtnTheme, $iCtrlCtx, $iCtrlNoThink, $iCtrlWebSearch) If Not FileExists($sConfigFile) Then Return "" GUICtrlSetData($iCtrlCtx, IniRead($sConfigFile, "Settings", "Context", "4096")) GUICtrlSetState($iCtrlNoThink, IniRead($sConfigFile, "Settings", "NoThink", "0") = "1" ? $GUI_CHECKED : $GUI_UNCHECKED) GUICtrlSetState($iCtrlWebSearch, IniRead($sConfigFile, "Settings", "WebSearch", "0") = "1" ? $GUI_CHECKED : $GUI_UNCHECKED) $bIsDarkMode = (IniRead($sConfigFile, "Settings", "DarkMode", "0") = "1") ApplyTheme($hWindow, $iBtnTheme, $bIsDarkMode) Return IniRead($sConfigFile, "Settings", "Model", "") EndFunc Func RemoveVietnameseAccents($sText) $sText = StringRegExpReplace(StringLower($sText), "(?i)(à|á|ạ|ả|ã|â|ầ|ấ|ậ|ẩ|ẫ|ă|ằ|ắ|ặ|ẳ|ẵ)", "a") $sText = StringRegExpReplace($sText, "(?i)(è|é|ẹ|ẻ|ẽ|ê|ề|ế|ệ|ể|ễ)", "e") $sText = StringRegExpReplace($sText, "(?i)(ì|í|ị|ỉ|ĩ)", "i") $sText = StringRegExpReplace($sText, "(?i)(ò|ó|ọ|ỏ|õ|ô|ồ|ố|ộ|ổ|ỗ|ơ|ờ|ớ|ợ|ở|ỡ)", "o") $sText = StringRegExpReplace($sText, "(?i)(ù|ú|ụ|ủ|ũ|ư|ừ|ứ|ự|ử|ữ)", "u") $sText = StringRegExpReplace($sText, "(?i)(ỳ|ý|ỵ|ỷ|ỹ)", "y") $sText = StringRegExpReplace($sText, "(?i)đ", "d") Return StringLeft(StringRegExpReplace($sText, "[^a-z0-9]+", "_"), 35) EndFunc Func ScanOllamaModels($iCtrlID, $iStatusCtrl, $sPreferredModel = "") Local $oHTTP = ObjCreate("WinHttp.WinHttpRequest.5.1") If Not IsObj($oHTTP) Then Return False $oHTTP.Open("GET", "http://localhost:11434/api/tags", True) $oHTTP.Send() While 1 If $oHTTP.WaitForResponse(0.05) Then ExitLoop If GUIGetMsg() = $GUI_EVENT_CLOSE Then Exit WEnd Local $aNames = StringRegExp(BinaryToString($oHTTP.ResponseBody, 4), '"name"\s*:\s*"([^"]+)"', 3) If @error Then Return False Local $sComboData = "", $sDefaultSelect = $aNames[0] For $i = 0 To UBound($aNames) - 1 $sComboData &= "|" & $aNames[$i] If $sPreferredModel = $aNames[$i] Then $sDefaultSelect = $sPreferredModel Next GUICtrlSetData($iCtrlID, $sComboData, $sDefaultSelect) EndFunc Func RefreshHistoryList($iListCtrl) Local $hSearch = FileFindFirstFile($sHistoryDir & "\*.txt"), $sListStr = "|" If $hSearch <> -1 Then While 1 Local $sFile = FileFindNextFile($hSearch) If @error Then ExitLoop $sListStr &= $sFile & "|" WEnd FileClose($hSearch) EndIf GUICtrlSetData($iListCtrl, $sListStr) EndFunc Func ApplyTheme($hWindow, $idToggleBtn, $bDark) Local $iGuiBg = $bDark ? 0x1E1E1E : 0xF0F0F0 Local $iEditBg = $bDark ? 0x252526 : 0xFFFFFF Local $iEditText = $bDark ? 0xD4D4D4 : 0x000000 Local $iLabelText = $bDark ? 0xCCCCCC : 0x000000 GUISetBkColor($iGuiBg, $hWindow) GUICtrlSetData($idToggleBtn, $bDark ? "Chế độ Sáng" : "Chế độ Tối") For $i = 0 To UBound($aLabels) - 1 GUICtrlSetColor($aLabels[$i], $iLabelText) GUICtrlSetBkColor($aLabels[$i], -2) Next For $i = 0 To UBound($aChecks) - 1 GUICtrlSetColor($aChecks[$i], $iLabelText) GUICtrlSetBkColor($aChecks[$i], -2) Next For $i = 0 To UBound($aEdits) - 1 GUICtrlSetColor($aEdits[$i], $iEditText) GUICtrlSetBkColor($aEdits[$i], $iEditBg) Next RenderHTML() EndFunc Func COM_ErrHandler() EndFunc