chat.prg
function main()
local cHtml := ""
cHtml += "<!DOCTYPE html>"
cHtml += "<html lang='es'>"
cHtml += "<head>"
cHtml += " <meta charset='UTF-8'>"
cHtml += " <meta name='viewport' content='width=device-width, initial-scale=1.0'>"
cHtml += " <title>AIOS 1.0</title>"
cHtml += " <link href='https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&family=JetBrains+Mono&display=swap' rel='stylesheet'>"
cHtml += " <style>"
cHtml += " :root {"
cHtml += " --bg: #0a0b10;"
cHtml += " --glass: rgba(255, 255, 255, 0.05);"
cHtml += " --glass-border: rgba(255, 255, 255, 0.1);"
cHtml += " --accent: linear-gradient(135deg, #6366f1, #a855f7);"
cHtml += " --text: #e2e8f0;"
cHtml += " --bot-bubble: rgba(30, 41, 59, 0.7);"
cHtml += " --user-bubble: linear-gradient(135deg, #4f46e5, #7c3aed);"
cHtml += " }"
cHtml += " * { margin:0; padding:0; box-sizing: border-box; }"
cHtml += " body {"
cHtml += " font-family: 'Outfit', sans-serif;"
cHtml += " background: var(--bg);"
cHtml += " background-image: radial-gradient(circle at 20% 20%, rgba(99, 102, 241, 0.15) 0%, transparent 40%),"
cHtml += " radial-gradient(circle at 80% 80%, rgba(168, 85, 247, 0.15) 0%, transparent 40%);"
cHtml += " color: var(--text);"
cHtml += " height: 100vh;"
cHtml += " display: flex;"
cHtml += " flex-direction: column;"
cHtml += " overflow: hidden;"
cHtml += " }"
cHtml += " header {"
cHtml += " padding: 1.5rem 2rem;"
cHtml += " background: var(--glass);"
cHtml += " backdrop-filter: blur(10px);"
cHtml += " border-bottom: 1px solid var(--glass-border);"
cHtml += " display: flex;"
cHtml += " justify-content: space-between;"
cHtml += " align-items: center;"
cHtml += " z-index: 10;"
cHtml += " }"
cHtml += " header h1 { font-size: 1.5rem; font-weight: 600; letter-spacing: -0.5px; }"
cHtml += " .version-badge { background: var(--accent); padding: 0.2rem 0.6rem; border-radius: 20px; font-size: 0.75rem; font-weight: bold; }"
cHtml += " #chat-container {"
cHtml += " flex: 1;"
cHtml += " overflow-y: auto;"
cHtml += " padding: 2rem;"
cHtml += " display: flex;"
cHtml += " flex-direction: column;"
cHtml += " gap: 1.5rem;"
cHtml += " }"
cHtml += " .message {"
cHtml += " max-width: 85%;"
cHtml += " padding: 1rem 1.25rem;"
cHtml += " border-radius: 1.2rem;"
cHtml += " line-height: 1.6;"
cHtml += " animation: fadeIn 0.3s ease-out;"
cHtml += " }"
cHtml += " @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }"
cHtml += " .user { align-self: flex-end; background: var(--user-bubble); color: white; border-bottom-right-radius: 0.2rem; }"
cHtml += " .bot { align-self: flex-start; background: var(--bot-bubble); backdrop-filter: blur(5px); border: 1px solid var(--glass-border); border-bottom-left-radius: 0.2rem; }"
cHtml += " .error { align-self: center; background: rgba(255,0,0,0.2); color: #ff8888; border: 1px solid #ff0000; padding: 0.5rem 1rem; border-radius: 10px; }"
cHtml += " pre { background: #1e1e1e; padding: 1rem; border-radius: 8px; overflow-x: auto; font-family: 'JetBrains Mono', monospace; }"
cHtml += " .input-wrapper { padding: 2rem; background: linear-gradient(to top, var(--bg), transparent); }"
cHtml += " .input-box {"
cHtml += " max-width: 900px; margin: 0 auto; background: var(--glass); backdrop-filter: blur(20px); border: 1px solid var(--glass-border);"
cHtml += " border-radius: 1.5rem; padding: 0.5rem; display: flex; align-items: center; gap: 0.5rem;"
cHtml += " }"
cHtml += " input { flex: 1; background: transparent; border: none; color: white; padding: 0.75rem 1rem; font-size: 1rem; outline: none; }"
cHtml += " button { background: var(--accent); border: none; color: white; padding: 0.75rem 1.5rem; border-radius: 1.1rem; font-weight: 600; cursor: pointer; }"
cHtml += " button:disabled { background: #444; }"
cHtml += " .typing { display: flex; gap: 4px; padding: 10px; }"
cHtml += " .dot { width: 8px; height: 8px; background: #6366f1; border-radius: 50%; animation: bounce 1.4s infinite ease-in-out; }"
cHtml += " @keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } }"
cHtml += " </style>"
cHtml += "</head>"
cHtml += "<body>"
cHtml += "<header>"
cHtml += " <h1>AIOS</h1>"
cHtml += " <button onclick='clearChat()'>Limpiar</button>"
cHtml += "</header>"
cHtml += "<div id='chat-container'></div>"
cHtml += "<div class='input-wrapper'>"
cHtml += " <div class='input-box'>"
cHtml += " <input type='text' id='input' placeholder='Pregunta algo...' onkeydown='if(event.keyCode===13) sendMessage()'>"
cHtml += " <button id='send-btn' onclick='sendMessage()'>Enviar</button>"
cHtml += " </div>"
cHtml += "</div>"
cHtml += "<script>"
cHtml += " window.onerror = function(m, u, l) { alert('Error: ' + m + ' en ' + l); };"
cHtml += " var history_data = [];"
cHtml += " function addMessage(role, text) {"
cHtml += " var container = document.getElementById('chat-container');"
cHtml += " var div = document.createElement('div');"
cHtml += " div.className = 'message ' + role;"
cHtml += " div.innerHTML = text.split('\\n').join('<br>');"
cHtml += " container.appendChild(div);"
cHtml += " setTimeout(function() { container.scrollTop = container.scrollHeight; }, 50);"
cHtml += " }"
cHtml += " function addTyping() {"
cHtml += " var container = document.getElementById('chat-container');"
cHtml += " var div = document.createElement('div');"
cHtml += " div.id = 'typing-indicator';"
cHtml += " div.className = 'message bot typing';"
cHtml += " div.innerHTML = 'ÔÜÖ´©Å Pensando...';"
cHtml += " container.appendChild(div);"
cHtml += " container.scrollTop = container.scrollHeight;"
cHtml += " }"
cHtml += " function removeTyping() {"
cHtml += " var indicator = document.getElementById('typing-indicator');"
cHtml += " if(indicator) indicator.parentNode.removeChild(indicator);"
cHtml += " }"
cHtml += " function sendMessage() {"
cHtml += " var input = document.getElementById('input');"
cHtml += " var text = input.value.trim();"
cHtml += " var btn = document.getElementById('send-btn');"
cHtml += " if (!text) return;"
cHtml += " addMessage('user', text);"
cHtml += " input.value = '';"
cHtml += " addTyping();"
cHtml += " btn.disabled = true;"
cHtml += " var params = 'query=' + encodeURIComponent(text);"
cHtml += " if (history_data.length > 0) params += '&history=' + encodeURIComponent(JSON.stringify(history_data));"
cHtml += " console.log('SENDING:', params);"
cHtml += " var xhr = new XMLHttpRequest();"
cHtml += " xhr.open('POST', 'aios.prg', true);"
cHtml += " xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');"
cHtml += " xhr.onreadystatechange = function() {"
cHtml += " if (xhr.status !== 200 && xhr.status !== 0) { addMessage('error', 'Status: ' + xhr.status); }"
cHtml += " if (xhr.readyState == 4) {"
cHtml += " removeTyping(); btn.disabled = false;"
cHtml += " if (xhr.status == 200) {"
cHtml += " try {"
cHtml += " var data = JSON.parse(xhr.responseText);"
cHtml += " if (data.success) {"
cHtml += " addMessage('bot', data.text);"
cHtml += " history_data.push({role:'user', parts:[{text:text}]});"
cHtml += " history_data.push({role:'model', parts:[{text:data.text}]});"
cHtml += " } else { addMessage('error', 'Error: ' + data.error); }"
cHtml += " } catch(e) { addMessage('error', 'JSON Error: ' + xhr.responseText.substring(0,100)); }"
cHtml += " } else if(xhr.status !== 0) { addMessage('error', 'Server Error: ' + xhr.status); }"
cHtml += " }"
cHtml += " };"
cHtml += " xhr.send(params);"
cHtml += " }"
cHtml += " function clearChat() {"
cHtml += " document.getElementById('chat-container').innerHTML = '';"
cHtml += " history_data = [];"
cHtml += " }"
cHtml += "</script>"
cHtml += "</body></html>"
UWrite(cHtml)
return ""aios.prg
// gemini_aios_v13.prg - ULTRA STABLE VERSION 1.7
// Final Robust Memory + Forced Context Awareness + hbcurl
#include "hbcurl.ch"
function main()
local cQuery, cModel, cHistory, cSystemPrompt := "", cJsonRes
local hResult := { "success" => .f. }, oErr, cOldMem, cSep, cMemPath, cBaseDir
cBaseDir := hb_DirBase()
cSep := "\" ; if !("\" $ cBaseDir) ; cSep := "/" ; endif
cQuery := UGet("query")
cModel := UGet("model")
cHistory := UGet("history")
if Empty(cQuery) ; cQuery := UPost("query") ; endif
if Empty(cModel) ; cModel := UPost("model") ; endif
if Empty(cHistory) ; cHistory := UPost("history") ; endif
// Clean data
if !Empty(cHistory) .and. "%" $ hb_ValToStr(cHistory) ; cHistory := Hix_UrlDecode(hb_ValToStr(cHistory)) ; endif
if !Empty(cQuery) .and. "%" $ hb_ValToStr(cQuery) ; cQuery := Hix_UrlDecode(hb_ValToStr(cQuery)) ; endif
if cQuery == "ping" ; UWrite('{"success":true, "text":"pong"}') ; return "" ; endif
if Empty(cQuery) ; UWrite('{"success":false, "error":"No query provided"}') ; return "" ; endif
cJsonRes := '{"success":false, "error":"Fatal Crash"}'
BEGIN SEQUENCE WITH {|e| break(e)}
if Empty(cModel) ; cModel := "gemini-2.0-flash" ; endif
// System Prompt - FORCED PERSONALITY
cSystemPrompt := "ERES HIX AIOS v1.7. ASISTENTE AVANZADO." + (Chr(13)+Chr(10))
cSystemPrompt += "TIENES ACCESO A MEMORIA PERSISTENTE QUE SE TE PROPORCIONA A CONTINUACIÓN." + (Chr(13)+Chr(10))
cSystemPrompt += "PROHIBIDO DECIR QUE NO TIENES MEMORIA." + (Chr(13)+Chr(10))
// Load Identity & Soul
if File(cBaseDir + "persona" + cSep + "IDENTITY.md")
cSystemPrompt += "TU IDENTIDAD:" + (Chr(13)+Chr(10)) + hb_MemoRead(cBaseDir + "persona" + cSep + "IDENTITY.md") + (Chr(13)+Chr(10))
endif
if File(cBaseDir + "persona" + cSep + "SOUL.md")
cSystemPrompt += "TU ALMA:" + (Chr(13)+Chr(10)) + hb_MemoRead(cBaseDir + "persona" + cSep + "SOUL.md") + (Chr(13)+Chr(10))
endif
// Load Memory (Last 8000 chars)
cMemPath := cBaseDir + "persona" + cSep + "MEMORY.md"
if File(cMemPath)
cOldMem := hb_MemoRead(cMemPath)
cSystemPrompt += "HISTORIAL DE CONVERSACIONES (MEMORIA):" + (Chr(13)+Chr(10)) + Right(cOldMem, 8000) + (Chr(13)+Chr(10))
endif
cSystemPrompt += "Si necesitas un chat_id para Telegram, BÚSCALO EN EL HISTORIAL ARRIBA." + (Chr(13)+Chr(10))
LogTrace("REQ: " + cQuery + " | PromptLen: " + AllTrim(Str(Len(cSystemPrompt))))
hResult := ExecuteReasoningLoop(cQuery, cModel, hb_ValToStr(cHistory), cSystemPrompt)
// Save to Memory
if ValType(hResult) == "H" .and. hb_HGetDef(hResult, 'success', .f.)
if !hb_DirExists(cBaseDir + "persona") ; hb_DirCreate(cBaseDir + "persona") ; endif
cOldMem := "" ; if File(cMemPath) ; cOldMem := hb_MemoRead(cMemPath) ; endif
hb_MemoWrit(cMemPath, cOldMem + "[" + Time() + "] User: " + cQuery + (Chr(13)+Chr(10)) + "HIX: " + hb_ValToStr(hResult['text']) + (Chr(13)+Chr(10)) + (Chr(13)+Chr(10)))
endif
hResult["v"] := "1.7"
cJsonRes := hb_jsonEncode(hResult)
RECOVER USING oErr
cJsonRes := '{"success":false, "error":"RTE: ' + hb_ValToStr(oErr:Description) + '", "v":"1.7-err"}'
END
UWrite(cJsonRes)
return ""
function LogTrace(cMsg)
local nH
nH := fOpen("aios_debug.log", 1)
if nH < 0 ; nH := fCreate("aios_debug.log") ; endif
if nH > 0
fSeek(nH, 0, 2)
fWrite(nH, "[" + DToC(Date()) + " " + Time() + "] v1.7: " + hb_ValToStr(cMsg) + (Chr(13)+Chr(10)))
fClose(nH)
endif
return nil
function ExecuteReasoningLoop(cQuery, cModel, cHistory, cSystemPrompt)
local aFunctions := BuildAiosFunctions()
local aMessages := {}, nStep := 0, hResult := { "success" => .f. }, hGeminiResult, hSkillResult, hMsg, aResponseParts, hPart, lHasFC
local cFullText := ""
if !Empty(cHistory) ; hb_jsonDecode(cHistory, @aMessages) ; endif
if ValType(aMessages) != "A" ; aMessages := {} ; endif
aAdd(aMessages, { "role" => "user", "parts" => { { "text" => cQuery } } })
do while nStep < 10 // Increase steps for safety
nStep++
hGeminiResult := GeminiCallFC(aMessages, aFunctions, cModel, cSystemPrompt)
if ValType(hGeminiResult) != "H" .or. !hb_HGetDef(hGeminiResult,"success",.f.)
if nStep > 1 .and. !Empty(cFullText)
// If it fails after Turn 1 but we have text, return it
hResult['success'] := .t. ; hResult['text'] := cFullText ; hResult['model_used'] := cModel ; retu hResult
endif
hResult['success'] := .f. ; hResult['error'] := hb_ValToStr(hb_HGetDef(hGeminiResult, "error", "API Fail"))
LogTrace("LOOP ERR: " + hResult['error'])
retu hResult
endif
hMsg := { "role" => "model", "parts" => hGeminiResult["raw_parts"] }
aAdd(aMessages, hMsg)
// Accumulate text from this turn
if !Empty(hGeminiResult['text'])
cFullText += hGeminiResult['text']
endif
if hGeminiResult['type'] == "function_call"
aResponseParts := {}
for each hPart in hGeminiResult["raw_parts"]
if ValType(hPart) == "H" .and. hb_HHasKey(hPart, "functionCall")
hSkillResult := ExecuteAiosSkill(hPart["functionCall"]["name"], hPart["functionCall"]["args"])
aAdd(aResponseParts, { "functionResponse" => { "name" => hPart["functionCall"]["name"], "response" => hSkillResult } } )
endif
next
aAdd(aMessages, { "role" => "function", "parts" => aResponseParts })
elseif hGeminiResult['type'] == "text"
hResult['success'] := .t. ; hResult['text'] := cFullText ; hResult['model_used'] := cModel ; retu hResult
else
// Type is 'empty' or unknown, but if we have text, we are done
hResult['success'] := .t. ; hResult['text'] := iif(Empty(cFullText), "OK", cFullText) ; hResult['model_used'] := cModel ; retu hResult
endif
enddo
return hResult
function GeminiCallFC(aMessages, aFunctions, cModel, cSystemPrompt)
local cApiKey := GetAiosApiKey()
local cUrl, cJson, hCurl, nError, cResponse := "", hResult := { "success" => .f. }
local hPayload := { "contents" => aMessages, "tools" => { { "function_declarations" => aFunctions } } }
if !Empty(cSystemPrompt) ; hPayload["system_instruction"] := { "parts" => { { "text" => cSystemPrompt } } } ; endif
cUrl := "https://generativelanguage.googleapis.com/v1beta/models/" + cModel + ":generateContent?key=" + cApiKey
cJson := hb_jsonEncode(hPayload)
hCurl := curl_easy_init()
if !Empty(hCurl)
curl_easy_setopt(hCurl, HB_CURLOPT_POST, .T.)
curl_easy_setopt(hCurl, HB_CURLOPT_URL, cUrl)
curl_easy_setopt(hCurl, HB_CURLOPT_HTTPHEADER, { "Content-Type: application/json" })
curl_easy_setopt(hCurl, HB_CURLOPT_SSL_VERIFYPEER, .F.)
curl_easy_setopt(hCurl, HB_CURLOPT_POSTFIELDS, cJson)
curl_easy_setopt(hCurl, HB_CURLOPT_DL_BUFF_SETUP)
nError := curl_easy_perform(hCurl)
if nError == HB_CURLE_OK
cResponse := curl_easy_dl_buff_get(hCurl)
LogTrace("RECV: " + Left(cResponse, 500) + " (len: " + AllTrim(Str(Len(cResponse))) + ")")
hResult := ParseAiosFCResponse(cResponse)
if !hResult['success'] ; LogTrace("PARSE ERR: " + hb_ValToStr(hResult['error'])) ; endif
else
hResult['error'] := "Curl error: " + hb_ValToStr(nError)
LogTrace("CURL ERR: " + AllTrim(Str(nError)))
endif
curl_easy_cleanup(hCurl)
else
hResult['error'] := "Curl init failed"
endif
retu hResult
function ParseAiosFCResponse(cJSON)
local hResult := { "success" => .f. }, hResponse := {=>}, aParts, nErr := 0, cW, nS, nE, hPart, lHasFC
cW := AllTrim(StrTran(hb_ValToStr(cJSON), Chr(0), ""))
nS := At("{", cW) ; nE := RAt("}", cW)
if nS > 0 .and. nE > nS ; cW := SubStr(cW, nS, nE - nS + 1) ; endif
if Empty(cW) ; hResult['error'] := "Empty response" ; return hResult ; endif
nErr := hb_jsonDecode(cW, @hResponse)
if (nErr == 0 .or. nErr == Len(cW)) .and. ValType(hResponse) == "H"
if hb_HHasKey(hResponse, "candidates") .and. Len(hResponse["candidates"]) > 0
aParts := hResponse["candidates"][1]["content"]["parts"]
hResult['success'] := .t. ; hResult['raw_parts'] := aParts ; lHasFC := .f.
hResult['text'] := ""
hResult['type'] := 'unknown'
for each hPart in aParts
if ValType(hPart) == "H"
if hb_HHasKey(hPart, "functionCall") ; lHasFC := .t. ; endif
if hb_HHasKey(hPart, "text") ; hResult['text'] += hPart["text"] ; endif
endif
next
if lHasFC
hResult['type'] := 'function_call'
elseif !Empty(hResult['text'])
hResult['type'] := 'text'
endif
elseif hb_HHasKey(hResponse, "error")
hResult['error'] := "API Error: " + hb_ValToStr(hResponse["error"]["message"])
elseif hb_HHasKey(hResponse, "usageMetadata")
// Valid response but empty of content (happens after tool turns sometimes)
hResult['success'] := .t. ; hResult['type'] := 'empty' ; hResult['raw_parts'] := {} ; hResult['text'] := ""
else
hResult['error'] := "Invalid API Response Structure"
endif
else
hResult['error'] := "JSON Parse Error (Code: " + AllTrim(Str(nErr)) + ")"
endif
retu hResult
function BuildAiosFunctions()
local aFuncs := {}
aAdd(aFuncs, { "name" => "filesystem_search", "description" => "Search files.", "parameters" => { "type" => "object", "properties" => { "pattern" => { "type" => "string" }, "path" => { "type" => "string" } }, "required" => {"pattern", "path"} } })
aAdd(aFuncs, { "name" => "identity_get_context", "description" => "Identity info.", "parameters" => { "type" => "object", "properties" => {=>}, "required" => {} } })
aAdd(aFuncs, { "name" => "telegram_send_message", "description" => "Send Telegram Msg.", "parameters" => { "type" => "object", "properties" => { "chat_id" => { "type" => "string" }, "text" => { "type" => "string" } }, "required" => {"chat_id", "text"} } })
aAdd(aFuncs, { "name" => "telegram_get_updates", "description" => "Read incoming Telegram messages.", "parameters" => { "type" => "object", "properties" => {=>}, "required" => {} } })
aAdd(aFuncs, { "name" => "config_set", "description" => "Save key-value pair.", "parameters" => { "type" => "object", "properties" => { "key" => { "type" => "string" }, "value" => { "type" => "string" } }, "required" => {"key", "value"} } })
aAdd(aFuncs, { "name" => "config_get", "description" => "Get saved info.", "parameters" => { "type" => "object", "properties" => { "key" => { "type" => "string", "description" => "Optional key" } }, "required" => {} } })
aAdd(aFuncs, { "name" => "memory_summarize", "description" => "Optimize/Summarize conversation history.", "parameters" => { "type" => "object", "properties" => { "summary" => { "type" => "string" } }, "required" => {"summary"} } })
aAdd(aFuncs, { "name" => "filesystem_get_datetime", "description" => "Get current date, time and day of week.", "parameters" => { "type" => "object", "properties" => {=>}, "required" => {} } })
aAdd(aFuncs, { "name" => "identity_update", "description" => "Update IDENTITY.md content.", "parameters" => { "type" => "object", "properties" => { "content" => { "type" => "string" } }, "required" => {"content"} } })
aAdd(aFuncs, { "name" => "soul_update", "description" => "Update SOUL.md content.", "parameters" => { "type" => "object", "properties" => { "content" => { "type" => "string" } }, "required" => {"content"} } })
aAdd(aFuncs, { "name" => "cron_add_reminder", "description" => "Schedule a reminder message.", "parameters" => { "type" => "object", "properties" => { "message" => { "type" => "string" }, "minutes" => { "type" => "number" }, "chat_id" => { "type" => "string" } }, "required" => {"message", "minutes"} } })
retu aFuncs
function ExecuteAiosSkill(cName, hArgs)
local hRes := { "success" => .f. }
do case
case cName == 'filesystem_search' ; hRes := Aios_FileSystem(cName, hArgs)
case cName == 'identity_get_context' ; hRes := Aios_Identity()
case cName == 'telegram_send_message' ; hRes := Aios_Telegram(cName, hArgs)
case cName == 'telegram_get_updates' ; hRes := Aios_Telegram(cName, hArgs)
case cName == 'config_set' ; hRes := Aios_Config('config_set', hArgs)
case cName == 'config_get' ; hRes := Aios_Config('config_get', hArgs)
case cName == 'memory_summarize' ; hRes := Aios_MemorySummarize(hArgs)
case cName == 'filesystem_get_datetime' ; hRes := Aios_FileSystem(cName, hArgs)
case cName == 'identity_update' ; hRes := Aios_PersonaUpdate('identity', hArgs)
case cName == 'soul_update' ; hRes := Aios_PersonaUpdate('soul', hArgs)
case cName == 'cron_add_reminder' ; hRes := Aios_Cron(cName, hArgs)
endcase
retu hRes
#include "skills\identity\identity.prg"
#include "skills\filesystem\hix_filesystem.prg"
#include "skills\telegram\telegram.prg"
#include "skills\config\config_skill.prg"
#include "skills\cron\cron_skill.prg"
function GetAiosApiKey()
local cKey := GetEnv("GEMINI_API_KEY"), hCfg := {=>}
if Empty(cKey) .and. File("gemini_config.json")
hb_jsonDecode(hb_MemoRead("gemini_config.json"), @hCfg)
if ValType(hCfg) == "H" ; cKey := hb_HGetDef(hCfg, "api_key", "") ; endif
endif
retu cKey
function Hix_UrlDecode( cT )
local cR := "", n := 1, cX
if Empty(cT) ; return "" ; endif
cT := StrTran( cT, "+", " " )
do while n <= Len( cT )
if SubStr( cT, n, 1 ) == "%"
cX := SubStr( cT, n + 1, 2 ) ; cR += Chr( Hix_HexToNum( cX ) ) ; n += 3
else ; cR += SubStr( cT, n, 1 ) ; n++ ; endif
enddo
retu cR
function Hix_HexToNum( cX )
local n := 0, i, cC, nV
cX := Upper( cX )
for i := 1 to Len( cX )
cC := SubStr( cX, i, 1 ) ; nV := At( cC, "0123456789ABCDEF" ) - 1
if nV >= 0 ; n := n * 16 + nV ; endif
next
retu n